로그인 메뉴 구현

로그인 메뉴 구현

기본 로그인 메뉴

간단한 로그인/로그아웃 링크

<div class="user-menu">
    <!--@if(!$is_logged)-->
        <!-- 비로그인 상태 -->
        <a href="{getUrl('act', 'dispMemberLoginForm')}" class="login-link">로그인</a>
        <a href="{getUrl('act', 'dispMemberSignUpForm')}" class="signup-link">회원가입</a>
    <!--@else-->
        <!-- 로그인 상태 -->
        <span class="welcome">
            <img src="{$logged_info->profile_image->src}" alt="" class="profile-img" cond="$logged_info->profile_image" />
            {$logged_info->nick_name}님
        </span>
        <a href="{getUrl('act', 'dispMemberInfo')}">마이페이지</a>
        <a href="{getUrl('act', 'dispMemberLogout')}">로그아웃</a>

        <!-- 관리자 링크 -->
        <a href="{getUrl('', 'module', 'admin')}" cond="$logged_info->is_admin == 'Y'" target="_blank">관리</a>
    <!--@endif-->
</div>

드롭다운 사용자 메뉴

HTML 구조

<div class="user-dropdown">
    <!--@if(!$is_logged)-->
        <!-- 비로그인 상태 -->
        <button class="user-toggle guest">
            <i class="icon-user"></i>
            <span>로그인</span>
        </button>

        <div class="dropdown-panel">
            <a href="{getUrl('act', 'dispMemberLoginForm')}">
                <i class="icon-login"></i> 로그인
            </a>
            <a href="{getUrl('act', 'dispMemberSignUpForm')}">
                <i class="icon-user-plus"></i> 회원가입
            </a>
            <a href="{getUrl('act', 'dispMemberFindAccount')}">
                <i class="icon-help"></i> ID/PW 찾기
            </a>
        </div>
    <!--@else-->
        <!-- 로그인 상태 -->
        <button class="user-toggle logged">
            <!--@if($logged_info->profile_image)-->
                <img src="{$logged_info->profile_image->src}" alt="" class="avatar" />
            <!--@else-->
                <div class="avatar-default">{substr($logged_info->nick_name, 0, 1)}</div>
            <!--@endif-->
            <span class="user-name">{$logged_info->nick_name}</span>
            <i class="icon-chevron-down"></i>
        </button>

        <div class="dropdown-panel">
            <!-- 사용자 정보 -->
            <div class="user-info">
                <div class="avatar-large">
                    <!--@if($logged_info->profile_image)-->
                        <img src="{$logged_info->profile_image->src}" alt="" />
                    <!--@else-->
                        <div class="avatar-default">{substr($logged_info->nick_name, 0, 1)}</div>
                    <!--@endif-->
                </div>
                <div class="info">
                    <strong>{$logged_info->nick_name}</strong>
                    <span class="email">{$logged_info->email_address}</span>
                    <div class="stats">
                        <span>포인트: {number_format($logged_info->point)}</span>
                        <span>레벨: {$logged_info->level}</span>
                    </div>
                </div>
            </div>

            <!-- 메뉴 링크 -->
            <ul class="user-links">
                <li>
                    <a href="{getUrl('act', 'dispMemberInfo')}">
                        <i class="icon-user"></i> 회원정보
                    </a>
                </li>
                <li>
                    <a href="{getUrl('act', 'dispMemberScrappedDocument')}">
                        <i class="icon-bookmark"></i> 스크랩
                    </a>
                </li>
                <li>
                    <a href="{getUrl('act', 'dispCommunicationMessages')}">
                        <i class="icon-message"></i> 쪽지함
                        <span class="badge" cond="$logged_info->new_message">{$logged_info->new_message}</span>
                    </a>
                </li>
                <li>
                    <a href="{getUrl('act', 'dispMemberOwnDocument')}">
                        <i class="icon-document"></i> 내 글 보기
                    </a>
                </li>
                <li>
                    <a href="{getUrl('act', 'dispMemberOwnComment')}">
                        <i class="icon-comment"></i> 내 댓글 보기
                    </a>
                </li>
            </ul>

            <!-- 관리자 메뉴 -->
            <ul class="admin-links" cond="$logged_info->is_admin == 'Y'">
                <li>
                    <a href="{getUrl('', 'module', 'admin')}" target="_blank">
                        <i class="icon-settings"></i> 사이트 관리
                    </a>
                </li>
                <li cond="$mid">
                    <a href="{getUrl('', 'module', 'admin', 'act', 'dispModuleAdminContent', 'module_srl', $module_info->module_srl)}" target="_blank">
                        <i class="icon-edit"></i> 모듈 관리
                    </a>
                </li>
            </ul>

            <!-- 로그아웃 -->
            <div class="logout-wrapper">
                <a href="{getUrl('act', 'dispMemberLogout')}" class="logout-btn">
                    <i class="icon-logout"></i> 로그아웃
                </a>
            </div>
        </div>
    <!--@endif-->
