package com.example.boardapp.controller;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.boardapp.domain.Member;
import com.example.boardapp.dto.SignUpRequest;
import com.example.boardapp.mapper.MemberMapper;
import jakarta.validation.Valid;
@Controller // 컨트롤러 빈으로 등록
@RequestMapping("/member") // URL prefix: 이 컨트롤러가 다루는 모든 URL 앞에 이 값을 붙인다
public class MemberController { // 멤버 관련된 요청/응답을 처리하는 컨트롤러
// 의존성 선언
private final MemberMapper memberMapper; // 멤버 데이터 다루는 용도
private final PasswordEncoder passwordEncoder; // 비밀번호 암호화하는 객체
public MemberController(MemberMapper memberMapper, PasswordEncoder passwordEncoder) {
this.memberMapper = memberMapper;
this.passwordEncoder = passwordEncoder;
}
// 회원가입 페이지 요청을 처리하는 핸들러(처리하는 사람)
@GetMapping("/join") // GET /join 요청을 처리한다
// Model: MVC(Model-View-Controller)에서 모델에 해당한다
// 모델의 역할: 데이터를 활용해 뷰를 업데이트한다
public String joinFrom(Model model) {
// 뷰의 memberForm과 가입요청(SignUpRequest)를 매핑한다
// 사용자가 가입할 때 입력한 값이 이 DTO에 저장된다
model.addAttribute("memberForm", new SignUpRequest());
// 회원가입 페이지를 클라이언트에게 전송한다.
return "member/join"; // 나중에 이 페이지를 만들거다
}
// 회원가입 처리 핸들러
@PostMapping("/join") // 요청을 처리할 주소. POST(데이터 생성 요청) /join
// dto 매개변수 앞에 두 개의 어노테이선 @Valid, @ModelAttribute가 붙은 상태
// @Valid: dto를 대상으로 사전에 정의한 유효성 검사를 실행하라
// @ModelAttribute: 가입 페이지의 memberForm과 가입요청 dto를 매핑한다
// BindingResult: validation이 가입요청DTO(SignUpRequest)를 대상으로
// 유효성 검증을 마치고 결과를 담은 객체
public String join(@Valid @ModelAttribute("memberForm") SignUpRequest dto, BindingResult
bindingResult) {
// 비밀번호와 비밀번호 확인(password confirm)이 일치하지 않을 때
if (!dto.getPassword().equals(dto.getPasswordConfirm())) {
// 이 경우 bindingResult에 직접 에러를 추가한다.
// bindingResult에 에러를 수집하는 중
// rejectValue(필드, 에러코드, 에러 메시지)
bindingResult.rejectValue("getPasswordConfirm", "passwordMismatch",
"비밀번호가 일치하지 않습니다");
}
// 사용자가 입력한 유저네임이 데이터베이스에 존재하는지 검사
// dto.getUsername(): 사용자가 가입할 때 입력한 유저네임(아이디)
// isPresent: 옵셔널의 메서드. 내부에 값이 존재하는지 확인한다 (boolean 타입으로 결과 반환)
if (memberMapper.findByUsername(dto.getUsername()).isPresent()) {
// 아이디 중복 오류를 bindingResult에 새롭게 추가한다
bindingResult.rejectValue("username", "duplicatedUsername",
"이미 존재하는 아이디입니다");
}
// bindingResult에 에러가 있다면
// 에러: validation이 추가한 에러, 우리가 직접 추가한 에러 등
if (bindingResult.hasErrors()) {
// 에러메시지와 함께 가입페이지를 다시 전송한다
return "member/join";
}
// 모든 검사를 통과한 경우 가입 절차를 시작한다.
// 멤버 객체를 생성한다
// 비밀번호를 암호화해서 저장한다
Member member = new Member(dto.getUsername(), passwordEncoder.encode(dto.getPassword()));
// 데이터베이스에 저장한다
memberMapper.save(member);
// 로그인 요청 핸들러로 이동한다
return "redirect:/member/login";
}
// 로그인 페이지 요청 핸들러
@GetMapping("/login")
public String loginForm() {
// 클라이언트에게 로그인 페이지를 전송한다
return "member/login";
}
}
MemberController는 회원(Member)과 관련된 요청을 처리하는 컨트롤러이다.
사용자의 회원가입 및 로그인 요청을 받아 처리하며 회원가입 페이지를 화면에 표시하거나 사용자가 입력한 회원 정보를 검증하는 역할을 한다.
또한 MemberMapper를 통해 데이터베이스와 통신하고 PasswordEncoder를 이용해 비밀번호를 암호화하여 안전하게 저장할 수 있도록 한다.
Spring MVC의 Model 객체를 활용하여 화면(View)에 필요한 데이터를 전달한다.
package com.example.boardapp.controller;
import java.security.Principal;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.boardapp.domain.Member;
import com.example.boardapp.domain.Post;
import com.example.boardapp.dto.PostDetailsResponse;
import com.example.boardapp.dto.PostListResponse;
import com.example.boardapp.dto.PostWriteRequest;
import com.example.boardapp.mapper.MemberMapper;
import com.example.boardapp.mapper.PostMapper;
import jakarta.validation.Valid;
@Controller // 컨트롤러 빈으로 등록한다
@RequestMapping("/posts") // URL prefix: 이 컨트롤러가 처리하는 URL 앞에 공통적으로 붙인다
public class PostController {
// 의존성 선언하기
private final PostMapper postMapper; // 게시물 데이터 처리
private final MemberMapper memberMapper; // 멤버 데이터 처리
public PostController(PostMapper postMapper, MemberMapper memberMapper) {
this.postMapper = postMapper;
this.memberMapper = memberMapper;
}
// 글 작성 페이지 요청 핸들러
@GetMapping("/write") // GET /write 요청을 처리한다
public String writeForm(Model model) {
// 뷰의 postForm(게시물 작성폼)과 게시물 생성 DTO(PostwriteRequest)를 매핑한다
model.addAttribute("postForm", new PostWriteRequest());
// 서버의 응답
return "post/write"; // 게시물 작성 페이지를 전송한다
}
// 글 작성을 처리하는 핸들러
@PostMapping // Post /prefix(/post)
// dto 앞에 @valid, @ModelAttribute 어노테이션이 붙은 상태
// @valid: 사전에 정의한 유효성 검사를 실행하라
// @ModelAttribute: postForm(게시물 작성폼)과 dto를 매핑한다.
// BindingResult: validation이 유효성 검사를 수행하고 결과를 담은 객체
public String write(@Valid @ModelAttribute("postForm") PostWriteRequest dto, BindingResult
bindingResult, Principal principal) {
// 게시물 유효성 검사를 통과하지 못한 경우 예) 사용자가 글 제목이나 본문을 입력 안하고 전송한 경우
if (bindingResult.hasErrors()) {
// 게시물 작성페이지를 전송한다. 오류 메시지와 함께
return "post/write";
}
// 로그인한 멤버를 확인한다 (데이터 베이스 저장용)
// principal.getName(): 로그인 중인 사용자의 유저네임을 반환
Member loginMember = memberMapper.findByUsername(principal.getName())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다"));
// 게시물을 작성자 정보와 함께 저장한다
Post post = new Post(dto.getTitle(), dto.getContent(), loginMember);
postMapper.save(post);
// 방금 작성한 글 보기 페이지로 이동
return "redirect:/posts/details?id=" + post.getId();
// .xml의 useGenerateKey=true를 활용하는 부분이 여기이다
// 게시물 생성 후 바로 id에 접근 가능
}
// 게시물 목록 페이지 요청 핸들러
@GetMapping // GET /prefix 요청을 처리한다
public String list(Model model) {
// 데이터베이스로부터 가져온 게시물들로부터 글 목록 DTO(PostListResponse)를 생성하는 과정
List<PostListResponse> posts = postMapper.findAll().stream()
.map((post) -> {
PostListResponse dto = new PostListResponse();
dto.setId(post.getId());
dto.setTitle(post.getTitle());
// 멤버 정보 중 이름만 가져온다
dto.setMemberName(post.getMember().getUsername());
return dto;
})
.collect(Collectors.toList());
// 뷰에게 dto를 전달한다
// posts(attributeName): 뷰에서 이 dto를 저장하는 부분
// 뷰는 이 데이터를 바탕으로 화면을 완성한다
model.addAttribute("posts", posts);
// 서버의 응답 (글 목록 페이지를 전송한다)
return "post/list";
}
// 게시물 상세보기 페이지
@GetMapping("/details")
// id 매개변서 앞에 @RequestParam 어노테이션이 붙은 상태
// @RequestParam: 요청-매개변수를 의미한다
// 이 요청 매개변수에 사용자가 전송한 게시물의 id가 저장되어있다. 예) /details?id=99
// 주소?요청 매개변수 키=값
public String details(@RequestParam("id") Long id, Model model) {
// id에 해당하는 게시물을 검색
Post post = postMapper.findById(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시물입니다"));
// 게시물로부터 글 상세보기 DTO(PostDetailsResponse)를 생성
PostDetailsResponse dto = new PostDetailsResponse();
dto.setId(post.getId());
dto.setTitle(post.getTitle());
dto.setContent(post.getContent());
dto.setMemberName(post.getMember().getUsername());
// 모델이 뷰에게 dto를 전달한다. 뷰는 화면을 이제 완성할 수 있다.
// 뷰의 'post' 자리에 이 dto가 들어온다
model.addAttribute("post", dto);
// 서버의 응답 (글 상세페이지 전송)
return "post/details";
}
// 게시물 삭제 요청 핸들러
@PostMapping("/delete")
// id: 사용자가 삭제를 요청한 게시물의 id
// Principal: 현재 로그인 중인 사용자 정보
public String remove(@RequestParam Long id, Principal principal) {
// 삭제할 게시물을 검색한다
Post post = postMapper.findById(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글: " + id));
// 예외를 던지면 예외처리기에서 400에러로 처리를 할 것
// 로그인한 멤버를 확인한다
Member loginMember = memberMapper.findByUsername((principal.getName()))
.orElseThrow(() -> new IllegalArgumentException("사용자 정보가 존재하지 않습니다"));
// 삭제를 요청한 사용자가 이 게시물의 작성자인지 확인
if (post.getMember().getId().equals(loginMember.getId())) {
// 게시물을 삭제
postMapper.delete(id);
}
// 게시물 목록 페이지로 돌아간다
return "redirect:/posts";
}
}
PostController는 게시글(Post)과 관련된 요청을 처리하는 컨트롤러이다.
게시글 목록 조회, 게시글 상세 조회, 게시글 작성 및 삭제 등의 기능을 담당한다.
사용자의 요청을 받아 PostMapper를 통해 데이터베이스와 통신하고 필요한 데이터를 DTO로 변환하여 화면(View)에 전달한다.
또한 Model 객체를 활용해 게시글 정보를 화면에 출력할 수 있도록 처리한다.
그 뒤 선생님께 받은 templates 파일을 넣고 실행시키면 로그인 화면과 회원가입 그리고 여러가지 구성 화면들이 나오는 것을 확인해 볼 수 있다.
힘들다...