
pizza-shop/
├── src/main/java/
│ └── com.pizzashop/
│ ├── controller/
│ │ ├── MenuController.java
│ │ └── MemberController.java
│ ├── dto/
│ │ ├── request/
│ │ │ ├── MemberRegisterRequest.java
│ │ │ └── MemberLoginRequest.java
│ │ └── response/
│ │ ├── ApiResponse.java
│ │ ├── MemberLoginResponse.java
│ ├── mapper/
│ │ ├── MenuMapper.java
│ │ └── MemberMapper.java
│ ├── service/
│ │ ├── MenuService.java
│ │ └── MemberService.java
│ └── vo/
│ ├── MenuVO.java
│ └── MemberVO.java
| 패키지 | 설명 |
|---|---|
controller | 클라이언트 요청을 받아 응답을 반환 |
dto/request | 클라이언트에서 받는 요청 데이터 |
dto/response | 클라이언트에게 보내는 응답 데이터 |
mapper | MyBatis를 통해 DB와 직접 통신 |
service | 비즈니스 로직 처리 (중복 확인, 비밀번호 검증 등) |
vo | DB 테이블과 1:1 매핑되는 데이터 객체 |
Controller → Service → Mapper → DB 순서로 요청이 흘러간다.
PIZZA-SHOP/
├── css/
│ ├── style.css
│ └── auth.css
├── js/
│ ├── common.js
│ ├── menu.js
│ └── auth.js
├── index.html
├── login.html
└── register.html
| 파일 | 설명 |
|---|---|
auth.css | 로그인 / 회원가입 페이지 전용 스타일 |
common.js | 공통 기능 (updateNav, logout) |
auth.js | 로그인 / 회원가입 API 연동 |
login.html | 로그인 페이지 |
register.html | 회원가입 페이지 |
요청 흐름은 다음과 같다.
클라이언트 → Controller → Service → Mapper → DB
회원 정보를 저장할 members 테이블을 먼저 생성한다.
CREATE TABLE IF NOT EXISTS members (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
name VARCHAR(50) NOT NULL,
role VARCHAR(20) DEFAULT 'USER',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
| 컬럼 | 타입 | 설명 |
|---|---|---|
id | INT | 고유번호 (자동 증가) |
username | VARCHAR(50) | 로그인 아이디 (중복 불가) |
password | VARCHAR(100) | 비밀번호 (실무에서는 암호화 필요) |
name | VARCHAR(50) | 닉네임 |
role | VARCHAR(20) | 권한 (USER / ADMIN, 기본값 USER) |
created_at | TIMESTAMP | 가입일 (자동 저장) |
DB 테이블과 1:1로 매핑되는 데이터 객체다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberVO {
private int id; // 고유번호
private String username; // 로그인 아이디
private String password; // 비밀번호 (실무에서는 암호화 필요)
private String name; // 닉네임
private String role; // 권한 (USER / ADMIN)
private String createdAt; // 가입일
}
| 어노테이션 | 설명 |
|---|---|
@Data | getter, setter, toString 자동 생성 |
@NoArgsConstructor | 기본 생성자 자동 생성 |
@AllArgsConstructor | 전체 필드 생성자 자동 생성 |
클라이언트와 주고받는 데이터를 명확하게 분리하기 위해 DTO를 사용한다.
VO를 그대로 사용하면 클라이언트가
role같은 민감한 필드를 임의로 넘길 수 있어 보안에 취약하다. DTO를 사용하면 필요한 필드만 주고받을 수 있다.
MemberRegisterRequest - 회원가입 요청 데이터
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberRegisterRequest {
private String username;
private String password;
private String name;
}
MemberLoginRequest - 로그인 요청 데이터
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginRequest {
private String username;
private String password;
}
ApiResponse - 공통 응답 데이터
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse {
private String result; // "ok" 또는 "fail"
private String message; // 성공 또는 실패 메시지
}
MemberLoginResponse - 로그인 응답 데이터
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginResponse {
private String result; // 실행 결과
private String name; // 사용자 이름
private String role; // 사용자 역할 (USER / ADMIN)
private String username; // 사용자 아이디
}
로그인 응답에
password는 절대 포함하면 안 된다.
@Mapper
public interface MemberMapper {
// 아이디 중복 체크 / 로그인 시 회원 조회
@Select("SELECT * FROM members WHERE username = #{username}")
MemberVO findByUsername(String username);
// 회원가입 INSERT
// id, role, created_at은 DB에서 자동 생성되므로 쿼리에서 제외
@Insert("INSERT INTO members(username, password, name) "
+ "VALUES (#{username}, #{password}, #{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(MemberVO member);
}
| 메서드 | 설명 |
|---|---|
findByUsername | 아이디로 회원 조회 (중복 체크 / 로그인에 사용) |
insert | 회원 정보 DB에 저장 |
@Options(useGeneratedKeys = true, keyProperty = "id")는 INSERT 후 자동 생성된 id 값을 MemberVO.id에 다시 넣어준다.
@Service
public class MemberService {
@Autowired
private MemberMapper memberMapper;
// 1. 같은 아이디가 DB에 존재하는지 조회
// 2. 있으면 false 반환 -> Controller에서 "중복 아이디" 응답
// 3. 없으면 INSERT 실행 -> 성공 시 true 반환
public boolean register(MemberRegisterRequest request) {
MemberVO existing = memberMapper.findByUsername(request.getUsername());
if (existing != null) {
return false; // 중복 아이디 -> 가입 거절
}
// Request -> VO 변환
MemberVO member = new MemberVO();
member.setUsername(request.getUsername());
member.setPassword(request.getPassword());
member.setName(request.getName());
return memberMapper.insert(member) > 0;
}
}
role과created_at은 DB에서 자동으로 설정되므로 따로 넣지 않아도 된다.
@RestController
@RequestMapping("/api/member")
@CrossOrigin(origins = "*")
public class MemberController {
@Autowired
private MemberService memberService;
// POST /api/member/register
// 요청 Body : {"username": "hong", "password": "1234", "name": "홍길동"}
// 응답 : {"result": "ok", "message": "회원가입 완료"}
@PostMapping("/register")
public ApiResponse register(@RequestBody MemberRegisterRequest request) {
boolean ok = memberService.register(request);
if (ok) {
return new ApiResponse("ok", "회원가입 완료");
} else {
return new ApiResponse("fail", "이미 사용 중인 아이디입니다.");
}
}
}
| 어노테이션 | 설명 |
|---|---|
@RestController | Controller + ResponseBody, JSON 자동 반환 |
@RequestMapping | 공통 URL prefix /api/member 설정 |
@CrossOrigin | 프론트에서 API 호출 허용 (CORS 설정) |
@PostMapping | POST 요청 처리 |
@RequestBody | JSON 요청 Body를 DTO로 변환 |
<div class="auth-container">
<div class="auth-box">
<h2>회원가입</h2>
<div class="form-group">
<label>아이디</label>
<input type="text" id="regId" placeholder="아이디를 입력하세요" />
</div>
<div class="form-group">
<label>비밀번호</label>
<input type="password" id="regPw" placeholder="비밀번호를 입력하세요" />
</div>
<div class="form-group">
<label>이름</label>
<input type="text" id="regName" placeholder="이름을 입력하세요" />
</div>
<button class="btn-submit" onclick="register()">회원가입</button>
<p class="auth-link">
이미 계정이 있으신가요? <a href="login.html">로그인</a>
</p>
</div>
</div>
const AUTH_API = "http://localhost:8080/api/member";
function register() {
const id = document.getElementById("regId").value.trim();
const pw = document.getElementById("regPw").value.trim();
const name = document.getElementById("regName").value.trim();
if (!id || !pw || !name) {
alert("모든 항목을 입력해 주세요.");
return;
}
fetch(`${AUTH_API}/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: id, password: pw, name: name }),
})
.then((res) => res.json())
.then((data) => {
if (data.result === "ok") {
alert("가입 완료! 로그인 페이지로 이동합니다.");
setTimeout(() => {
location.href = "login.html";
}, 1500);
} else {
alert("가입 실패 : 아이디를 확인해 주세요.");
}
})
.catch(() => alert("서버 오류"));
}
| 단계 | 설명 |
|---|---|
| 입력값 검증 | 빈 값이면 alert 후 종료 |
fetch POST 요청 | username, password, name을 JSON으로 전송 |
| 응답 처리 | result === "ok" 면 로그인 페이지로 이동 |
// 1. 아이디로 회원 조회
// 2. 비밀번호 일치하면 MemberVO 반환 (로그인 성공)
// 3. 아이디가 없거나 비밀번호 틀리면 null 반환 (로그인 실패)
public MemberVO login(String username, String password) {
MemberVO member = memberMapper.findByUsername(username);
if (member != null && member.getPassword().equals(password)) {
return member;
}
return null;
}
// POST /api/member/login
// 요청 Body : {"username": "hong", "password": "1234"}
// 응답(성공) : {"result": "ok", "name": "홍길동", "role": "USER", "username": "hong"}
// 응답(실패) : {"result": "fail", "name": null, "role": null, "username": null}
@PostMapping("/login")
public MemberLoginResponse login(@RequestBody MemberLoginRequest request) {
MemberVO member = memberService.login(request.getUsername(), request.getPassword());
if (member != null) {
return new MemberLoginResponse("ok", member.getName(), member.getRole(), member.getUsername());
} else {
return new MemberLoginResponse("fail", null, null, null);
}
}
<div class="auth-container">
<div class="auth-box">
<h2>로그인</h2>
<div class="form-group">
<label>아이디</label>
<input type="text" id="loginId" placeholder="아이디를 입력하세요" />
</div>
<div class="form-group">
<label>비밀번호</label>
<input type="password" id="loginPw" placeholder="비밀번호를 입력하세요" />
</div>
<button class="btn-submit" onclick="login()">로그인</button>
<p class="auth-link">
계정이 없으신가요? <a href="register.html">회원가입</a>
</p>
</div>
</div>
function login() {
const id = document.getElementById("loginId").value.trim();
const pw = document.getElementById("loginPw").value.trim();
if (!id || !pw) {
alert("아이디와 비밀번호를 입력해 주세요.");
return;
}
fetch(`${AUTH_API}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: id, password: pw }),
})
.then((res) => res.json())
.then((data) => {
if (data.result === "ok") {
localStorage.setItem("loginUser", data.name);
localStorage.setItem("loginRole", data.role);
localStorage.setItem("loginUserId", data.username);
alert(`${data.name}님 환영합니다😊`);
setTimeout(() => {
location.href = "index.html";
}, 1500);
} else {
alert("아이디 또는 비밀번호가 틀렸습니다.");
}
})
.catch(() => alert("서버 오류"));
}
| 단계 | 설명 |
|---|---|
| 입력값 검증 | 빈 값이면 alert 후 종료 |
fetch POST 요청 | username, password를 JSON으로 전송 |
| 로그인 성공 | name, role, username을 localStorage에 저장 후 메인으로 이동 |
| 로그인 실패 | 오류 메시지 alert |
로그인 성공 시
localStorage에 사용자 정보를 저장해두면 다른 페이지에서도 로그인 상태를 유지할 수 있다.
// nav 업데이트
function updateNav() {
const isLoggedIn = localStorage.getItem("loginUser");
const loginRole = localStorage.getItem("loginRole");
const navArea = document.getElementById("navArea");
if (isLoggedIn) {
const adminMenu = loginRole === "ADMIN" ? `<a href="admin.html">메뉴 관리</a>` : "";
const cartMenu = loginRole === "USER" ? `<a href="cart.html">장바구니</a>` : "";
navArea.innerHTML = `
<a href="index.html">메뉴</a>
${adminMenu}
${cartMenu}
<a href="#">로그아웃</a>
`;
}
}
// 로그아웃
function logout() {
localStorage.clear();
location.href = "login.html";
}
로그인 상태면 역할에 따라 nav가 동적으로 바뀐다. ADMIN이면 메뉴 관리, USER면 장바구니가 추가된다.
📸 [스크린샷 - 회원가입 화면]
아이디, 비밀번호, 이름을 입력하고 회원가입 버튼을 클릭하면 가입 완료 알림이 뜨고 자동으로 로그인 페이지로 이동한다.

📸 [스크린샷 - 로그인 화면]
아이디와 비밀번호를 입력하고 로그인 버튼을 클릭하면 환영 메시지가 뜨고 메인 페이지로 이동한다.

📸 [스크린샷 - 로그인 성공 후 nav 변경된 화면]
로그인 성공 시 localStorage에 사용자 정보가 저장되고, nav가 역할에 따라 동적으로 변경된다. USER면 장바구니, ADMIN이면 메뉴 관리 링크가 추가된다.

이번 글에서는 회원가입과 로그인 기능을 백엔드와 프론트엔드로 나눠서 구현해봤다.
다음 편에서는 관리자 페이지에서 메뉴 CRUD 기능을 구현할 예정이다.