카테고리 없음

Thymeleaf 가이드 - #4. 표준 표현식 심화

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

4장. 표준 표현식 심화

4.1 조건 연산자와 삼항 연산자

Thymeleaf에서 조건부 로직을 처리하는 다양한 방법들을 살펴보겠습니다.

기본 삼항 연산자

<div class="conditional-examples">
    <!-- 기본 삼항 연산자 (condition ? trueValue : falseValue) -->
    <p th:text="${user.age >= 18} ? '성인' : '미성년자'">연령 구분</p>
    <p th:text="${user.isActive()} ? '활성' : '비활성'">계정 상태</p>

    <!-- 숫자 조건 -->
    <span th:class="${product.stock > 0} ? 'in-stock' : 'out-of-stock'"
          th:text="${product.stock > 0} ? '재고있음' : '품절'">재고 상태</span>

    <!-- 문자열 조건 -->
    <p th:text="${#strings.isEmpty(user.nickname)} ? user.name : user.nickname">표시명</p>

    <!-- null 조건 -->
    <img th:src="${user.profileImage != null} ? ${user.profileImage} : '/images/default.png'"
         th:alt="${user.name}">
</div>

중첩된 삼항 연산자

<div class="nested-conditionals">
    <!-- 다중 조건 분기 -->
    <span th:class="${user.role == 'admin'} ? 'badge-admin' : 
                   (${user.role == 'moderator'} ? 'badge-moderator' : 'badge-user')"
          th:text="${user.role}">역할</span>

    <!-- 점수 기반 등급 -->
    <div class="grade-display">
        <span th:text="${user.score >= 90} ? 'A' : 
                       (${user.score >= 80} ? 'B' : 
                       (${user.score >= 70} ? 'C' : 
                       (${user.score >= 60} ? 'D' : 'F')))">등급</span>

        <span th:style="${user.score >= 90} ? 'color: gold;' : 
                        (${user.score >= 80} ? 'color: silver;' : 
                        (${user.score >= 70} ? 'color: bronze;' : 'color: gray;'))">
            ★
        </span>
    </div>

    <!-- 복잡한 상태 판단 -->
    <div class="user-status">
        <span th:text="${user.isActive()} ? 
                       (${user.isVerified()} ? '정상 사용자' : '미인증 사용자') : 
                       (${user.isSuspended()} ? '정지된 사용자' : '비활성 사용자')">
            사용자 상태
        </span>
    </div>

    <!-- 날짜 기반 조건 -->
    <div class="membership-status" th:with="daysSinceJoin=${#temporals.daysBetween(user.joinDate, #temporals.createNow())}">
        <span th:text="${daysSinceJoin < 7} ? '신규 회원' : 
                       (${daysSinceJoin < 30} ? '새 회원' : 
                       (${daysSinceJoin < 365} ? '일반 회원' : '베테랑 회원'))">
            회원 등급
        </span>
    </div>
</div>

조건부 HTML 속성

<div class="conditional-attributes">
    <!-- 조건부 클래스 -->
    <div th:class="${user.isOnline()} ? 'user-card online' : 'user-card offline'">
        <h3 th:text="${user.name}">사용자명</h3>
    </div>

    <!-- 조건부 스타일 -->
    <div th:style="${user.isPremium()} ? 'border: 2px solid gold;' : 'border: 1px solid gray;'">
        프리미엄 사용자 카드
    </div>

    <!-- 조건부 링크 -->
    <a th:href="${user.hasProfile()} ? @{/user/{id}/profile(id=${user.id})} : @{/user/{id}/setup(id=${user.id})}"
       th:text="${user.hasProfile()} ? '프로필 보기' : '프로필 설정'">링크</a>

    <!-- 조건부 이벤트 핸들러 -->
    <button th:onclick="${user.canEdit()} ? |editUser(${user.id})| : 'alert(\'권한이 없습니다\')'">
        편집
    </button>

    <!-- 조건부 데이터 속성 -->
    <div th:attr="data-status=${user.isActive()} ? 'active' : 'inactive',
                  data-role=${user.role},
                  data-premium=${user.isPremium()} ? 'yes' : 'no'">
        데이터 속성 예제
    </div>
</div>

4.2 Elvis 연산자와 No-Operation 토큰

