AWS Back Day 75. "Spring Boot와 React를 이용한 도서 관리 페이지 만들기

이강용·2023년 4월 18일
0

Spring Boot

목록 보기
10/20

로그인 페이지 만들기

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

Book Management Spring Project 생성

spring project 생성하기

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

java 비밀번호 정규식

^(?=.*[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;
}

AOP 의존성 추가

<!-- 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;
}

React 연결 작업

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;
profile
HW + SW = 1

0개의 댓글