npm i react-icons --save
components > UI > Login > LoginInput
LoginInput.js
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react';
import Input from '../../atoms/Input/Input';
const loginInput = css`
position: relative;
margin-bottom: 20px;
border-bottom: 1px solid #dbdbdb;
padding: 0px 5px 0px 40px ;
width: 100%;
`;
const icon = css`
position: absolute;
transform: translateY(-50%);
top: 50%;
left: 0px;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
`;
const LoginInput = ({ type, placeholder, onChange, children }) => {
return (
<div css={ loginInput }>
<div css={ icon }>{ children }</div>
<Input
type={type}
placeholder={placeholder}
onChange={onChange}/>
</div>
);
};
export default LoginInput;
pages > Login
Login.js
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react';
import LoginInput from '../../components/UI/Login/LoginInput/LoginInput';
import { FiUser, FiLock } from 'react-icons/fi';
import { Link } from 'react-router-dom';
import { BsGoogle } from 'react-icons/bs';
import { SiNaver,SiKakao } from 'react-icons/si';
;
const container = css`
display: flex;
flex-direction: column;
align-items: center;
padding: 70px 30px;
`;
const logo = css`
margin: 50px 0px;
font-size: 34px;
font-weight: 600;
`;
const mainContainer = css`
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #dbdbdb;
border-radius: 10px;
padding: 40px 20px;
width: 400px;
`;
const authForm = css`
width: 100%;
`;
const inputLabel = css`
margin-left: 5px;
font-size: 12px;
font-weight: 600;
`;
const forgotPassword = css`
display: flex;
justify-content: flex-end;
align-content: center;
margin-bottom: 45px;
width: 100%;
font-size: 12px;
font-weight: 600;
`;
const loginButton = css`
margin: 10px 0px ;
border: 1px solid #dbdbdb;
border-radius: 7px;
width: 100%;
height: 50px;
background-color: white;
font-weight: 900;
cursor: pointer;
&:hover {
background-color: #fafafa;
}
&:active {
background-color: #eee;
}
`;
const oauth2Container = css`
display: flex;
justify-content: center;
align-items: center;
margin: 20px;
width: 100%;
`;
const oauth2 = (provider) => css`
display: flex;
justify-content: center;
align-items: center;
margin: 0px 10px;
border: 1px solid ${provider === "google" ? "#0075ff" : provider === "naver" ? "#19ce60": "#ffdc00"};
border-radius: 50%;
width: 50px;
height: 50px;
font-size: ${provider === "kakao" ? "30px" : "20px"};
cursor: pointer;
&:hover {
background-color: ${provider === "google" ? "#0075ff" : provider === "naver" ? "#19ce60": "#ffdc00"};
}
`;
const signupMessage = css`
margin-top: 20px;
font-size: 14px;
font-weight: 600;
color: #777;
`;
const register = css`
margin-top: 10px;
font-weight: 600;
`;
const Login = () => {
return (
<div css= {container}>
<header>
<h1 css= { logo } >Login</h1>
</header>
<main css={ mainContainer }>
<div css={authForm}>
<label css={ inputLabel }>Email</label>
<LoginInput type="email" placeholder="Type your email">
<FiUser />
</LoginInput>
<label css={ inputLabel }>Password</label>
<LoginInput type="password" placeholder="Type your password">
<FiLock />
</LoginInput>
<div css= { forgotPassword }><Link to="/forgot/password">Forgot Password?</Link></div>
<button css={ loginButton }>LOGIN</button>
</div>
<div></div>
</main>
<div css = { signupMessage }>Or Sign Up Using</div>
<div css= {oauth2Container}>
<div css={ oauth2("google") }><BsGoogle /></div>
<div css={ oauth2("naver") }><SiNaver /></div>
<div css={ oauth2("kakao") }><SiKakao /></div>
</div>
<div css= { signupMessage }>Or Sign Up Using</div>
<footer>
<div css = { register }><Link to="/register">SIGN UP</Link></div>
</footer>
</div>
);
};
export default Login;
styles/Global
reset.js(수정)
import { css } from '@emotion/react';
export const Reset = css`
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
* {
box-sizing : border-box;
color : #333;
}
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
margin: 10px auto;
border: 3px solid #dbdbdb;
border-radius: 10px;
width: 768px;
height: 1000px;
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
`;
App.js (수정)
import { Global } from '@emotion/react';
import { Reset } from './styles/Global/reset';
import { Route,Routes } from 'react-router-dom';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
function App() {
return (
<>
<Global styles={ Reset }></Global>
<Routes>
<Route exact path="/login" Component={Login} />
<Route path= "/register" Component={Register} />
</Routes>
</>
);
}
export default App;
중간 과정
pages > Register
Register.js
Dependencies 추가 (7개)
workspace에 다운 받은 폴더 옮기기
pom.xml 수정
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.toyproject</groupId>
<artifactId>bookmanagement</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>bookmanagement</name>
<description>Book Management project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application 확장자 yml로 변경
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/book_management
username: root
password: root
mybatis:
mapper-locations:
- /mappers/*.xml
jwt:
secret:uAdzVUhnjML7pCLQLDapBdNacinrqdRjbaqLD7sMUfe0ILk8KKqk5Xb0WncSuIre
secreatkey generator(검색)
resources > mappers 폴더 생성
해당 경로로 패키지 생성
controller > AuthenticationController(클래스 생성)
package com.toyproject.bookmanagement.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class AuthenticationController {
@PostMapping("/login")
public ResponseEntity<?> login() {
return ResponseEntity.ok(null);
}
@PostMapping("/signup")
public ResponseEntity<?> signup() {
return ResponseEntity.ok(null);
}
}
config > SecurityConfig 생성
package com.toyproject.bookmanagement.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.httpBasic().disable();
http.formLogin().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers("/auth/**")
.permitAll()
.anyRequest()
.authenticated();
}
}
^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$
- ^$ ( 정규식의 시작과 끝)
(?=.*[A-Za-z])
->(?=
앞쪽 일치).*
= 모든 글자(a-zA-Z)- (?=.*
\\d
) -> 숫자 = 모든 숫자(?=.*[@$!%*#?&])
-> 특수 문자 = 모든 특수 문자
dto > auth(패키지생성) > SignupReqDto
package com.toyproject.bookmanagement.dto.auth;
import javax.validation.constraints.Pattern;
import lombok.Data;
@Data
public class SignupReqDto {
private String email;
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$")
private String password;
private String name;
}
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>3.0.5</version>
</dependency>
aop > ValidationAop 생성
package com.toyproject.bookmanagement.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ValidationAop {
@Pointcut("@annotation(com.toyproject.bookmanagement.aop.annotation.ValidAspect)")
private void pointCut() {}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
aop > annotation (패키지 생성) > annotation 생성
ValidAspect
package com.toyproject.bookmanagement.aop.annotation;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(METHOD)
public @interface ValidAspect {
}
exception(패키지)
CustomException
package com.toyproject.bookmanagement.exception;
import java.util.Map;
import lombok.Getter;
@Getter
public class CustomException extends RuntimeException {
private static final long serialVersionUID = 5362220879197727700L;
private Map<String, String> errorMap;
public CustomException(String message) {
super(message);
}
public CustomException(String message, Map<String, String> errorMap) {
super(message);
this.errorMap = errorMap;
}
}
ValidationAop(수정)
package com.toyproject.bookmanagement.aop;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import com.toyproject.bookmanagement.exception.CustomException;
@Aspect
@Component
public class ValidationAop {
@Pointcut("@annotation(com.toyproject.bookmanagement.aop.annotation.ValidAspect)")
private void pointCut() {}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
BindingResult bindingResult = null;
for(Object arg : args) {
if(arg.getClass() == BeanPropertyBindingResult.class) {
bindingResult = (BeanPropertyBindingResult)arg;
}
}
if(bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
errorMap.put(error.getField(), error.getDefaultMessage());
});
throw new CustomException("Validation Failed", errorMap);
}
return joinPoint.proceed();
}
}
controller > advice(패키지)
AdviceController
package com.toyproject.bookmanagement.controller.advice;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.toyproject.bookmanagement.dto.common.ErrorResponseDto;
import com.toyproject.bookmanagement.exception.CustomException;
@RestControllerAdvice
public class AdviceController {
@ExceptionHandler(CustomException.class)
public ResponseEntity<?> customException(CustomException e){
return ResponseEntity.badRequest().body(new ErrorResponseDto<>(e.getMessage(), e.getErrorMap()));
}
}
dto > common
ErrorResponseDto
package com.toyproject.bookmanagement.dto.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponseDto<T> {
private String message;
private T errorData;
}
Register.js (수정)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React, { useState } from 'react';
import LoginInput from '../../components/UI/Login/LoginInput/LoginInput';
import { FiUser, FiLock } from 'react-icons/fi';
import { Link } from 'react-router-dom';
import {BiRename} from 'react-icons/bi';
import axios from 'axios';
;
const container = css`
display: flex;
flex-direction: column;
align-items: center;
padding: 70px 30px;
`;
const logo = css`
margin: 50px 0px;
font-size: 34px;
font-weight: 600;
`;
const mainContainer = css`
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #dbdbdb;
border-radius: 10px;
padding: 40px 20px;
width: 400px;
`;
const authForm = css`
width: 100%;
`;
const inputLabel = css`
margin-left: 5px;
font-size: 12px;
font-weight: 600;
`;
const loginButton = css`
margin: 10px 0px ;
border: 1px solid #dbdbdb;
border-radius: 7px;
width: 100%;
height: 50px;
background-color: white;
font-weight: 900;
cursor: pointer;
&:hover {
background-color: #fafafa;
}
&:active {
background-color: #eee;
}
`;
const signupMessage = css`
margin-top: 20px;
font-size: 14px;
font-weight: 600;
color: #777;
`;
const register = css`
margin-top: 10px;
font-weight: 600;
`;
const Register = () => {
const [registerUser, setRegisterUser] = useState( { email:"",password:"",name:"" } )
const onChangeHandle = (e) => {
const { name, value } = e.target;
setRegisterUser( { ...registerUser, [name]:value } );
}
const registeSubmit = () => {
const data = {
...registerUser
}
const option = {
headers: {
"Content-Type" : "application/json"
}
}
axios
.post("http://localhost:8080/auth/signup", JSON.stringify(data), option)
.then( response => {
console.log("성공");
console.log(response);
})
.catch(error => {
console.log("에러");
console.log(error);
});
console.log("비동기 테스트");
}
return (
<div css= {container}>
<header>
<h1 css= { logo } >SIGN UP</h1>
</header>
<main css={ mainContainer }>
<div css={authForm}>
<label css={ inputLabel }>Email</label>
<LoginInput type="email" placeholder="Type your email" onChange={onChangeHandle} name="email">
<FiUser />
</LoginInput>
<label css={ inputLabel }>Password</label>
<LoginInput type="password" placeholder="Type your password" onChange={onChangeHandle} name="password">
<FiLock />
</LoginInput>
<label css={ inputLabel }>Name</label>
<LoginInput type="text" placeholder="Type your name" onChange={onChangeHandle} name ="name">
<BiRename />
</LoginInput>
<button css={ loginButton } onClick={registeSubmit}>REGISTER</button>
</div>
<div></div>
</main>
<div css= { signupMessage }>Already a user?</div>
<footer>
<div css = { register }><Link to="/login">LOGIN</Link></div>
</footer>
</div>
);
};
export default Register;
axios 비동기 처리
Input.js (수정)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react';
const input = css`
border: none;
outline: none;
padding: 5px 10px ;
width: 100%;
height: 40px;
`;
const Input = ( { type, placeholder, onChange,name} ) => {
return (
<>
<input css={input}
type={type}
placeholder={placeholder}
onChange={onChange}
name={name}/>
</>
);
};
export default Input;
LoginInput.js (수정)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react';
import Input from '../../atoms/Input/Input';
const loginInput = css`
position: relative;
margin-bottom: 20px;
border-bottom: 1px solid #dbdbdb;
padding: 0px 5px 0px 40px ;
width: 100%;
`;
const icon = css`
position: absolute;
transform: translateY(-50%);
top: 50%;
left: 0px;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
`;
const LoginInput = ({ type, placeholder, onChange, name, children }) => {
return (
<div css={ loginInput }>
<div css={ icon }>{ children }</div>
<Input
type={type}
placeholder={placeholder}
onChange={onChange}
name={name}
/>
</div>
);
};
export default LoginInput;