Elvis 연산자(?:)와 No-Operation 토큰(_)은 더 간결한 조건 처리를 가능하게 합니다.

Elvis 연산자 기본 사용법

<div class="elvis-examples">
    <!-- 기본 Elvis 연산자 (leftExpression ?: rightExpression) -->
    <h3 th:text="${user.nickname ?: user.name}">표시명</h3>
    <p th:text="${user.bio ?: '자기소개가 없습니다.'}">자기소개</p>

    <!-- null이나 빈 문자열 처리 -->
    <p th:text="${user.company ?: '회사 정보 없음'}">회사</p>
    <p th:text="${user.website ?: '#'}">웹사이트</p>

    <!-- 숫자 기본값 -->
    <p>점수: <span th:text="${user.score ?: 0}">0</span>점</p>
    <p>레벨: <span th:text="${user.level ?: 1}">1</span></p>

    <!-- 컬렉션 기본값 -->
    <p>취미 개수: <span th:text="${#lists.size(user.hobbies ?: {})}">0</span>개</p>
</div>

체인된 Elvis 연산자

<div class="chained-elvis">
    <!-- 여러 값 중 첫 번째 null이 아닌 값 -->
    <h3 th:text="${user.displayName ?: user.nickname ?: user.firstName ?: 'Unknown'}">
        표시명
    </h3>

    <!-- 연락처 우선순위 -->
    <p>연락처: <span th:text="${user.mobile ?: user.phone ?: user.email ?: '연락처 없음'}">연락처</span></p>

    <!-- 이미지 fallback -->
    <img th:src="${user.avatar ?: user.profilePicture ?: '/images/default-avatar.png'}" 
         th:alt="${user.name}">

    <!-- 주소 정보 -->
    <p th:text="${user.address?.fullAddress ?: user.address?.city ?: '주소 미입력'}">주소</p>

    <!-- 소셜 링크 -->
    <a th:href="${user.website ?: user.blog ?: user.socialMedia ?: '#'}"
       th:text="${user.website ?: user.blog ?: user.socialMedia ?: 'No Link'}">링크</a>
</div>

Safe Navigation과 Elvis 연산자 조합

<div class="safe-navigation-elvis">
    <!-- 중첩 객체의 안전한 접근 -->
    <p th:text="${user.profile?.company?.name ?: '소속 없음'}">회사명</p>
    <p th:text="${user.settings?.privacy?.email ?: 'public'}">이메일 공개 설정</p>

    <!-- 컬렉션의 안전한 접근 -->
    <p th:text="${user.orders?[0]?.orderNumber ?: '주문 없음'}">최근 주문번호</p>
    <p th:text="${user.projects?[0]?.title ?: '프로젝트 없음'}">최근 프로젝트</p>

    <!-- 메소드 체이닝과 Elvis -->
    <p th:text="${user.getLatestPost()?.getTitle()?.toUpperCase() ?: 'NO POSTS'}">
        최신 게시글 (대문자)
    </p>

    <!-- 날짜 포맷과 Elvis -->
    <p th:text="${user.lastLogin != null} ? ${#temporals.format(user.lastLogin, 'yyyy-MM-dd')} : '로그인 기록 없음'">
        마지막 로그인
    </p>

    <!-- Safe Navigation + Elvis + 포맷팅 -->
    <p th:text="${#temporals.format(user.profile?.lastUpdate, 'yyyy-MM-dd') ?: '업데이트 없음'}">
        프로필 마지막 업데이트
    </p>
</div>

No-Operation 토큰 활용

