728x90

1. MySQL 테이블 상태

CREATE TABLE `tb_board` (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `content` text NOT NULL,
  `writer_id` int NOT NULL,
  `delete_yn` varchar(1) DEFAULT 'N',
  `image` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `writer_id` (`writer_id`),
  CONSTRAINT `tb_board_ibfk_1` FOREIGN KEY (`writer_id`) REFERENCES `tb_userinfo` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `tb_userinfo` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL,
  `password` varchar(100) NOT NULL,
  `email` varchar(45) NOT NULL,
  `role` varchar(20) DEFAULT 'MEMBER',
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`,`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

2. build.gradle 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'

3. application.properties 작성

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/board?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username=아이디
spring.datasource.password=비밀번호
spring.devtools.livereload.enabled=true
spring.freemaker.cache=false
spring.thymeleaf.cache=false

# MyBatis
# mapper.xml 위치 src/main/resource/mapper/**/*.xml
mybatis.mapper-locations=classpath:mapper/**/*.xml

# camel case
mybatis.configuration.map-underscore-to-camel-case=true

# 패키지명 alias
mybatis.type-aliases-package=com.min.board.model

# mapper 로그정보
logging.level.com.min.board.model.repository=TRACE

4. 모델 추가 및 Mapper 인터페이스 추가

@Data
public class Board {

    private Long id;

    @NotNull
    @Size(min = 2, max = 30, message = "제목은 2자 이상 30자 이하입니다.")
    private String title;

    @NotNull
    @NotBlank(message = "내용을 입력하세요.")
    private String content;
    private Long writerId;
    private String writer;
    private String deleteYN;
    private Timestamp createDate;

    private String image;
}
@Mapper
public interface BoardMapper {
    // 모든 글 조회
    List<Board> selectAllBoards();

    // 게시글 개수 반환 (메인 게시글, 글 관리, 휴지통)
    int selectBoardTotalCount(Pagination pagination);

    // 삭제된 글 제외 모두 조회 (메인 게시글, 글 관리, 휴지통)
    List<Board> selectBoardList(Pagination pagination);

    // 게시글 수정
    void updateBoard(Board board);

    // 아이디로 글 찾기
    Board findById(Long id);

    // 휴지통으로 이동 (임시삭제)
    void temporaryDeleteById(Long id);

    // 글 작성
    void insertBoard(Board board);

    // 휴지통 비우기
    void permanentlyDeleteById(Long boardId);
}

5. resources/mapper 에 mapper.xml 추가

구조

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="CommonMapper">
    <sql id="paging">
        LIMIT
            #{startList}, #{listSize}
    </sql>

    <sql id="search">
        delete_yn = 'N'
        <if test="searchText != null and searchText !=''">
            AND
            (
            title LIKE CONCAT('%', #{searchText}, '%')
            OR content LIKE CONCAT('%', #{searchText}, '%')
            )
        </if>
    </sql>

    <sql id="myPost">
        delete_yn = 'N'
        <if test="writer != null and writer !=''">
            AND writer = #{writer}
        </if>
    </sql>

    <sql id="trash">
        delete_yn = 'Y'
        <if test="writer != null and writer !=''">
            AND writer = #{writer}
        </if>
    </sql>
</mapper>
  • <sql> : <include>를 통해 다른 쿼리에 활용 가능
  • refid : 참조
  • (select, update, insert, delete의) id : Mapper인터페이스 함수명과 동일
  • parameterType : #{}에 들어갈 파라미터 타입 (사용 권장하지 않음.)
  • resultType : 쿼리 결과 타입
  • #{} : 파리미터(get을 의미) -> DTO(모델)과 똑같은 필드명 작성
    • #{id} => getId
  • <if> : if문. test="조건"

참조 매퍼

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="CommonMapper">
    <sql id="paging">
        LIMIT
            #{startList}, #{listSize}
    </sql>

    <sql id="search">
        delete_yn = 'N'
        <if test="searchText != null and searchText !=''">
            AND
            (
            title LIKE CONCAT('%', #{searchText}, '%')
            OR content LIKE CONCAT('%', #{searchText}, '%')
            )
        </if>
    </sql>

    <sql id="myPost">
        delete_yn = 'N'
        <if test="writer != null and writer !=''">
            AND writer = #{writer}
        </if>
    </sql>

    <sql id="trash">
        delete_yn = 'Y'
        <if test="writer != null and writer !=''">
            AND writer = #{writer}
        </if>
    </sql>
</mapper>

6. Service에서 사용

    private final BoardMapper boardMapper;

    @Autowired
    public BoardService(BoardMapper boardMapper) {
        this.boardMapper = boardMapper;
    }

    // id를 이용해서 해당 글 수정
    public Board contentLoad(Long id) {
        Board board = boardMapper.findById(id);
        return board;
    }

 

 

728x90

회원탈퇴를 진행하는 도중 체크박스로 동의 여부를 받기 위해 체크박스 사용법을 기록

<head>
    ...
   <script type="text/javascript">
        // 체크박스 체크여부 확인
        function CheckForm(secession) {
            var chk=document.secessionForm.check.checked;
    
            if(!chk){
                alert("체크박스를 체크해주세요.");
                return false;
            }
        }
    </script>
</head>

<body>
	<form th:action="@{/account/secession}" method="post" name="secessionForm" onsubmit="return CheckForm(this)">
    	...
        	<div class="checkbox mt-3 mb-3">
			<label>
				<input type="checkbox" name="check" value="agreement"> 정말 탈퇴하시겠습니까?
			</label>
    		</div>
	</form>
</body>
  • form 태그에 name과 onsubmit(js 함수)을 설정
  • form > input 태그의 name 설정
  • script태그로 body 부분에서 정의한 name들로 함수를 정의해서 사용.
  • document.[form태그 이름].[input 체크박스 이름].checked
  • alert : 팝업창
728x90

아이디 중복 체크 같은 커스텀이 필요한 검증을 하려면 Validator를 구현해줘야 한다.

@Valid사용으로 기본 검증에 대한 내용: https://black-mint.tistory.com/25

 

[Spring Boot] @Valid로 폼 입력값 검증하기

Entity 클래스에 @Id, @Size, @NotBlank 등을 붙여주고 Controller 클래스에서 @Valid 어노테이션을 추가적으로 사용함으로써 기본적인 검증은 가능하다. 1. 기본 form 입력값 검증하기 @Data @Entity @DynamicUpd..

black-mint.tistory.com

1. Validator 인터페이스 구현

@Component
public class MemeberValidator implements Validator {

    @Autowired
    private MemberService memberService;

    @Override
    public boolean supports(Class<?> clazz) {
        return Member.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        Member member = (Member) obj;

        if(memberService.checkUsername(member.getUsername())) {
            errors.rejectValue("username", "key", "이미 존재하는 아이디입니다.");
        }
    }
}
  • validator 패키지 추가 후 Validator 인터페이스를 구현
  • 검증이 필요한 Entity 클래스를 supports에 작성
  • validate에서 View -> Controller -> MemberValidator로 넘어온 member 객체를 통해 값의 검증을 시작
  • errors.rejectValue("에러 필드명", "키값", "키 값이 없을 때 사용할 메세지") 로 구성된다.

2. Controller 적용

@PostMapping("/join")
public String join(@Valid Member member, BindingResult bindingResult){
    memeberValidator.validate(member, bindingResult);

    if(bindingResult.hasErrors()) {
        return "/account/joinForm";
    }

    String encPwd = memberService.pwdEncoding(member.getPassword());
    member.setPassword(encPwd);

    memberService.join(member);

    return "redirect:/loginForm";
}
  • 컨트롤러에서 구현한 Validator에 Member와 BindingResult를 전달해준다.

3. View 적용

<form th:action="@{/join}" method="post" th:object="${member}">          
          <div class="form-floating mt-4">
            <input type="text" class="form-control" th:field="*{username}" th:classappend="${#fields.hasErrors('username')} ? 'is-invalid'" id="floatingInput" placeholder="Username">
            <label for="floatingInput">Username</label>
            <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}" id="validationServer03Feedback" class="invalid-feedback text-start">
              Username Error
            </div>
          </div>
          <div class="form-floating">
            <input type="password" class="form-control mt-2" th:field="*{password}" th:classappend="${#fields.hasErrors('password')} ? 'is-invalid'" id="floatingPassword" placeholder="Password">
            <label for="floatingPassword">Password</label>
            <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-bottom: 8px;">
              Password Error
            </div>
          </div>
          <div class="form-floating">
            <input type="email" class="form-control" th:field="*{email}" th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'" id="floatingPassword" placeholder="Email">
            <label for="floatingPassword">example@board.com</label>
            <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-top: 8px;">
              Email Error
            </div>
          </div>
      
          <div class="checkbox mb-3">
          </div>
          <button class="w-100 btn btn-lg btn-primary mt-2" type="submit">Get started!</button>
          <a type="button" class="w-100 btn btn-lg btn-primary mt-2" th:href="@{/}">exit</a>
        </form>
  • 타임리프의 th:if가 달린 3개의 div 태그에서 에러를 출력해준다.
  • th:if=${#fields.hasErrors('필드명')} : 에러가 발생한 경우 true 값을 반환하여 div태그를 표시해준다.
  • form 태그의 th:object="{member}" : GetMapping 때 전달받은 Member의 키 값을 넣어 전달 받은 객체
  • "*{username}" :  위 설명을 참고해서 Member.getUsername()을 View에서 사용할 수 있도록 한 값이다. -> ${member.username} 과 같다고 생각!
  • th:errors="*{password}" : password에 대한 에러 내용을 표시해준다.
728x90

Entity 클래스에 @Id, @Size, @NotBlank 등을 붙여주고 Controller 클래스에서 @Valid 어노테이션을 추가적으로 사용함으로써 기본적인 검증은 가능하다.

 

1. 기본 form 입력값 검증하기

@Data
@Entity
@DynamicUpdate
@DynamicInsert
@Table(name = "tb_userinfo")
@IdClass(MemberID.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Id
    @NotNull
    @Pattern(regexp = "^[a-z0-9]*$", message = "username은 영문(소문자)과 숫자만 가능합니다.")
    @Size(min = 5, max = 20, message = "username은 5자 이상 20자 이하입니다.")
    private String username;

    @NotNull
    @Size(min = 6, max = 100, message = "암호는 6자 이상 100자 이하 입니다.")
    private String password;

    @NotNull
    @Size(min = 6, max = 45, message = "올바른 이메일을 입력하세요.")
    @Email(message = "이메일 형식을 맞춰주세요.")
    private String email;

    @Column
    private String role;
}
  • 각 필드별로 제약조건 어노테이션을 추가해준다.
@GetMapping("/joinForm")
public String joinForm(Model model) {
    model.addAttribute("member", new Member());
    return "/account/joinForm";
}

@PostMapping("/join")
public String join(@Valid Member member, BindingResult bindingResult){ // view의 form->input 의 name과 매핑됨.
    if(bindingResult.hasErrors()) {
        return "/account/joinForm";
    }

    String encPwd = memberService.pwdEncoding(member.getPassword());
    member.setPassword(encPwd);

    memberService.join(member);

    return "redirect:/loginForm";
}
  • GetMapping으로 처음 회원가입 화면으로 진입할 때 Member 객체를 만들어서 member라는 키값으로 전달해준다.
  • PostMapping에서는 View에서 데이터를 담아 전달해준 member 객체에 @Valid 어노테이션을 달아준다.
  • BindingResult.hasErrors()를 통해 기본적인 에러 유무를 알아 낼 수 있고, 다시 회원가입 화면으로 돌려보낸다.
<form th:action="@{/join}" method="post" th:object="${member}">          
          <div class="form-floating mt-4">
            <input type="text" class="form-control" th:field="*{username}" th:classappend="${#fields.hasErrors('username')} ? 'is-invalid'" id="floatingInput" placeholder="Username">
            <label for="floatingInput">Username</label>
            <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}" id="validationServer03Feedback" class="invalid-feedback text-start">
              Username Error
            </div>
          </div>
          <div class="form-floating">
            <input type="password" class="form-control mt-2" th:field="*{password}" th:classappend="${#fields.hasErrors('password')} ? 'is-invalid'" id="floatingPassword" placeholder="Password">
            <label for="floatingPassword">Password</label>
            <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-bottom: 8px;">
              Password Error
            </div>
          </div>
          <div class="form-floating">
            <input type="email" class="form-control" th:field="*{email}" th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'" id="floatingPassword" placeholder="Email">
            <label for="floatingPassword">example@board.com</label>
            <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-top: 8px;">
              Email Error
            </div>
          </div>
      
          <div class="checkbox mb-3">
          </div>
          <button class="w-100 btn btn-lg btn-primary mt-2" type="submit">Get started!</button>
          <a type="button" class="w-100 btn btn-lg btn-primary mt-2" th:href="@{/}">exit</a>
        </form>
  • 타임리프의 th:if가 달린 3개의 div 태그에서 에러를 출력해준다.
  • th:if=${#fields.hasErrors('필드명')} : 에러가 발생한 경우 true 값을 반환하여 div태그를 표시해준다.
  • form 태그의 th:object="{member}" : GetMapping 때 전달받은 Member의 키 값을 넣어 전달 받은 객체
  • "*{username}" :  위 설명을 참고해서 Member.getUsername()을 View에서 사용할 수 있도록 한 값이다. -> ${member.username} 과 같다고 생각!
  • th:errors="*{password}" : password에 대한 에러 내용을 표시해준다.
728x90

문제점 : 게시판 프로젝트를 진행 중에 로그인과 게시판 기능을 일부 완성하고 연결하려하니 JPA 관련 오류가 발생했다.

  • 이유는 Entity객체에 JPA 사용을 위한 복합키 처리와 조인 처리를 안했던 것이었다.

해결 : 데이터베이스 설계를 다음 링크와 같이 바꾸었다. https://black-mint.tistory.com/21

 

[DataBase] MySQL 복합키 설계

문제점 : USERINFO의 PK(기본키)를 참조해서 BOARD에 FK를 만들려고하는데 계속 오류가 났다. 오류의 이유는 기존 USERINFO테이블의 PK를 id컬럼에만 적용하고는 BOARD 테이블에서 USERINFO테이블의 username컬

black-mint.tistory.com

그 후 기존 Entity클래스(Member, Board)에 @IdClass 어노테이션을 적용하기 위해 [Member, MemberID, Board] 3개의 클래스로 만들었다.

@Data
@Entity
@DynamicUpdate
@DynamicInsert
@Table(name = "tb_userinfo")
@IdClass(MemberID.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Id
    @NotNull(message = "username을 입력하세요.")
    @Size(min = 1, max = 20, message = "username은 1자 이상 20자 이하입니다.")
    private String username;

    @NotNull(message = "암호를 입력하세요.")
    @Size(min = 6, max = 100, message = "암호는 6자 이상 100자 이하 입니다.")
    private String password;

    @NotNull
    @Size(min = 6, max = 45, message = "이메일은 6자 이상 45자 이하 입니다.")
    @Email
    private String email;

    @Column
    private String role;
}
  • @Data : lombok을 설치해야 사용 가능. Getter/Setter 등 자동 생성
  • @IdClass(식별자 클래스) : 식별자 클래스 지정 (복합키를 넣을 클래스)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberID implements Serializable {

    @Column(name = "id")
    private long id;

    @Column(name = "username")
    private String username;
}
  • 식별자 클래스는 public 이어야 하며, Serializable 인터페이스를 구현해야 한다. 또한, 기본 생성자 필수!
  • Entity객체와 필드명 같게 작성
  • @Data : 식별자 클래스는 equals(), hashCode()를 필수적으로 작성해줘야 한다. 이를 자동 생성
  • @AllArgsConstructor : 모든 필드 값을 파라미터로 받는 생성자를 생성
  • @NoArgsConstructor : 파라미터가 없는 생성자 생성
@Entity
@Data
@Table(name = "tb_board")
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Size(min = 2, max = 30, message = "제목은 2자 이상 30자 이하입니다.")
    private String title;

    @NotNull
    @Size(min = 1, message = "내용을 입력하세요.")
    private String content;

    @ManyToOne(targetEntity = Member.class, fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "writer", referencedColumnName = "username"),
            @JoinColumn(name = "writer_id", referencedColumnName = "id")
    })
    private Member member;
    private String image;
}
  • 게시판 - 사람 이기때문에 ManyToOne으로 구현. (여러 관계가 있으나, 주인 쪽이 FK를 관리하도록 설계하는 것이 좋다고 한다.)
  • @ManyToOne : 게시판 - 사람 관계이므로 @ManyToOne을 사용
  • @JoinColumns : 복합키 일 때 사용
  • @JoinColumn : 그림과 같이 참조하는 필드명과 참조받는 필드명을 작성
728x90

1. 의존성 추가

 - build.gradle 에 다음 의존성을 추가해준다. 

 

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'

 

 

 - Maven의 경우 pom.xml 파일에 다음을 추가한다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <scope>test</scope>
</dependency>

2. 시큐어Configure 파일을 생성해서 다음의 코드를 작성해준다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http
                .authorizeRequests()
                    .antMatchers("/css/**").permitAll()
                    .antMatchers("/board/**").authenticated()
                    .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") // 권한 설정
                    .anyRequest().permitAll() 
                    .and()
                .formLogin()
                    .loginPage("/loginForm") // 로그인 페이지
                    .loginProcessingUrl("/login")
                    .defaultSuccessUrl("/")
                    .and()
                .logout()
                    .permitAll();
    }
}
  • @EnableWebSecurity : 스프링 시큐리티 필터가 스프링 필터체인에 등록된다.
  • BCryptPasswordEncoder : 유저의 패스워드를 암호화하는 인코더
  • authorizeRequests() : 시큐리티 처리에 HttpServletRequest를 이용하겠다는 의미
  • antMatchers() : 특정 경로 지정
  • permitAll() : 접근 허용
  • authenticated() : authorizeRequests에 적힌 경로는 로그인 후에 접근 가능
  • access : 주어진 SpEL표현식의 결과가 true이면 접근 허용
  • hasRole('ROLE_ADMIN') : 권한이 ROLE_ADMIN인 경우만 접근 가능
  • anyRequest().permitAll() : 그 외 요청은 허용
  • loginProcessingUrl("/login") : 시큐리티에서 로그인을 대신 진행하도록 정의 -> 컨트롤러에 안만들어도 됨.

 

3. 비밀번호 인코딩

@PostMapping("/join")
public String join(Member member){ // view의 form->input 의 name과 매핑됨.
    String encPwd = bCryptPasswordEncoder.encode(member.getPassword());
    member.setPassword(encPwd);

    memberService.join(member);

    return "redirect:/loginForm";
}
  • 컨트롤러에서 BCryptPasswordEncoder 객체를 이용해서 encode 함수를 통해 인코딩 한다.

-

 

 

* 해당 글은 인프런에 게시된 '최주호'님의 강의를 듣고 개인적으로 정리한 글입니다.

강의 출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0

728x90

1. MySQL에서 테이블을 만들어준다.

ID에 적용할 것이므로 AI(Auto Increment) 체크!

 

2. 자바 코드에서 Model에 @GeneratedValue 어노테이션을 추가해준다.

@GeneratedValue(strategy = GenerationType.IDENTITY)

다음은 코드에 적용한 모습이다.

 

+ Recent posts