Auth / Login
Import
<link rel="stylesheet" href="/guide/css/base/index.css" /> <link rel="stylesheet" href="/guide/css/components/form.css" /> <link rel="stylesheet" href="/guide/css/components/button.css" /> <link rel="stylesheet" href="/guide/css/components/alert.css" /> <link rel="stylesheet" href="/guide/css/components/auth-card.css" />
Live
유효성 검사, 비밀번호 일치 확인, 약관 동의 검사가 동작합니다. 이메일 형식 오류 / 비밀번호 불일치 등 실제 에러 메시지를 확인해 보세요.
로그인
Logo
로그인
서비스를 이용하려면 로그인 하세요
✕
로그인 실패
아이디 또는 비밀번호가 올바르지 않습니다
회원가입
Logo
회원가입
회원가입을 하세요
비밀번호 찾기
Logo
비밀번호 찾기
가입한 이메일로 재설정 링크를 보내드립니다
이메일을 전송했습니다
입력하신 이메일로 비밀번호 재설정 링크를 발송했습니다. 메일함을 확인해 주세요.
States
로그인 — 유효성 검사 오류
로그인
아이디를 입력해주세요
비밀번호를 입력해주세요
로그인 — 로그인 실패 (서버 오류)
로그인
✕
로그인 실패
아이디 또는 비밀번호가 올바르지 않습니다
회원가입 — 비밀번호 불일치
회원가입
사용 가능한 비밀번호입니다
비밀번호가 일치하지 않습니다
회원가입 — 약관 미동의
회원가입
비밀번호 찾기 — 이메일 전송 성공
비밀번호 찾기
가입한 이메일로 재설정 링크를 보내드립니다
이메일을 전송했습니다
example@email.com으로 비밀번호 재설정 링크를 발송했습니다. 메일함을 확인해 주세요.
Code
HTML — 로그인
<div class="auth-card"> <div class="auth-card__logo"> <img class="auth-card__logo-img" src="/img/logo.png" alt="Brand" /> </div> <div class="auth-card__header"> <h2 class="auth-card__title">로그인</h2> <p class="auth-card__subtitle">서비스를 이용하려면 로그인 하세요</p> </div> <!-- 서버 오류 Alert --> <div class="alert alert--error" role="alert" hidden> <div class="alert__bar"></div> <span class="alert__icon">✕</span> <div class="alert__content"> <p class="alert__title">로그인 실패</p> <p class="alert__text">아이디 또는 비밀번호가 올바르지 않습니다</p> </div> </div> <form class="auth-card__body" id="login-form" novalidate> <div class="form-group"> <label class="label" for="login-id">아이디 <span class="label__required">*</span></label> <input class="input" type="text" id="login-id" name="username" placeholder="아이디 입력" autocomplete="username" /> <span class="helper-text helper-text--error" hidden>아이디를 입력해주세요</span> </div> <div class="form-group"> <label class="label" for="login-pw">비밀번호 <span class="label__required">*</span></label> <input class="input" type="password" id="login-pw" name="password" placeholder="비밀번호 입력" autocomplete="current-password" /> <span class="helper-text helper-text--error" hidden>비밀번호를 입력해주세요</span> </div> <div class="checkbox"> <input class="checkbox__input" type="checkbox" id="login-remember" name="remember" /> <label class="checkbox__label" for="login-remember"> <span class="checkbox__control"><span class="checkbox__check"></span></span> 로그인 상태 유지 </label> </div> </form> <button class="button button--primary button--full" type="submit" id="login-submit">로그인</button> <p class="auth-card__footer">계정이 없으신가요? <a class="auth-card__link" href="/guide/signup">회원가입</a></p> </div>
HTML — 회원가입
<div class="auth-card"> <div class="auth-card__header"> <h2 class="auth-card__title">회원가입</h2> <p class="auth-card__subtitle">회원가입을 하세요</p> </div> <form class="auth-card__body" id="signup-form" novalidate> <!-- 이름 --> <div class="form-group"> <label class="label" for="name">이름 <span class="label__required">*</span></label> <input class="input" type="text" id="name" name="name" placeholder="홍길동" /> <span class="helper-text helper-text--error" hidden>이름을 입력해주세요</span> </div> <!-- 이메일 --> <div class="form-group"> <label class="label" for="email">이메일 <span class="label__required">*</span></label> <input class="input" type="email" id="email" name="email" placeholder="example@email.com" /> <span class="helper-text helper-text--error" hidden>올바른 이메일 형식을 입력해주세요</span> </div> <!-- 아이디 --> <div class="form-group"> <label class="label" for="uid">아이디 <span class="label__required">*</span></label> <input class="input" type="text" id="uid" name="username" placeholder="아이디 입력" /> <span class="helper-text helper-text--error" hidden>아이디를 입력해주세요</span> </div> <!-- 비밀번호 --> <div class="form-group"> <label class="label" for="pw">비밀번호 <span class="label__required">*</span></label> <input class="input" type="password" id="pw" name="password" placeholder="8자 이상, 영문+숫자 조합" /> <span class="helper-text helper-text--error" hidden></span> </div> <!-- 비밀번호 확인 --> <div class="form-group"> <label class="label" for="pw2">비밀번호 확인 <span class="label__required">*</span></label> <input class="input" type="password" id="pw2" name="password_confirm" placeholder="비밀번호 재입력" /> <span class="helper-text helper-text--error" hidden>비밀번호가 일치하지 않습니다</span> </div> <!-- 약관 동의 --> <div class="form-group"> <div class="checkbox"> <input class="checkbox__input" type="checkbox" id="terms" name="terms" /> <label class="checkbox__label" for="terms"> <span class="checkbox__control"><span class="checkbox__check"></span></span> <span><a href="/guide/terms">이용약관</a> 및 <a href="/guide/privacy">개인정보처리방침</a>에 동의합니다 <span class="label__required">*</span></span> </label> </div> <span class="helper-text helper-text--error" hidden>약관에 동의해주세요</span> </div> </form> <button class="button button--primary button--full" type="submit" id="signup-submit">회원가입</button> <p class="auth-card__footer">이미 계정이 있으신가요? <a class="auth-card__link" href="/guide/login">로그인</a></p> </div>
HTML — 비밀번호 찾기
<div class="auth-card"> <div class="auth-card__header"> <h2 class="auth-card__title">비밀번호 찾기</h2> <p class="auth-card__subtitle">가입한 이메일로 재설정 링크를 보내드립니다</p> </div> <!-- 이메일 전송 성공 상태 (JS로 표시/숨김) --> <div class="auth-card__sent" id="findpw-success" hidden> <div class="auth-card__sent-icon"> <svg viewBox="0 0 28 28" fill="none"> <path d="M4 14l7 7L24 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </div> <p class="auth-card__sent-title">이메일을 전송했습니다</p> <p class="auth-card__sent-desc">입력하신 이메일로 비밀번호 재설정 링크를 발송했습니다. 메일함을 확인해 주세요.</p> </div> <form class="auth-card__body" id="findpw-form" novalidate> <div class="form-group"> <label class="label" for="findpw-email">이메일 <span class="label__required">*</span></label> <input class="input" type="email" id="findpw-email" name="email" placeholder="example@email.com" /> <span class="helper-text helper-text--error" hidden>올바른 이메일 형식을 입력해주세요</span> </div> </form> <button class="button button--primary button--full" id="findpw-submit">재설정 링크 보내기</button> <a class="auth-card__back-link" href="/guide/login">← 로그인으로 돌아가기</a> </div>
JavaScript — 유효성 검사 + 폼 제출
/* ── 공통 유틸 ── */
function setFieldError(input, errEl, msg) {
input.classList.add('input--error');
input.setAttribute('aria-invalid', 'true');
if (errEl) { errEl.textContent = msg; errEl.hidden = false; }
}
function clearFieldError(input, errEl) {
input.classList.remove('input--error');
input.removeAttribute('aria-invalid');
if (errEl) errEl.hidden = true;
}
function isValidEmail(v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
}
function isValidPassword(v) {
return v.length >= 8 && /[a-zA-Z]/.test(v) && /[0-9]/.test(v);
}
/* ── 로그인 ── */
document.getElementById('login-submit').addEventListener('click', function () {
var id = document.getElementById('login-id');
var pw = document.getElementById('login-pw');
var errAlert = document.getElementById('login-error-alert');
var valid = true;
clearFieldError(id, document.getElementById('login-id-err'));
clearFieldError(pw, document.getElementById('login-pw-err'));
errAlert.hidden = true;
if (!id.value.trim()) {
setFieldError(id, document.getElementById('login-id-err'), '아이디를 입력해주세요');
valid = false;
}
if (!pw.value) {
setFieldError(pw, document.getElementById('login-pw-err'), '비밀번호를 입력해주세요');
valid = false;
}
if (!valid) return;
/* 서버 요청 (아래 백엔드 연동 코드 참조) */
loginRequest({ username: id.value.trim(), password: pw.value })
.then(function (res) {
window.location.href = '/dashboard'; /* 성공 → 리다이렉트 */
})
.catch(function () {
errAlert.hidden = false; /* 실패 → 에러 Alert 표시 */
});
});
/* ── 회원가입 ── */
document.getElementById('signup-submit').addEventListener('click', function () {
var name = document.getElementById('signup-name');
var email = document.getElementById('signup-email');
var uid = document.getElementById('signup-id');
var pw = document.getElementById('signup-pw');
var pw2 = document.getElementById('signup-pw2');
var terms = document.getElementById('signup-terms');
var valid = true;
[name, email, uid, pw, pw2].forEach(function (el) {
clearFieldError(el, document.getElementById(el.id + '-err') || null);
});
document.getElementById('signup-terms-err').hidden = true;
if (!name.value.trim())
{ setFieldError(name, document.getElementById('signup-name-err'), '이름을 입력해주세요'); valid = false; }
if (!email.value.trim() || !isValidEmail(email.value.trim()))
{ setFieldError(email, document.getElementById('signup-email-err'), '올바른 이메일 형식을 입력해주세요'); valid = false; }
if (!uid.value.trim())
{ setFieldError(uid, document.getElementById('signup-id-err'), '아이디를 입력해주세요'); valid = false; }
if (!isValidPassword(pw.value)) {
var pwErr = document.getElementById('signup-pw-err');
setFieldError(pw, pwErr, '8자 이상, 영문자와 숫자를 포함해야 합니다');
valid = false;
}
if (pw.value !== pw2.value)
{ setFieldError(pw2, document.getElementById('signup-pw2-err'), '비밀번호가 일치하지 않습니다'); valid = false; }
if (!terms.checked)
{ document.getElementById('signup-terms-err').hidden = false; valid = false; }
if (!valid) return;
/* 서버 요청 */
signupRequest({ name: name.value.trim(), email: email.value.trim(),
username: uid.value.trim(), password: pw.value })
.then(function () {
window.location.href = '/login?registered=1';
});
});
/* ── 비밀번호 찾기 ── */
document.getElementById('findpw-submit').addEventListener('click', function () {
var email = document.getElementById('findpw-email');
var errEl = document.getElementById('findpw-email-err');
clearFieldError(email, errEl);
if (!email.value.trim() || !isValidEmail(email.value.trim())) {
setFieldError(email, errEl, '올바른 이메일 형식을 입력해주세요');
return;
}
findPasswordRequest({ email: email.value.trim() })
.then(function () {
document.getElementById('findpw-form').hidden = true;
document.getElementById('findpw-submit').hidden = true;
document.getElementById('findpw-success').hidden = false;
});
});
/* ── 비밀번호 표시 토글 ── */
document.querySelectorAll('.auth-card__pw-toggle').forEach(function (btn) {
btn.addEventListener('click', function () {
var input = document.getElementById(btn.dataset.target);
if (!input) return;
var isText = input.type === 'text';
input.type = isText ? 'password' : 'text';
btn.setAttribute('aria-label', isText ? '비밀번호 표시' : '비밀번호 숨김');
});
});
백엔드 연동 — fetch API
/* ── 로그인 ── */
function loginRequest(data) {
return fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
credentials: 'include' /* 세션 쿠키 */
}).then(function (res) {
if (!res.ok) throw new Error('login_failed');
return res.json();
});
}
/* ── 회원가입 ── */
function signupRequest(data) {
return fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(function (res) {
if (!res.ok) return res.json().then(function (e) { throw e; });
return res.json();
});
}
/* ── 비밀번호 찾기 (이메일 발송) ── */
function findPasswordRequest(data) {
return fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(function (res) {
if (!res.ok) throw new Error('request_failed');
return res.json();
});
}
/* ────────────────────────────────────────────
서버 API 스펙 (백엔드 담당자 참고)
────────────────────────────────────────────
POST /api/auth/login
Request { username, password }
Response 200: { token, user: { id, name, email } }
Response 401: { code: "INVALID_CREDENTIALS" }
POST /api/auth/register
Request { name, email, username, password }
Response 201: { message: "ok" }
Response 409: { code: "USERNAME_TAKEN" | "EMAIL_TAKEN" }
Response 422: { code: "VALIDATION_ERROR", fields: [...] }
POST /api/auth/forgot-password
Request { email }
Response 200: { message: "email_sent" }
Response 404: { code: "USER_NOT_FOUND" }
JWT 사용 시:
- 로그인 성공 → res.token → localStorage.setItem('token', token)
- 이후 요청 header: Authorization: Bearer
세션 사용 시:
- credentials: 'include' 필수 (쿠키 포함)
- 서버에서 Set-Cookie: sessionId=xxx; HttpOnly; SameSite=Strict
──────────────────────────────────────────── */
백엔드 연동 — jQuery AJAX
/* jQuery 환경에서의 로그인 예시 */
$.ajax({
url: '/api/auth/login',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ username: id, password: pw }),
success: function (res) {
localStorage.setItem('token', res.token);
window.location.href = '/dashboard';
},
error: function (xhr) {
var code = xhr.responseJSON && xhr.responseJSON.code;
if (code === 'INVALID_CREDENTIALS') {
$('#login-error-alert').removeAttr('hidden');
}
}
});