<div class="no-operation-examples">
    <!-- 조건부 텍스트 표시 (조건이 거짓이면 원래 텍스트 유지) -->
    <p th:text="${user.isVip()} ? 'VIP 회원' : _">일반 텍스트</p>
    <h2 th:text="${pageTitle} ?: _">기본 페이지 제목</h2>

    <!-- 조건부 속성 설정 -->
    <div th:class="${user.isPremium()} ? 'premium-badge' : _"
         th:attr="data-premium=${user.isPremium()} ? 'true' : _">
        조건부 속성
    </div>

    <!-- 조건부 링크 -->
    <a th:href="${user.hasPermission('ADMIN')} ? @{/admin} : _"
       th:text="관리자 페이지">관리자 페이지</a>

    <!-- 조건부 이벤트 -->
    <button th:onclick="${user.canDelete()} ? |deleteItem(${item.id})| : _">삭제</button>

    <!-- 조건부 스타일 -->
    <div th:style="${item.isHighlighted()} ? 'background-color: yellow;' : _">
        하이라이트 항목
    </div>

    <!-- 복잡한 조건부 처리 -->
    <div class="notification" 
         th:class="${notification.isUrgent()} ? 'notification urgent' : 'notification'"
         th:attr="data-sound=${notification.hasSound()} ? 'enabled' : _,
                  data-popup=${notification.isPopup()} ? 'true' : _">
        <span th:text="${notification.message}">알림 메시지</span>
        <button th:onclick="${notification.isDismissible()} ? |dismiss(${notification.id})| : _">
            닫기
        </button>
    </div>
</div>

4.3 문자열 연산과 비교 연산

문자열 연산

<div class="string-operations">
    <!-- 기본 문자열 연결 -->
    <p th:text="'Hello' + ' ' + 'World'">Hello World</p>
    <p th:text="${user.firstName} + ' ' + ${user.lastName}">전체 이름</p>

    <!-- 문자열 템플릿 (권장 방법) -->
    <p th:text="|안녕하세요, ${user.name}님!|">인사말</p>
    <p th:text="|${user.name} (${user.email})|">사용자 정보</p>

    <!-- 복잡한 문자열 구성 -->
    <p th:text="|주문 #${order.id}: ${order.status} - ${#numbers.formatCurrency(order.total)}|">
        주문 정보
    </p>

    <!-- 조건부 문자열 구성 -->
    <p th:text="|상태: ${user.isActive() ? '활성' : '비활성'} | 레벨: ${user.level ?: 1}|">
        사용자 상태 정보
    </p>

    <!-- 문자열과 숫자 혼합 -->
    <p th:text="|총 ${#lists.size(products)}개 상품 중 ${#lists.size(products.?[inStock])}개 재고 있음|">
        재고 현황
    </p>

    <!-- URL과 문자열 조합 -->
    <a th:href="|/user/${user.id}/posts?page=${currentPage}|" 
       th:text="|${user.name}의 게시글 보기|">게시글 링크</a>

    <!-- CSS 클래스명 동적 생성 -->
    <div th:class="|user-card ${user.role} ${user.isOnline() ? 'online' : 'offline'}|">
        동적 클래스
    </div>

    <!-- 이미지 경로 생성 -->
    <img th:src="|/images/avatars/${user.id}_${user.avatarVersion ?: 'default'}.jpg|" 
         th:alt="|${user.name} 아바타|">
</div>

문자열 비교 연산

<div class="string-comparison">
    <!-- 기본 문자열 비교 -->
    <p th:if="${user.role == 'admin'}">관리자입니다.</p>
    <p th:if="${user.status != 'banned'}">정상 사용자입니다.</p>

    <!-- 대소문자 무시 비교 -->
    <p th:if="${#strings.equalsIgnoreCase(user.role, 'ADMIN')}">
        관리자 권한 (대소문자 무시)
    </p>

    <!-- 문자열 포함 검사 -->
    <div th:if="${#strings.contains(user.email, '@gmail.com')}">
        Gmail 사용자입니다.
    </div>

    <!-- 문자열 시작/끝 검사 -->
    <div th:if="${#strings.startsWith(user.phone, '010')}">
        휴대폰 번호입니다.
    </div>

    <div th:if="${#strings.endsWith(user.website, '.com')}">
        .com 도메인입니다.
    </div>

    <!-- 빈 문자열 검사 -->
    <div th:if="${#strings.isEmpty(user.bio)}">
        자기소개가 비어있습니다.
    </div>

    <div th:unless="${#strings.isEmpty(user.nickname)}">
        닉네임: <span th:text="${user.nickname}">닉네임</span>
    </div>

    <!-- 문자열 길이 비교 -->
    <div th:if="${#strings.length(user.password) < 8}">
        비밀번호가 너무 짧습니다.
    </div>

    <!-- 정규식 매칭 -->
    <div class="validation-results">
        <p th:if="${#strings.matches(user.email, '^[A-Za-z0-9+_.-]+@(.+))}">
            유효한 이메일 형식입니다.
        </p>

        <p th:if="${#strings.matches(user.phone, '^01[016789]-?[0-9]{3,4}-?[0-9]{4})}">
            유효한 휴대폰 번호입니다.
        </p>

        <p th:if="${#strings.matches(user.zipCode, '^[0-9]{5})}">
            유효한 우편번호입니다.
        </p>
    </div>

    <!-- 복합 문자열 조건 -->
    <div th:if="${#strings.contains(user.email, 'admin') and #strings.endsWith(user.email, '.com')}">
        관리자 이메일로 추정됩니다.
    </div>
