로그인 시스템

로그인 시스템

라이믹스 레이아웃에서 다양한 로그인 기능을 구현하는 방법을 학습합니다.

로그인 방법 구분

아이디/이메일 로그인 구분

<!-- 로그인 방법에 따른 폼 구성 -->
{@
    // 로그인 방법 확인
    $member_config = getModel('member')->getMemberConfig();
    $identifier = $member_config->identifier;
    // identifier: 'user_id', 'email_address', 'user_id,email_address'
}

<form action="{getUrl()}" method="post" class="login-form">
    <input type="hidden" name="module" value="member" />
    <input type="hidden" name="act" value="procMemberLogin" />
    <input type="hidden" name="xe_validator_id" value="modules/member/skin/default/login/1" />

    <div class="login-body">
        <!-- 아이디로 로그인 -->
        <div cond="$identifier == 'user_id'" class="input-group">
            <label for="uid">아이디</label>
            <input type="text" name="user_id" id="uid" required placeholder="아이디를 입력하세요" />
        </div>

        <!-- 이메일로 로그인 -->
        <div cond="$identifier == 'email_address'" class="input-group">
            <label for="email">이메일</label>
            <input type="email" name="user_id" id="email" required placeholder="이메일을 입력하세요" />
        </div>

        <!-- 아이디 또는 이메일로 로그인 -->
        <div cond="$identifier == 'user_id,email_address'" class="input-group">
            <label for="login_id">아이디 또는 이메일</label>
            <input type="text" name="user_id" id="login_id" required 
                   placeholder="아이디 또는 이메일을 입력하세요" />
        </div>

        <!-- 비밀번호 -->
        <div class="input-group">
            <label for="password">비밀번호</label>
            <input type="password" name="password" id="password" required 
                   placeholder="비밀번호를 입력하세요" />
        </div>

        <!-- 로그인 유지 -->
        <div class="checkbox-group">
            <label>
                <input type="checkbox" name="keep_signed" value="Y" />
                로그인 상태 유지
            </label>
        </div>

        <!-- 로그인 버튼 -->
        <button type="submit" class="btn-login">로그인</button>

        <!-- 추가 링크 -->
        <div class="login-links">
            <a href="{getUrl('act', 'dispMemberFindAccount')}">아이디/비밀번호 찾기</a>
            <a href="{getUrl('act', 'dispMemberSignUpForm')}">회원가입</a>
        </div>
    </div>
</form>

소셜 로그인

소셜 로그인 위젯 삽입

<!-- 소셜 로그인 버튼 -->
<div class="social-login-widget">
    <h4>소셜 계정으로 로그인</h4>

    {@
        // 소셜 로그인 설정 확인
        $oMemberModel = getModel('member');
        $config = $oMemberModel->getMemberConfig();
        $sns_config = $config->sns_config;
    }

    <div class="social-buttons">
        <!-- 네이버 로그인 -->
        <a cond="$sns_config->naver->enabled == 'Y'" 
           href="{getUrl('', 'module', 'member', 'act', 'dispMemberSnsLogin', 'sns', 'naver')}" 
           class="btn-social naver">
            <i class="xi-naver"></i>
            <span>네이버</span>
        </a>

        <!-- 카카오 로그인 -->
        <a cond="$sns_config->kakao->enabled == 'Y'" 
           href="{getUrl('', 'module', 'member', 'act', 'dispMemberSnsLogin', 'sns', 'kakao')}" 
           class="btn-social kakao">
            <i class="xi-kakaotalk"></i>
            <span>카카오</span>
        </a>

        <!-- 구글 로그인 -->
        <a cond="$sns_config->google->enabled == 'Y'" 
           href="{getUrl('', 'module', 'member', 'act', 'dispMemberSnsLogin', 'sns', 'google')}" 
           class="btn-social google">
            <i class="xi-google"></i>
            <span>구글</span>
        </a>

        <!-- 페이스북 로그인 -->
        <a cond="$sns_config->facebook->enabled == 'Y'" 
           href="{getUrl('', 'module', 'member', 'act', 'dispMemberSnsLogin', 'sns', 'facebook')}" 
           class="btn-social facebook">
            <i class="xi-facebook"></i>
            <span>페이스북</span>
        </a>

        <!-- 애플 로그인 -->
        <a cond="$sns_config->apple->enabled == 'Y'" 
           href="{getUrl('', 'module', 'member', 'act', 'dispMemberSnsLogin', 'sns', 'apple')}" 
           class="btn-social apple">
            <i class="xi-apple"></i>
            <span>Apple</span>
        </a>
    </div>