</div>

CSS 스타일

/* 사용자 드롭다운 */
.user-dropdown {
    position: relative;
}

.user-toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 15px;
    background: none;
    border: 1px solid #ddd;
    border-radius: 25px;
    cursor: pointer;
    transition: all 0.3s;
}

.user-toggle:hover {
    border-color: #007bff;
}

.user-toggle .avatar,
.user-toggle .avatar-default {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    object-fit: cover;
}

.avatar-default {
    display: flex;
    align-items: center;
    justify-content: center;
    background: #007bff;
    color: white;
    font-weight: bold;
}

/* 드롭다운 패널 */
.dropdown-panel {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 10px;
    min-width: 280px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 5px 25px rgba(0,0,0,0.15);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-10px);
    transition: all 0.3s;
    z-index: 1000;
}

.user-dropdown:hover .dropdown-panel,
.user-dropdown.active .dropdown-panel {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/* 사용자 정보 섹션 */
.user-info {
    display: flex;
    gap: 15px;
    padding: 20px;
    border-bottom: 1px solid #eee;
}

.user-info .avatar-large {
    width: 60px;
    height: 60px;
    flex-shrink: 0;
}

.user-info .avatar-large img,
.user-info .avatar-large .avatar-default {
    width: 100%;
    height: 100%;
    border-radius: 50%;
}

.user-info .info {
    flex: 1;
}

.user-info strong {
    display: block;
    margin-bottom: 5px;
}

.user-info .email {
    display: block;
    color: #666;
    font-size: 14px;
    margin-bottom: 10px;
}

.user-info .stats {
    display: flex;
    gap: 15px;
    font-size: 13px;
    color: #888;
}

/* 링크 목록 */
.user-links,
.admin-links {
    list-style: none;
    padding: 10px 0;
    margin: 0;
}

.admin-links {
    border-top: 1px solid #eee;
}

.user-links a,
.admin-links a {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 20px;
    color: #333;
    text-decoration: none;
    transition: all 0.2s;
}

.user-links a:hover,
.admin-links a:hover {
    background: #f8f9fa;
    color: #007bff;
}

/* 배지 */
.badge {
    display: inline-block;
    min-width: 20px;
    padding: 2px 6px;
    background: #dc3545;
    color: white;
    font-size: 11px;
    font-weight: bold;
    text-align: center;
    border-radius: 10px;
}

/* 로그아웃 버튼 */
.logout-wrapper {
    padding: 10px;
    border-top: 1px solid #eee;
}

.logout-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    padding: 10px;
    background: #f8f9fa;
    color: #333;
    text-decoration: none;
    border-radius: 6px;
    transition: all 0.3s;
}

.logout-btn:hover {
    background: #e9ecef;
}

인라인 로그인 폼

헤더에 포함된 로그인 폼

<div class="inline-login" cond="!$is_logged">
    <form action="{getUrl('act', 'procMemberLogin')}" method="post" class="login-form">
        <input type="hidden" name="success_return_url" value="{$current_url}" />
        <input type="hidden" name="error_return_url" value="{$current_url}" />

        <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 class="remember">
                <input type="checkbox" name="keep_signed" value="Y" />
                <span>로그인 유지</span>
            </label>

            <a href="{getUrl('act', 'dispMemberFindAccount')}" class="find-account">
                ID/PW 찾기
            </a>
        </div>

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

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

모달 로그인 폼

HTML 구조

<!-- 로그인 버튼 -->
<button class="open-login-modal" cond="!$is_logged">로그인</button>