</div>

고급 문자열 처리

<div class="advanced-string-operations">
    <!-- 문자열 변환 -->
    <p th:text="${#strings.toUpperCase(user.name)}">이름 (대문자)</p>
    <p th:text="${#strings.toLowerCase(user.email)}">이메일 (소문자)</p>
    <p th:text="${#strings.capitalizeWords(user.address)}">주소 (단어별 대문자)</p>

    <!-- 문자열 자르기 -->
    <p th:text="${#strings.substring(user.bio, 0, 100)} + ${#strings.length(user.bio) > 100 ? '...' : ''}">
        자기소개 미리보기
    </p>

    <!-- 문자열 분할과 조인 -->
    <div th:with="tags=${#strings.listSplit(user.tags, ',')}">
        <span th:each="tag, stat : ${tags}">
            <span th:text="${#strings.trim(tag)}">태그</span>
            <span th:if="${!stat.last}">, </span>
        </span>
    </div>

    <!-- 문자열 치환 -->
    <p th:text="${#strings.replace(user.bio, '\n', '<br>')}">
        줄바꿈 처리된 자기소개
    </p>

    <!-- 문자열 패딩 -->
    <div class="user-id">
        ID: <span th:text="${#strings.leftPad(#strings.toString(user.id), 6, '0')}">000001</span>
    </div>

    <!-- 문자열 트림 -->
    <p th:text="${#strings.trim(user.company)}">회사명 (공백 제거)</p>

    <!-- 문자열 null 안전 처리 -->
    <p th:text="${#strings.defaultString(user.nickname, '닉네임 없음')}">닉네임</p>

    <!-- 복잡한 문자열 가공 -->
    <div class="formatted-address" th:if="${user.address}">
        <span th:text="${#strings.abbreviate(#strings.capitalizeWords(user.address.street), 20)}">
            주소
        </span>
        <br>
        <span th:text="${#strings.toUpperCase(user.address.city)}">도시</span>
        <span th:text="${user.address.zipCode}">우편번호</span>
    </div>
</div>

4.4 산술 연산과 논리 연산

산술 연산 심화

