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

유효성 검사, 비밀번호 일치 확인, 약관 동의 검사가 동작합니다. 이메일 형식 오류 / 비밀번호 불일치 등 실제 에러 메시지를 확인해 보세요.

로그인

로그인

서비스를 이용하려면 로그인 하세요

회원가입

회원가입

회원가입을 하세요

8자 이상, 영문자와 숫자를 포함해야 합니다

비밀번호 찾기

비밀번호 찾기

가입한 이메일로 재설정 링크를 보내드립니다

← 로그인으로 돌아가기

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');
		}
	}
});