<!-- 로그인 모달 -->
<div class="login-modal" id="loginModal">
    <div class="modal-backdrop"></div>
    <div class="modal-content">
        <button class="modal-close">&times;</button>

        <h2 class="modal-title">로그인</h2>

        <form action="{getUrl('act', 'procMemberLogin')}" method="post" class="modal-login-form">
            <input type="hidden" name="success_return_url" value="{$current_url}" />

            <!-- 소셜 로그인 -->
            <div class="social-login" cond="$social_login">
                <button type="button" class="social-btn naver" onclick="loginWithSocial('naver')">
                    <img src="images/naver-icon.png" alt="" />
                    네이버로 로그인
                </button>
                <button type="button" class="social-btn kakao" onclick="loginWithSocial('kakao')">
                    <img src="images/kakao-icon.png" alt="" />
                    카카오로 로그인
                </button>
                <button type="button" class="social-btn google" onclick="loginWithSocial('google')">
                    <img src="images/google-icon.png" alt="" />
                    구글로 로그인
                </button>
            </div>

            <div class="divider">
                <span>또는</span>
            </div>

            <!-- 일반 로그인 -->
            <div class="form-group">
                <label for="login_id">아이디 또는 이메일</label>
                <input type="text" name="user_id" id="login_id" required />
            </div>

            <div class="form-group">
                <label for="login_pw">비밀번호</label>
                <input type="password" name="password" id="login_pw" required />
            </div>

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

                <a href="{getUrl('act', 'dispMemberFindAccount')}">비밀번호를 잊으셨나요?</a>
            </div>

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

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

모달 스크립트

jQuery(function($) {
    // 모달 열기
    $('.open-login-modal').click(function(e) {
        e.preventDefault();
        $('#loginModal').addClass('active');
        $('body').addClass('modal-open');
    });

    // 모달 닫기
    $('.modal-close, .modal-backdrop').click(function() {
        $('#loginModal').removeClass('active');
        $('body').removeClass('modal-open');
    });

    // ESC 키로 닫기
    $(document).keyup(function(e) {
        if (e.keyCode === 27) {
            $('#loginModal').removeClass('active');
            $('body').removeClass('modal-open');
        }
    });

    // 폼 제출
    $('.modal-login-form').on('submit', function(e) {
        var $form = $(this);
        var userId = $form.find('[name="user_id"]').val();
        var password = $form.find('[name="password"]').val();

        if (!userId || !password) {
            e.preventDefault();
            alert('아이디와 비밀번호를 입력해주세요.');
            return false;
        }
    });
});

// 소셜 로그인
function loginWithSocial(provider) {
    var width = 500;
    var height = 600;
    var left = (screen.width - width) / 2;
    var top = (screen.height - height) / 2;

    window.open(
        current_url + '?module=sociallogin&act=procSocialloginAuth&provider=' + provider,
        'social_login',
        'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top
    );
}

스마트 헤더 로그인

팝업이나 페이지 이동 없이 헤더에서 바로 로그인:

<div class="smart-login" cond="!$is_logged">
    <button class="smart-login-toggle">
        <i class="icon-user"></i>
        <span>로그인</span>
    </button>

    <div class="smart-login-panel">
        <form action="{getUrl('act', 'procMemberLogin')}" method="post">
            <input type="hidden" name="success_return_url" value="{$current_url}" />

            <input type="text" name="user_id" placeholder="아이디" required />
            <input type="password" name="password" placeholder="비밀번호" required />

            <div class="button-group">
                <button type="submit">로그인</button>
                <a href="{getUrl('act', 'dispMemberSignUpForm')}" class="signup">가입</a>
            </div>

            <div class="links">
                <label>
                    <input type="checkbox" name="keep_signed" value="Y" />
                    <span>자동</span>
                </label>
                <a href="{getUrl('act', 'dispMemberFindAccount')}">ID/PW 찾기</a>
            </div>
        </form>
    </div>
</div>

보안 고려사항

CSRF 토큰

<form action="{getUrl('act', 'procMemberLogin')}" method="post">
    <input type="hidden" name="csrf_token" value="{$csrf_token}" />
    <!-- 폼 필드들 -->
</form>

로그인 실패 처리

<!--@if($XE_VALIDATOR_MESSAGE && $XE_VALIDATOR_ID == 'modules/member/proc/member_login')-->
<div class="alert alert-danger">
    {$XE_VALIDATOR_MESSAGE}
</div>
<!--@endif-->

계정 보안 알림

<!-- 로그인 후 보안 정보 표시 -->
<div class="security-info" cond="$is_logged && $logged_info->last_login">
    <p>마지막 로그인: {zdate($logged_info->last_login, 'Y-m-d H:i')}</p>
    <p>로그인 IP: {$logged_info->last_login_ip}</p>
</div>