<div class="advanced-arithmetic">
    <!-- 기본 산술 연산 -->
    <div class="price-calculation">
        <p>상품 가격: <span th:text="${product.basePrice}">10000</span>원</p>
        <p>할인율: <span th:text="${product.discountRate}">10</span>%</p>
        <p>할인 금액: <span th:text="${product.basePrice * product.discountRate / 100}">1000</span>원</p>
        <p>최종 가격: <span th:text="${product.basePrice * (100 - product.discountRate) / 100}">9000</span>원</p>
    </div>

    <!-- 복잡한 계산 -->
    <div class="shopping-cart" th:with="total=${#aggregates.sum(cartItems.![quantity * price])}">
        <div th:each="item : ${cartItems}">
            <span th:text="${item.name}">상품명</span>:
            <span th:text="${item.quantity}">2</span> × 
            <span th:text="${#numbers.formatCurrency(item.price)}">₩10,000</span> = 
            <span th:text="${#numbers.formatCurrency(item.quantity * item.price)}">₩20,000</span>
        </div>

        <div class="cart-summary">
            <p>소계: <span th:text="${#numbers.formatCurrency(total)}">₩50,000</span></p>
            <p>배송비: <span th:text="${total >= 30000} ? '무료' : '₩3,000'">무료</span></p>
            <p>총합계: 
                <span th:text="${#numbers.formatCurrency(total + (total >= 30000 ? 0 : 3000))}">
                    ₩50,000
                </span>
            </p>
        </div>
    </div>

    <!-- 수학 함수 활용 -->
    <div class="math-functions">
        <!-- 반올림 -->
        <p>평균 점수: <span th:text="${#numbers.formatDecimal(totalScore / studentCount, 1, 2)}">85.67</span></p>

        <!-- 최대/최소값 -->
        <p>최고 점수: <span th:text="${#aggregates.max(scores)}">95</span></p>
        <p>최저 점수: <span th:text="${#aggregates.min(scores)}">72</span></p>

        <!-- 절댓값 -->
        <p>차이: <span th:text="${#numbers.formatInteger(#math.abs(score1 - score2), 0, 'COMMA')}">15</span></p>

        <!-- 거듭제곱과 제곱근 -->
        <p>제곱: <span th:text="${value * value}">25</span></p>
        <p>세제곱: <span th:text="${value * value * value}">125</span></p>
    </div>

    <!-- 날짜 계산 -->
    <div class="date-calculations" th:with="now=${#dates.createNow()}">
        <p>가입한지: 
            <span th:text="${#dates.daysBetween(user.joinDate, now)}">30</span>일 경과
        </p>

        <p>다음 결제일까지: 
            <span th:text="${#dates.daysBetween(now, user.nextPaymentDate)}">15</span>일 남음
        </p>

        <!-- 나이 계산 -->
        <p>나이: 
            <span th:text="${#dates.year(now) - #dates.year(user.birthDate)}">25</span>세
        </p>

        <!-- 근무 연차 계산 -->
        <p>근무 연차: 
            <span th:text="${(#dates.daysBetween(user.hireDate, now) / 365.0) + 1}">3.2</span>년
        </p>
    </div>
</div>

논리 연산 심화

<div class="advanced-logical-operations">
    <!-- 복합 논리 조건 -->
    <div class="access-control">
        <!-- 관리자이거나 자신의 프로필인 경우 -->
        <div th:if="${currentUser.isAdmin() or currentUser.id == user.id}">
            <button>편집</button>
            <button>삭제</button>
        </div>

        <!-- 활성 상태이고 인증된 사용자만 -->
        <div th:if="${user.isActive() and user.isVerified() and not user.isSuspended()}">
            <p>모든 기능을 사용할 수 있습니다.</p>
        </div>

        <!-- 프리미엄이거나 높은 점수를 가진 사용자 -->
        <div th:if="${user.isPremium() or user.score >= 90}">
            <div class="premium-features">
                <p>프리미엄 기능 이용 가능</p>
            </div>
        </div>
    </div>

    <!-- 날짜 기반 논리 연산 -->
    <div class="date-based-logic" th:with="now=${#temporals.createNow()}">
        <!-- 신규 사용자 (가입 후 7일 이내) -->
        <div th:if="${#temporals.daysBetween(user.joinDate, now) <= 7}">
            <div class="welcome-message">
                <p>신규 회원 환영합니다! 🎉</p>
            </div>
        </div>

        <!-- 오랫동안 활동하지 않은 사용자 -->
        <div th:if="${user.lastLoginDate != null and #temporals.daysBetween(user.lastLoginDate, now) > 30}">
            <div class="inactive-warning">
                <p>오랫동안 로그인하지 않았습니다. 계정을 확인해주세요.</p>
            </div>
        </div>

        <!-- 구독 만료 임박 -->
        <div th:if="${user.subscriptionEndDate != null and #temporals.daysBetween(now, user.subscriptionEndDate) <= 7 and #temporals.daysBetween(now, user.subscriptionEndDate) >= 0}">
            <div class="subscription-warning">
                <p>구독이 곧 만료됩니다. 갱신해주세요.</p>
            </div>
        </div>
    </div>

    <!-- 컬렉션 기반 논리 연산 -->
    <div class="collection-based-logic">
        <!-- 빈 컬렉션 검사 -->
        <div th:if="${#lists.isEmpty(user.orders)}">
            <p>아직 주문 내역이 없습니다.</p>
        </div>

        <!-- 컬렉션 크기 조건 -->
        <div th:if="${#lists.size(user.friends) >= 10}">
            <div class="social-badge">
                <p>인기 사용자 배지 🌟</p>
            </div>
        </div>

        <!-- 컬렉션 내용 검사 -->
        <div th:if="${#lists.contains(user.roles, 'EDITOR')}">
            <button>글쓰기</button>
        </div>

        <!-- 모든/일부 조건 -->
        <div th:if="${not #lists.isEmpty(user.projects) and #lists.size(user.projects.?[completed]) == #lists.size(user.projects)}">
            <div class="completion-badge">
                <p>모든 프로젝트 완료! 🎯</p>
            </div>
        </div>
    </div>

    <!-- 문자열 기반 논리 연산 -->
    <div class="string-based-logic">
        <!-- 이메일 도메인 검사 -->
        <div th:if="${#strings.contains(user.email, '@company.com') or #strings.contains(user.email, '@partner.com')}">
            <div class="corporate-user">
                <p>기업 사용자입니다.</p>
            </div>
        </div>

        <!-- 전화번호 유효성과 지역 검사 -->
        <div th:if="${not #strings.isEmpty(user.phone) and (#strings.startsWith(user.phone, '02') or #strings.startsWith(user.phone, '010'))}">
            <p>서울 또는 휴대폰 번호입니다.</p>
        </div>

        <!-- 사용자명 길이와 형식 검사 -->
        <div th:if="${#strings.length(user.username) >= 3 and #strings.length(user.username) <= 20 and #strings.matches(user.username, '^[a-zA-Z0-9_]+)}">
            <p>유효한 사용자명입니다.</p>
        </div>
    </div>
