이전글
https://greed-yb.tistory.com/305
application.properties
# 쿼리문을 보여준다
spring.jpa.show-sql=true
# 쿼리문을 가독성 있게 바꿔준다
spring.jpa.properties.hibernate.format_sql=true
JPA 를 적용한 상태고 쿼리를 Log 에 찍히게 한다
회원가입 페이지
https://greed-yb.tistory.com/301
이전글을 참고하여 Bootstrap 에 있던 register.html 을 register.tsx 파일로 만들었다
import React, {useState} from "react";
import axios from "axios";
import {Link} from "react-router-dom";
function Register() {
// 변수 선언
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
// 회원가입
const handleRegister = async (e: { preventDefault: () => void; }) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:7172/api/register', {
name,
email,
username,
password,
}, {withCredentials: true }); // 쿠키를 자동 입력해 준다
if(response.data.message == "success"){
// 회원가입하면 로그인 페이지로
alert("회원가입 되셨습니다.");
window.location.href = '/signin';
}else if(response.data.message == "duplication") {
alert("이미 가입된 ID 입니다");
}else {
console.error('register failed');
}
} catch (error) {
console.error('register error:', error);
}
};
return(
<main>
<div className="container">
<section
className="section register min-vh-100 d-flex flex-column align-items-center justify-content-center py-4">
<div className="container">
<div className="row justify-content-center">
<div
className="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center">
<div className="d-flex justify-content-center py-4">
<a href="index.html" className="logo d-flex align-items-center w-auto">
<img src="assets/img/logo.png" alt=""/>
<span className="d-none d-lg-block">NiceAdmin</span>
</a>
</div>
<div className="card mb-3">
<div className="card-body">
<div className="pt-4 pb-2">
<h5 className="card-title text-center pb-0 fs-4">Create an Account</h5>
<p className="text-center small">Enter your personal details to create
account</p>
</div>
<form className="row g-3 needs-validation" noValidate onSubmit={handleRegister}>
<div className="col-12">
<label htmlFor="name" className="form-label">Your Name</label>
<input type="text" name="name" className="form-control" id="name"
required onChange={(e)=> setName(e.target.value)}/>
<div className="invalid-feedback">Please, enter your name!</div>
</div>
<div className="col-12">
<label htmlFor="email" className="form-label">Your Email</label>
<input type="email" name="email" className="form-control" id="email"
required onChange={(e)=> setEmail(e.target.value)}/>
<div className="invalid-feedback">Please enter a valid Email
adddress!
</div>
</div>
<div className="col-12">
<label htmlFor="username" className="form-label">ID</label>
<div className="input-group has-validation">
<span className="input-group-text" id="inputGroupPrepend">@</span>
<input type="text" name="username" className="form-control"
id="username" required onChange={(e)=> setUsername(e.target.value)}/>
<div className="invalid-feedback">Please choose a username.
</div>
</div>
</div>
<div className="col-12">
<label htmlFor="password" className="form-label">Password</label>
<input type="password" name="password" className="form-control"
id="password" required onChange={(e) => setPassword(e.target.value)}/>
<div className="invalid-feedback">Please enter your password!</div>
</div>
<div className="col-12">
<button className="btn btn-primary w-100" type="submit">Create Account
</button>
</div>
<div className="col-12">
<p className="small mb-0">Already have an account?
<Link to="/signin">Log in</Link>
</p>
</div>
</form>
</div>
</div>
<div className="credits">
Designed by <a href="https://bootstrapmade.com/">BootstrapMade</a>
</div>
</div>
</div>
</div>
</section>
</div>
</main>
);
}
export default Register;
입력 필드의 변수들을 선언하고
각각 태그에 onChange 함수를 , <form> 태그에 onSubmit 함수를 설정한다
login 페이지에서 해당 버튼을 누루면 회원가입 페이지로 이동될 수 있도록 코드를 수정한다
React 에 맞춰서 <a> 태그를 <Link> 태그로 변경해준다
App.tsx
이전글에서 layout 때문에 login 페이지만
분리를 하였었는데 register 페이지에도 적용을 하기 위해 변경한다
import {BrowserRouter, Route, Routes, useLocation} from "react-router-dom";
import Header from "./components/layout/header";
import Footer from "./components/layout/footer";
import Sidebar from "./components/layout/sidebar";
import Dashboard from "./components/pages/dashboard";
import Profile from "./components/pages/profile";
import Login from "./components/pages/login";
import PrivateRoute from "./components/utils/privateRoute";
import Register from "./components/pages/register";
// 페이지 layout 구분하기
function AppLayout(){
const location = useLocation();
const layoutPass = ["/signin" , "/register"]; // 로그인 , 회원가입 페이지 제외
const pageCheck = layoutPass.includes(location.pathname);
return (
<div className="App">
{!pageCheck && <Header/>}
{!pageCheck && <Sidebar/>}
<Routes>
<Route path="/signin" element={<Login />}/>
<Route path="/register" element={<Register />}/>
<Route path="/" element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}/>
<Route path="/profile" element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}/>
</Routes>
{!pageCheck && <Footer/>}
</div>
);
}
function App() {
return (
<BrowserRouter>
<AppLayout/>
</BrowserRouter>
);
}
export default App;
UserVo
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Data
@Entity
@Table(name = "usermember")
public class UserVo {
@Id // PK 지정
// @GeneratedValue(strategy = GenerationType.IDENTITY) // 값 자동 생성
// private Long id;
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
}
@Data 는 lombok 의 어노테이션이며 Getter , Setter 를 자동완성 해준다
( 값이 증가되는 increment 의 기능이 필요하다면
@GeneratedValue(strategy = GenerationType.IDENTITY) 을 이용하면 된다 )
UserRepository
import com.example.reactpractice.vo.userVo.UserVo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<UserVo, String> { // Entity 와 PK의 데이터 타입 지정
}
기본적인 CRUD 의 경우 따로 메서드를 작성할 필요가 없다
상속하는 JpaRepository<Entity 명 , PK 컬럼 데이터 타입 > 만 지정해 놓는다
ResigetController
import com.example.reactpractice.repository.user.UserRepository;
import com.example.reactpractice.vo.userVo.UserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/*")
public class RegisterController {
@Autowired
private UserRepository userRepository;
@PostMapping("/register")
public ResponseEntity<Map<String, Object>> register(@RequestBody UserVo vo) throws Exception{
Map<String, Object> data = new HashMap<>();
// Optional 을 사용하면 nullPoint 발생을 줄여준다
// ID 가 있는지 select
Optional<UserVo> idCheck = userRepository.findById(vo.getUsername());
// ID 가 없으면 회원가입
if(idCheck.isEmpty()){
try{
// 회원가입 시 비밀번호를 암호화 시킨다
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String changePassword = passwordEncoder.encode(vo.getPassword());
vo.setPassword(changePassword);
// 회원가입
userRepository.save(vo);
data.put("message", "success");
return ResponseEntity.ok(data);
}catch (Exception e){
data.put("message", "fail");
return ResponseEntity.ok(data);
}
}else{
// ID 가 중복되면
data.put("message", "duplication");
return ResponseEntity.ok(data);
}
}
}
JPA 사용 시 간단한 CRUD 의 경우
따로 메서드 필요 없이
findById() - @ID 로 검색
findAll() - 모두 검색
save() - 저장, 수정
delete() - 삭제
...등등
으로 가능하다
회원가입 - TEST
LoginController
@RestController
@RequestMapping("/api/*")
public class LoginController {
@Autowired
private UserRepository userRepository;
private final JwtTokenUtil jwtTokenUtil;
// jwtTokenUtil 에 설정한 시간과 동일한 시간으로 맞춘다
private int accessTime = 1 * 60; // 1분
private int refreshTime = 5 * 60; // 5분
public LoginController(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> loginCheck(@RequestBody UserVo vo, HttpServletResponse response) {
// 반환할 데이터 구성
Map<String, Object> data = new HashMap<>();
try {
/////////////////////////////////////////////////////////////////////
// 세션 기반 인증 및 특정 로직일때 사용함 , 혹시 필요할까 싶어서 작성 , 지워도 무방하다
// 사용자 인증
Authentication authentication = new UsernamePasswordAuthenticationToken(vo.getUsername(), vo.getPassword());
// 사용자 정보 set
SecurityContextHolder.getContext().setAuthentication(authentication);
/////////////////////////////////////////////////////////////////////
// 사용자가 입력한 비밀번호와 DB에 암호화 되어 있는 사용자의 비밀번호를 비교하기 위함
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//Optional 을 사용하면 nullPoint 발생을 줄여준다
Optional<UserVo> dbCheck = userRepository.findById(vo.getUsername());
// 사용자가 입력한 비밀번호와 DB의 비밀번호가 일치하지 않으면 login 페이지로 이동
if (dbCheck.isEmpty() || !passwordEncoder.matches(vo.getPassword(), dbCheck.get().getPassword())) {
// 인증 실패 시 다시 로그인 페이지로
data.put("message", "fail");
return ResponseEntity.ok(data);
}
// JWT 토큰 생성
String accessToken = jwtTokenUtil.generateAccessToken(vo.getUsername());
String refreshToken = jwtTokenUtil.generateRefreshToken(vo.getUsername());
// 쿠키에 JWT 저장
Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setDomain("localhost"); // localhost에 쿠키 설정
accessTokenCookie.setPath("/");
accessTokenCookie.setMaxAge(accessTime);
accessTokenCookie.setHttpOnly(false); // javascript 로 cookie 접근 허용(false)
response.addCookie(accessTokenCookie);
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setDomain("localhost"); // localhost에 쿠키 설정
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(refreshTime);
refreshTokenCookie.setHttpOnly(false); // javascript 로 cookie 접근 허용(false)
response.addCookie(refreshTokenCookie);
data.put("accessToken", accessToken);
data.put("refreshToken", refreshToken);
data.put("role", "guest"); // test 용 임의 권한 지정
data.put("message", "success");
}catch (Exception e){
e.printStackTrace();
}
return ResponseEntity.ok(data);
}
....
....
....
}
Test 용으로 사용자 비밀번호를 1111 지정했던 코드를 삭제하고
JPA 를 이용하여 DB 에서 유저 정보를 가져와서
사용자가 입력한 Password 와 DB의 Password 를 비교하고 일치하면 login 하는 코드를 넣었다
login.tsx
import React, {useState} from "react";
import axios from "axios";
import {Link} from "react-router-dom";
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async (e: { preventDefault: () => void; }) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:7172/api/login', {
username,
password,
}, {withCredentials: true }); // 쿠키를 자동 입력해 준다
if(response.data.message == "success"){
// 로그인 성공 후 홈 페이지로 이동
window.location.href = '/';
}else {
alert("로그인 실패하였습니다.");
}
} catch (error) {
console.error('Login failed:', error);
}
};
return(
...
..
..);
}
export default Login;
로그인 - TEST
로그인 실패와 로그인 성공
입력 필드의 공백 또는 null 검증이나 if 문 또는 ResponseEntity 등
테스트를 위해 간단히 작성하거나 구현하지 않았기 때문에 참고 하길 바란다
'개발 > Spring' 카테고리의 다른 글
[SpringBoot] React + TypeScript + JPA 프로젝트 (6) - 게시판 만들기(페이징 처리) (0) | 2024.11.07 |
---|---|
[SpringBoot] React + TypeScript + JPA 프로젝트 (5) - 게시판 만들기(목록) (0) | 2024.11.04 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (3) - SpringSecurity 적용 (0) | 2024.10.31 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (2) - Bootstrap 적용 (1) | 2024.10.25 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (1) - 생성 (0) | 2024.10.24 |
댓글