로그인 시스템¶
라이믹스 레이아웃에서 다양한 로그인 기능을 구현하는 방법을 학습합니다.
로그인 방법 구분¶
아이디/이메일 로그인 구분¶
<!-- 로그인 방법에 따른 폼 구성 -->
{@
// 로그인 방법 확인
$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>
모범 사례¶
- 보안: HTTPS 사용, CSRF 토큰 검증
- 사용성: 명확한 에러 메시지, 자동 완성
- 접근성: 키보드 네비게이션, 레이블 제공
- 반응형: 모바일 최적화된 로그인 UI
- 성능: 불필요한 리다이렉트 최소화
다음 단계¶
로그인 시스템을 구현했다면, 위젯 시스템을 학습하세요.