</div>

4.5 정규 표현식 활용

기본 정규식 매칭

<div class="regex-examples">
    <!-- 이메일 유효성 검사 -->
    <div class="email-validation">
        <p th:if="${#strings.matches(user.email, '^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,}))}">
            ✅ 유효한 이메일 주소
        </p>
        <p th:unless="${#strings.matches(user.email, '^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,}))}">
            ❌ 이메일 형식이 올바르지 않습니다
        </p>
    </div>

    <!-- 전화번호 유효성 검사 -->
    <div class="phone-validation">
        <div th:switch="${true}">
            <p th:case="${#strings.matches(user.phone, '^01[016789]-?[0-9]{3,4}-?[0-9]{4})}">
                📱 유효한 휴대폰 번호
            </p>
            <p th:case="${#strings.matches(user.phone, '^0[2-9][0-9]?-?[0-9]{3,4}-?[0-9]{4})}">
                📞 유효한 일반 전화번호
            </p>
            <p th:case="*">
                ❌ 전화번호 형식이 올바르지 않습니다
            </p>
        </div>
    </div>

    <!-- 비밀번호 강도 검사 -->
    <div class="password-strength" th:with="password=${userForm.password}">
        <div class="strength-indicators">
            <span th:class="${#strings.matches(password, '.*[a-z].*')} ? 'valid' : 'invalid'">소문자</span>
            <span th:class="${#strings.matches(password, '.*[A-Z].*')} ? 'valid' : 'invalid'">대문자</span>
            <span th:class="${#strings.matches(password, '.*[0-9].*')} ? 'valid' : 'invalid'">숫자</span>
            <span th:class="${#strings.matches(password, '.*[!@#$%^&*(),.?\":{}|<>].*')} ? 'valid' : 'invalid'">특수문자</span>
            <span th:class="${#strings.length(password) >= 8} ? 'valid' : 'invalid'">8자 이상</span>
        </div>

        <!-- 전체 강도 평가 -->
        <div th:with="strongPassword=${#strings.matches(password, '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*(),.?\":{}|<>]).{8,})}">
            <p th:if="${strongPassword}" class="strong">🔒 강력한 비밀번호</p>
            <p th:unless="${strongPassword}" class="weak">⚠️ 비밀번호를 더 강력하게 만드세요</p>
        </div>
    </div>

    <!-- URL 유효성 검사 -->
    <div class="url-validation">
        <a th:if="${#strings.matches(user.website, '^https?://.*)}" 
           th:href="${user.website}" 
           th:text="${user.website}">웹사이트</a>

        <span th:unless="${#strings.matches(user.website, '^https?://.*)}"
              th:text="${user.website}">유효하지 않은 URL</span>
    </div>
