로그인 메뉴 구현¶
기본 로그인 메뉴¶
간단한 로그인/로그아웃 링크¶
<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">×</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>