</div>

<style>
.social-buttons {
    display: flex;
    gap: 10px;
    margin-top: 15px;
}

.btn-social {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 10px;
    border-radius: 5px;
    text-decoration: none;
    color: white;
    transition: opacity 0.3s;
}

.btn-social:hover {
    opacity: 0.8;
}

.btn-social i {
    font-size: 20px;
    margin-right: 5px;
}

.btn-social.naver { background-color: #03C75A; }
.btn-social.kakao { background-color: #FEE500; color: #000; }
.btn-social.google { background-color: #4285F4; }
.btn-social.facebook { background-color: #1877F2; }
.btn-social.apple { background-color: #000000; }
</style>

로그인 폼 커스터마이징

모달 로그인

<!-- 모달 로그인 폼 -->
<div id="loginModal" class="modal" style="display:none;">
    <div class="modal-content">
        <div class="modal-header">
            <h3>로그인</h3>
            <button type="button" class="close" onclick="closeLoginModal()">
                <i class="xi-close"></i>
            </button>
        </div>

        <div class="modal-body">
            <!-- 탭 네비게이션 -->
            <ul class="login-tabs">
                <li class="active" data-tab="normal">일반 로그인</li>
                <li data-tab="social">소셜 로그인</li>
            </ul>

            <!-- 일반 로그인 -->
            <div class="tab-content active" id="normal-login">
                <form action="{getUrl()}" method="post" onsubmit="return doLogin(this)">
                    <input type="hidden" name="module" value="member" />
                    <input type="hidden" name="act" value="procMemberLogin" />

                    <div class="form-group">
                        <input type="text" name="user_id" placeholder="아이디 또는 이메일" required />
                    </div>

                    <div class="form-group">
                        <input type="password" name="password" placeholder="비밀번호" required />
                    </div>

                    <div class="form-options">
                        <label>
                            <input type="checkbox" name="keep_signed" value="Y" />
                            자동 로그인
                        </label>
                        <a href="{getUrl('act', 'dispMemberFindAccount')}">
                            비밀번호를 잊으셨나요?
                        </a>
                    </div>

                    <button type="submit" class="btn-submit">로그인</button>
                </form>
            </div>

            <!-- 소셜 로그인 -->
            <div class="tab-content" id="social-login">
                <div class="social-login-buttons">
                    <!-- 소셜 로그인 버튼들 -->
                </div>
            </div>
        </div>

        <div class="modal-footer">
            아직 회원이 아니신가요? 
            <a href="{getUrl('act', 'dispMemberSignUpForm')}">회원가입</a>
        </div>
    </div>
</div>

<script>
// 모달 열기/닫기
function openLoginModal() {
    $('#loginModal').fadeIn();
    $('body').addClass('modal-open');
}

function closeLoginModal() {
    $('#loginModal').fadeOut();
    $('body').removeClass('modal-open');
}

// 탭 전환
$('.login-tabs li').click(function() {
    var tab = $(this).data('tab');

    $('.login-tabs li').removeClass('active');
    $(this).addClass('active');

    $('.tab-content').removeClass('active');
    $('#' + tab + '-login').addClass('active');
});

// AJAX 로그인
function doLogin(form) {
    var params = $(form).serialize();

    $.ajax({
        url: request_uri,
        type: 'POST',
        data: params,
        dataType: 'json',
        success: function(response) {
            if(response.error != 0) {
                alert(response.message);
            } else {
                // 로그인 성공
                if(response.redirect_url) {
                    location.href = response.redirect_url;
                } else {
                    location.reload();
                }
            }
        }
    });

    return false;
}
</script>

로그인 상태 표시

로그인/로그아웃 메뉴

<!-- 로그인 상태에 따른 메뉴 -->
<div class="user-menu">
    <!-- 로그인 전 -->
    <div cond="!$logged_info" class="guest-menu">
        <a href="#" onclick="openLoginModal(); return false;" class="btn-login">
            <i class="xi-log-in"></i> 로그인
        </a>
        <a href="{getUrl('act', 'dispMemberSignUpForm')}" class="btn-signup">
            <i class="xi-user-plus"></i> 회원가입
        </a>
    </div>

    <!-- 로그인 후 -->
    <div cond="$logged_info" class="member-menu">
        <!-- 프로필 이미지 -->
        <div class="profile-image">
            {@$profile_image = $logged_info->getProfileImage()}
            <img src="{$profile_image ? $profile_image->src : '/modules/member/skins/default/images/default_profile.png'}" 
                 alt="프로필" />
        </div>

        <!-- 드롭다운 메뉴 -->
        <div class="dropdown">
            <button class="dropdown-toggle">
                {$logged_info->nick_name}
                <i class="xi-angle-down"></i>
            </button>

            <ul class="dropdown-menu">
                <li class="user-info">
                    <strong>{$logged_info->nick_name}</strong>
                    <span>{$logged_info->email_address}</span>
                </li>

                <li class="divider"></li>

                <li>
                    <a href="{getUrl('act', 'dispMemberInfo')}">
                        <i class="xi-user"></i> 내 정보
                    </a>
                </li>

                <li cond="$logged_info->is_admin == 'Y'">
                    <a href="{getUrl('', 'module', 'admin')}">
                        <i class="xi-cog"></i> 관리자
                    </a>
                </li>

                <li>
                    <a href="{getUrl('act', 'dispMemberScrappedDocument')}">
                        <i class="xi-bookmark"></i> 스크랩
                    </a>
                </li>

                <li>
                    <a href="{getUrl('act', 'dispCommunicationMessages')}">
                        <i class="xi-message"></i> 쪽지
                        <span class="badge" cond="$logged_info->new_message">
                            {$logged_info->new_message}
                        </span>
                    </a>
                </li>

                <li class="divider"></li>

                <li>
                    <a href="{getUrl('act', 'dispMemberLogout')}">
                        <i class="xi-log-out"></i> 로그아웃
                    </a>
                </li>
            </ul>
        </div>
    </div>
</div>

<script>
// 드롭다운 메뉴 토글
$('.dropdown-toggle').click(function(e) {
    e.stopPropagation();
    $(this).next('.dropdown-menu').toggle();
});

// 외부 클릭시 닫기
$(document).click(function() {
    $('.dropdown-menu').hide();
});
</script>

별도 로그인 페이지

전체 화면 로그인 페이지

<!-- 별도 로그인 페이지 레이아웃 -->
<div class="login-page" cond="$act == 'dispMemberLoginForm'">
    <div class="login-container">
        <div class="login-box">
            <!-- 로고 -->
            <div class="logo">
                <a href="{getUrl('')}">
                    <img src="/layouts/my_layout/img/logo.png" alt="{$layout_info->site_title}" />
                </a>
            </div>

            <!-- 로그인 폼 -->
            <form action="{getUrl()}" method="post" class="login-form">
                <input type="hidden" name="module" value="member" />
                <input type="hidden" name="act" value="procMemberLogin" />
                <input type="hidden" name="success_return_url" value="{$success_return_url}" />

                <h2>로그인</h2>

                <!-- 에러 메시지 -->
                <div cond="$XE_VALIDATOR_MESSAGE" class="alert alert-danger">
                    {$XE_VALIDATOR_MESSAGE}
                </div>

                <div class="form-group">
                    <div class="input-group">
                        <span class="input-icon"><i class="xi-user"></i></span>
                        <input type="text" name="user_id" placeholder="아이디 또는 이메일" 
                               value="{$user_id}" required autofocus />
                    </div>
                </div>

                <div class="form-group">
                    <div class="input-group">
                        <span class="input-icon"><i class="xi-lock"></i></span>
                        <input type="password" name="password" placeholder="비밀번호" required />
                    </div>
                </div>

                <div class="form-group">
                    <label class="checkbox">
                        <input type="checkbox" name="keep_signed" value="Y" />
                        로그인 상태 유지
                    </label>
                </div>

                <button type="submit" class="btn-login btn-block">로그인</button>

                <!-- 소셜 로그인 -->
                <div class="social-login">
                    <div class="divider">
                        <span>또는</span>
                    </div>

                    <div class="social-buttons">
                        <!-- 소셜 로그인 버튼들 -->
                    </div>
                </div>

                <!-- 추가 링크 -->
                <div class="login-footer">
                    <a href="{getUrl('act', 'dispMemberFindAccount')}">
                        아이디/비밀번호 찾기
                    </a>
                    <span class="separator">|</span>
                    <a href="{getUrl('act', 'dispMemberSignUpForm')}">
                        회원가입
                    </a>
                </div>
            </form>
        </div>

        <!-- 배경 이미지나 패턴 -->
        <div class="login-background"></div>
    </div>
</div>

<style>
.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.login-box {
    background: white;
    padding: 40px;
    border-radius: 10px;
    box-shadow: 0 10px 25px rgba(0,0,0,0.1);
    width: 400px;
    max-width: 90%;
}

.input-group {
    position: relative;
}

.input-icon {
    position: absolute;
    left: 15px;
    top: 50%;
    transform: translateY(-50%);
    color: #999;
}

.input-group input {
    padding-left: 45px;
}

.divider {
    text-align: center;
    margin: 20px 0;
    position: relative;
}

.divider:before {
    content: '';
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    height: 1px;
    background: #ddd;
}

.divider span {
    background: white;
    padding: 0 15px;
    position: relative;
}
</style>

보안 강화

로그인 시도 제한

<!-- 로그인 시도 제한 메시지 -->
{@
    // 로그인 시도 횟수 확인
    $login_attempts = $_SESSION['login_attempts'] ?: 0;
    $max_attempts = 5;
    $lockout_time = 300; // 5분

    if(isset($_SESSION['lockout_until']) && $_SESSION['lockout_until'] > time()) {
        $remaining_time = $_SESSION['lockout_until'] - time();
        $is_locked = true;
    } else {
        $is_locked = false;
    }
}

<div cond="$is_locked" class="alert alert-warning">
    너무 많은 로그인 시도가 있었습니다. 
    <span id="lockout-timer">{floor($remaining_time / 60)}분 {$remaining_time % 60}초</span> 
    후에 다시 시도해주세요.
</div>

<script cond="$is_locked">
// 잠금 시간 카운트다운
var remaining = {$remaining_time};
var timer = setInterval(function() {
    remaining--;

    if(remaining <= 0) {
        clearInterval(timer);
        location.reload();
    } else {
        var minutes = Math.floor(remaining / 60);
        var seconds = remaining % 60;
        $('#lockout-timer').text(minutes + '분 ' + seconds + '초');
    }
}, 1000);
</script>

<!-- reCAPTCHA 연동 -->
<div cond="$login_attempts >= 3" class="captcha-container">
    <div class="g-recaptcha" data-sitekey="{$layout_info->recaptcha_site_key}"></div>
</div>

<script src="https://www.google.com/recaptcha/api.js" async defer></script>

모범 사례

  1. 보안: HTTPS 사용, CSRF 토큰 검증
  2. 사용성: 명확한 에러 메시지, 자동 완성
  3. 접근성: 키보드 네비게이션, 레이블 제공
  4. 반응형: 모바일 최적화된 로그인 UI
  5. 성능: 불필요한 리다이렉트 최소화

다음 단계

로그인 시스템을 구현했다면, 위젯 시스템을 학습하세요.