</div>

입력 값 검증과 형식화

<div class="input-validation">
    <!-- 사용자명 검증 -->
    <div class="username-validation" th:with="username=${userForm.username}">
        <div class="validation-rules">
            <p th:class="${#strings.matches(username, '^[a-zA-Z0-9_]{3,20})} ? 'valid' : 'invalid'">
                영문, 숫자, 밑줄만 사용, 3-20자
            </p>
            <p th:class="${#strings.matches(username, '^[a-zA-Z].*')} ? 'valid' : 'invalid'">
                영문으로 시작
            </p>
            <p th:class="${not #strings.matches(username, '.*[_]{2,}.*')} ? 'valid' : 'invalid'">
                연속된 밑줄 사용 금지
            </p>
        </div>
    </div>

    <!-- 주민등록번호 검증 (예시) -->
    <div class="ssn-validation" th:if="${userForm.ssn}">
        <p th:if="${#strings.matches(userForm.ssn, '^[0-9]{6}-[1-4][0-9]{6})}" class="valid">
            ✅ 올바른 주민등록번호 형식
        </p>
        <p th:unless="${#strings.matches(userForm.ssn, '^[0-9]{6}-[1-4][0-9]{6})}" class="invalid">
            ❌ 주민등록번호 형식이 올바르지 않습니다
        </p>
    </div>

    <!-- 신용카드 번호 검증 -->
    <div class="card-validation" th:if="${paymentForm.cardNumber}">
        <div th:switch="${true}">
            <p th:case="${#strings.matches(paymentForm.cardNumber, '^4[0-9]{12}(?:[0-9]{3})?)}">
                💳 Visa 카드
            </p>
            <p th:case="${#strings.matches(paymentForm.cardNumber, '^5[1-5][0-9]{14})}">
                💳 MasterCard
            </p>
            <p th:case="${#strings.matches(paymentForm.cardNumber, '^3[47][0-9]{13})}">
                💳 American Express
            </p>
            <p th:case="*" class="invalid">
                ❌ 지원하지 않는 카드입니다
            </p>
        </div>
    </div>

    <!-- IP 주소 검증 -->
    <div class="ip-validation" th:if="${serverConfig.ipAddress}">
        <p th:if="${#strings.matches(serverConfig.ipAddress, '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))}" class="valid">
            🌐 유효한 IPv4 주소
        </p>
    </div>
</div>

텍스트 추출과 변환

<div class="text-extraction">
    <!-- 해시태그 추출 -->
    <div class="hashtag-extraction" th:if="${post.content}">
        <div class="hashtags">
            <!-- JavaScript로 처리하는 예시 (Thymeleaf에서는 복잡한 정규식 추출이 제한적) -->
            <span th:text="${post.content}" style="display: none;" th:attr="data-content=${post.content}"></span>
            <div id="extracted-hashtags"></div>
        </div>

        <!-- 멘션 추출 -->
        <div class="mentions">
            <span th:if="${#strings.contains(post.content, '@')}" class="has-mentions">
                이 게시글에 멘션이 포함되어 있습니다.
            </span>
        </div>
    </div>

    <!-- 코드 블록 감지 -->
    <div class="code-detection" th:if="${comment.text}">
        <div th:if="${#strings.contains(comment.text, '```') or #strings.contains(comment.text, '<code>')}"
             class="code-comment">
            💻 코드가 포함된 댓글
        </div>
    </div>

    <!-- 마크다운 링크 감지 -->
    <div class="markdown-detection" th:if="${document.content}">
        <span th:if="${#strings.matches(document.content, '.*\\[.*\\]\\(.*\\).*')}" class="has-links">
            🔗 링크 포함
        </span>

        <span th:if="${#strings.matches(document.content, '.*!\\[.*\\]\\(.*\\).*')}" class="has-images">
            🖼️ 이미지 포함
        </span>
    </div>

    <!-- 금지어 필터링 -->
    <div class="content-filter" th:with="bannedWords=${'욕설1|욕설2|금지어'}">
        <div th:if="${#strings.matches(userInput, '.*(' + bannedWords + ').*')}" class="content-warning">
            ⚠️ 부적절한 내용이 감지되었습니다.
        </div>
    </div>
</div>

<script>
// 해시태그 추출을 위한 JavaScript (Thymeleaf 보완)
document.addEventListener('DOMContentLoaded', function() {
    const contentElement = document.querySelector('[data-content]');
    if (contentElement) {
        const content = contentElement.getAttribute('data-content');
        const hashtags = content.match(/#[a-zA-Z0-9가-힣_]+/g);
        const hashtagContainer = document.getElementById('extracted-hashtags');

        if (hashtags && hashtagContainer) {
            hashtags.forEach(tag => {
                const span = document.createElement('span');
                span.className = 'hashtag';
                span.textContent = tag;
                hashtagContainer.appendChild(span);
            });
        }
    }
});
</script>

실용적인 정규식 활용 예제

<div class="practical-regex-examples">
    <!-- 파일 확장자 검사 -->
    <div class="file-upload-validation" th:each="file : ${uploadedFiles}">
        <div th:switch="${true}">
            <span th:case="${#strings.matches(file.name, '.*\\.(jpg|jpeg|png|gif))}" class="image-file">
                🖼️ <span th:text="${file.name}">이미지</span>
            </span>
            <span th:case="${#strings.matches(file.name, '.*\\.(pdf|doc|docx))}" class="document-file">
                📄 <span th:text="${file.name}">문서</span>
            </span>
            <span th:case="${#strings.matches(file.name, '.*\\.(mp4|avi|mov))}" class="video-file">
                🎬 <span th:text="${file.name}">비디오</span>
            </span>
            <span th:case="*" class="other-file">
                📎 <span th:text="${file.name}">기타</span>
            </span>
        </div>
    </div>

    <!-- 버전 번호 비교 -->
    <div class="version-check" th:with="currentVersion=${'1.2.3'}, requiredVersion=${'1.2.0'}">
        <p th:if="${#strings.matches(currentVersion, '^[0-9]+\\.[0-9]+\\.[0-9]+)}" class="valid-version">
            현재 버전: <span th:text="${currentVersion}">1.2.3</span>
        </p>
    </div>

    <!-- HTML 태그 제거 검증 -->
    <div class="html-sanitization" th:if="${userInput}">
        <div th:if="${#strings.matches(userInput, '.*<[^>]+>.*')}" class="html-detected">
            ⚠️ HTML 태그가 감지되었습니다. 텍스트만 입력해주세요.
        </div>
    </div>

    <!-- 색상 코드 검증 -->
    <div class="color-validation" th:if="${themeForm.primaryColor}">
        <div th:if="${#strings.matches(themeForm.primaryColor, '^#[0-9A-Fa-f]{6})}" 
             th:style="|background-color: ${themeForm.primaryColor}; color: white; padding: 5px;|">
            유효한 색상 코드: <span th:text="${themeForm.primaryColor}">#FF5733</span>
        </div>
    </div>

    <!-- 시간 형식 검증 -->
    <div class="time-validation" th:if="${scheduleForm.startTime}">
        <span th:if="${#strings.matches(scheduleForm.startTime, '^([01]?[0-9]|2[0-3]):[0-5][0-9])}" class="valid">
            ⏰ 시작 시간: <span th:text="${scheduleForm.startTime}">14:30</span>
        </span>
    </div>
</div>

이 4장에서는 Thymeleaf의 표현식을 더욱 깊이 있게 다루었습니다. 조건 연산자, Elvis 연산자, 문자열 처리, 산술 연산, 논리 연산, 그리고 정규 표현식까지 실무에서 자주 사용되는 고급 기능들을 살펴보았습니다.

핵심 요약

  1. 조건 연산자: 삼항 연산자를 통한 간단한 조건 분기
  2. Elvis 연산자: null 안전한 기본값 설정
  3. No-Operation 토큰: 조건부 속성 설정 시 유용
  4. 문자열 연산: 동적 문자열 생성과 검증
  5. 산술/논리 연산: 복잡한 비즈니스 로직 구현
  6. 정규 표현식: 입력 검증과 텍스트 처리

이러한 고급 표현식들을 잘 활용하면 더욱 동적이고 유연한 템플릿을 만들 수 있습니다. 다음 장에서는 Thymeleaf의 표준 속성들에 대해 상세히 알아보겠습니다.

반응형