참고 링크 : https://wikidocs.net/book/7601
오라클DB와 intelliJ로 작업하였습니다
게시판의 질문, 답변에는 누가 글을 작성했는지 알려주는 "글쓴이" 항목이 필요하다.
이번에는 Question과 Answer 엔티티에 "글쓴이"에 해당되는 author 속성을 추가해 보자.
Question 속성 추가
먼저 Question 엔티티에 author 속성을 추가하자.
package com.gosari.repick_project.question;
import com.gosari.repick_project.answer.Answer;
import com.gosari.repick_project.user.SiteUser;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
@Setter
@Getter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Integer id;
private String subject;
private String content;
@Column(insertable = false, updatable = false, columnDefinition = "date default LOCALTIMESTAMP")
private Date createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer>answerList; //질문엔티티에 답변엔티티참조 (질문1:답변N)
@ManyToOne //여러개의 질문이 한명의 사용자에게 작성될수있으므로 @ManyToOne
private SiteUser author; /*author 속성추가*/
}
author 속성은 SiteUser 엔티티를 @ManyToOne으로 적용했다.
여러개의 질문이 한 명의 사용자에게 작성될 수 있으므로 @ManyToOne 관계가 성립한다.
Answer 속성 추가
Question 엔티티와 같은 방법으로 Answer 엔티티에도 author 속성을 추가하자.
package com.gosari.repick_project.answer;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.user.SiteUser;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Date;
@Setter
@Getter
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Integer id;
private String content;
@Column(insertable = false, updatable = false, columnDefinition = "date default LOCALTIMESTAMP")
private Date createDate;
@ManyToOne
private Question question; //답변엔티티에 질문엔티티참조 (질문1:답변N)
@ManyToOne //여러개의 질문이 한명의 사용자에게 작성될수있으므로 @ManyToOne
private SiteUser author; /*author 속성추가*/
}
테이블 확인
위와 같이 Question, Answer 엔티티를 변경하고 DBtool에 접속하여 question, answer 테이블을 확인해 보자.
question, answer 테이블에 author_id 컬럼이 생성된 것을 확인할 수 있다.
이 컬럼에는 site_user 테이블의 id 값이 저장되어 SiteUser 엔티티와 연결된다.
author 저장
이제 Question, Answer 엔티티에 author 속성이 추가되었으므로 질문과 답변 저장시에 author도 함께 저장할 수 있다.
질문, 답변에 글쓴이를 추가한다는 느낌으로 작업을 진행하자.
답변에 작성자 저장하기
먼저 답변을 작성하는 AnswerController를 수정하자.
package com.gosari.repick_project.answer;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.question.QuestionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
import java.security.Principal;
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
@PostMapping("/create/{id}")/*AnswerForm을 사용하도록 컨트롤러 변경*/
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult,
Principal principal) {
/*createAnswer 메서드에 Principal 객체를 매개변수로 지정*/
Question question = this.questionService.getQuestion(id);
if(bindingResult.hasErrors()){
model.addAttribute("question", question);
return "question_detail";
}
this.answerService.create(question, answerForm.getContent());
return String.format("redirect:/question/detail/%s", id);
}
}
현재 로그인한 사용자에 대한 정보를 알기 위해서는 스프링 시큐리티가 제공하는 Principal 객체를 사용해야 한다.
위와 같이 createAnswer 메서드에 Principal 객체를 매개변수로 지정하면 된다.
principal.getName()을 호출하면 현재 로그인한 사용자의 사용자명(사용자ID)을 알수 있다.
principal 객체를 사용하면 이제 로그인한 사용자의 사용자명을 알수 있으므로
사용자명을 통해 SiteUser객체를 조회할 수 있다.
먼저 User 서비스를 통해 SiteUser를 조회할 수 있는 getUser 메서드를 UserService에 추가하자.
package com.gosari.repick_project.user;
import com.gosari.repick_project.exception.DataNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public SiteUser create(String username, String email, String password) {
SiteUser user = new SiteUser();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
this.userRepository.save(user);
return user;
}
/*SiteUser를 조회할수있는 getUser메서드*/
public SiteUser getUser(String username){
Optional<SiteUser> siteUser = this.userRepository.findByusername(username);
if(siteUser.isPresent()){
return siteUser.get();
}else{
throw new DataNotFoundException("siteuser not found");
}
}
}
UserRepository에 이미 findByusername을 선언했으므로 쉽게 만들수 있다.
그리고 답변 저장시 작성자를 저장할 수 있도록 다음과 같이 AnswerService를 수정하자.
package com.gosari.repick_project.answer;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.user.SiteUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Date;
@RequiredArgsConstructor
@Service
public class AnswerService {
private final AnswerRepository answerRepository;
public Answer create(Question question, String content, SiteUser author) {
Answer answer = new Answer();
answer.setContent(content);
answer.setCreateDate(new Date());
answer.setQuestion(question);
answer.setAuthor(author);
this.answerRepository.save(answer);
return answer;
}
}
create 메서드에 SiteUser 객체를 추가로 전달받아 답변 저장시 author 속성에 세팅했다.
이제 답변을 작성하면 작성자도 함께 저장될 것이다.
이제 다음과 같이 AnswerController의 createAnswer 메서드를 완성해 보자.
package com.gosari.repick_project.answer;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.question.QuestionService;
import com.gosari.repick_project.user.SiteUser;
import com.gosari.repick_project.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
import java.security.Principal;
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
private final UserService userService;
@PostMapping("/create/{id}")/*AnswerForm을 사용하도록 컨트롤러 변경*/
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult,
Principal principal) {
/*createAnswer 메서드에 Principal 객체를 매개변수로 지정*/
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if(bindingResult.hasErrors()){
model.addAttribute("question", question);
return "question_detail";
}
this.answerService.create(question, answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s", id);
}
}
principal 객체를 통해 사용자명을 얻은 후에 사용자명을 통해
SiteUser 객체를 얻어서 답변을 등록하는 AnswerService의 create 메서드에 전달하여 답변을 저장하도록 했다.
질문에 작성자 저장하기
질문도 답변과 동일한 방법이므로 빠르게 작성해 보자.
먼저 작성자 정보를 저장하기 위해 QuestionService를 다음과 같이 수정하자.
package com.gosari.repick_project.question;
import com.gosari.repick_project.user.SiteUser;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import com.gosari.repick_project.exception.DataNotFoundException;
@RequiredArgsConstructor //QuestionRepository 생성자 생성해줌
@Service
public class QuestionService {
private final QuestionRepository questionRepository;
public List<Question> getList(){
return this.questionRepository.findAll();
}
/*Question 데이터 조사*/
public Question getQuestion(Integer id){
Optional<Question> question = this.questionRepository.findById(id);
if(question.isPresent()){
return question.get();
}else{
throw new DataNotFoundException("question not found");
}
}
/*페이징*/
public Page<Question> getList(int page){
List<Sort.Order> sorts = new ArrayList<>();
sorts.add(Sort.Order.desc("createDate"));
Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
return this.questionRepository.findAll(pageable);
}
/*질문데이터를 저장하는 create메서드*/
public void create(String subject, String content, SiteUser user){
Question q = new Question();
q.setSubject(subject);
q.setContent(content);
q.setCreateDate(new Date()); //.now()가 아닌 new Date()로 해주면 된다
q.setAuthor(user);
this.questionRepository.save(q);
}
}
create 메서드에 SiteUser 매개변수를 추가하여 Question 데이터를 생성했다.
이어서 QuestionController도 다음과 같이 수정하자.
package com.gosari.repick_project.question;
import com.gosari.repick_project.answer.AnswerForm;
import com.gosari.repick_project.user.SiteUser;
import com.gosari.repick_project.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.security.Principal;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
private final UserService userService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model,
@RequestParam(value="page", defaultValue="0") int page){
Page<Question> paging = this.questionService.getList(page);
model.addAttribute("paging", paging); /*페이징*/
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id,
AnswerForm answerForm) { /*AnswerForm추가*/
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
/*질문등록하기*/
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult bindingResult,
Principal principal) {
if (bindingResult.hasErrors()) {
return "question_form";
}
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
}
이제 다시 로컬 서버를 시작하고 로그인한 다음 질문 · 답변 등록을 테스트해보자. 잘 될 것이다.
SbbApplicationTests.java
QuestionService의 create 메서드의 매개변수로 SiteUser가 추가되었기 때문에 이전에 작성한 테스트 케이스가 오류가 발생할 것이다. 테스트 케이스의 오류를 임시 해결하기 위해 다음과 같이 수정하자.
package com.mysite.sbb;
(... 생략 ...)
@Test
void testJpa() {
for (int i = 1; i <= 300; i++) {
String subject = String.format("테스트 데이터입니다:[%03d]", i);
String content = "내용무";
this.questionService.create(subject, content, null);
}
}
}
로그인이 필요한 메서드
하지만 로그아웃 상태에서 질문 또는 답변을 등록하면 다음과 같은 서버오류(500 오류)가 발생한다.
이 오류는 principal 객체가 널(null)값이라서 발생한 오류이다.
principal 객체는 로그인을 해야만 생성되는 객체이기 때문이다.
이 문제를 해결하려면 principal 객체를 사용하는 메서드에
@PreAuthorize("isAuthenticated()") 애너테이션을 사용해야 한다.
@PreAuthorize("isAuthenticated()") 애너테이션이 붙은 메서드는 로그인이 필요한 메서드를 의미한다.
만약 @PreAuthorize("isAuthenticated()") 애너테이션이 적용된 메서드가 로그아웃 상태에서 호출되면
로그인 페이지로 이동된다.
QuestionController를 다음과 같이 수정하자.
package com.gosari.repick_project.question;
import com.gosari.repick_project.answer.AnswerForm;
import com.gosari.repick_project.user.SiteUser;
import com.gosari.repick_project.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.security.Principal;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
private final UserService userService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model,
@RequestParam(value="page", defaultValue="0") int page){
Page<Question> paging = this.questionService.getList(page);
model.addAttribute("paging", paging); /*페이징*/
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id,
AnswerForm answerForm) { /*AnswerForm추가*/
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/
@PreAuthorize("isAuthenticated()") //추가
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
/*질문등록하기*/
@PreAuthorize("isAuthenticated()") //추가
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult bindingResult,
Principal principal) {
if (bindingResult.hasErrors()) {
return "question_form";
}
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
}
로그인이 필요한 메서드들에 @PreAuthorize("isAuthenticated()") 애너테이션을 적용했다.
AnswerController도 다음과 같이 수정하자.
package com.gosari.repick_project.answer;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.question.QuestionService;
import com.gosari.repick_project.user.SiteUser;
import com.gosari.repick_project.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
import java.security.Principal;
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
private final UserService userService;
@PreAuthorize("isAuthenticated()") //추가
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult,
Principal principal) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if(bindingResult.hasErrors()){
model.addAttribute("question", question);
return "question_detail";
}
this.answerService.create(question, answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s", id);
}
}
그리고 @PreAuthorize 애너테이션이 동작할 수 있도록 SecurityConfig를 다음과 같이 수정해야 한다.
package com.gosari.repick_project.security;
import com.gosari.repick_project.user.UserSecurityService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@RequiredArgsConstructor
@Configuration //스프링환경설정파일의미-스프링시큐리티의설정
@EnableWebSecurity //모든요청URL이 스프링시큐리티의 제어받음
@EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize 애너테이션을 사용하기 위해
public class SecurityConfig {
private final UserSecurityService userSecurityService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
/*스프링 시큐리티의 세부 설정은 SecurityFilterChain 빈을 생성하여 설정 가능*/
/* 모든 인증되지 않은 요청을 허락한다는 의미 - 로그인하지않더라도 모든페이지에 접근가능*/
http.authorizeRequests().antMatchers("/**").permitAll()
/*스프링 시큐리티의 로그인 설정을 담당하는 부분*/
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
/*로그아웃을 위한 설정 추가*/
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
;
return http.build();
}
//PasswordEncoder 빈(bean)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/*AuthenticationManager 스프링 시큐리티의 인증을 담당*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
SecurityConfig에 적용한
@EnableGlobalMethodSecurity 애너테이션의 prePostEnabled = true 설정은 QuestionController와 AnswerController에서 로그인 여부를 판별하기 위해 사용했던 @PreAuthorize 애너테이션을 사용하기 위해 반드시 필요하다.
이렇게 수정한후 로그아웃 상태에서 질문을 등록하거나 답변을 등록하면 자동으로 로그인 화면으로 이동하는 것을 확인할 수 있을 것이다.
로그인 하지 않은 상태에서 "질문 등록" 버튼을 누르면 "로그인" 화면으로 이동한다.
그리고 로그인을 진행하면 원래 하려고 했던 "질문 등록" 화면으로 이동한다.
이것은 로그인 후에 원래 하려고 했던 페이지로 리다이렉트 시키는 스프링 시큐리티의 기능이다.
disabled
한가지 더 생각해 봐야 할 것이 있다. 현재 질문 등록은 로그아웃 상태에서는 아예 글을 작성할 수 없어서 만족스럽다.
하지만 답변 등록은 로그아웃 상태에서도 글을 작성할 수 있다. 물론 답변 작성 후 <저장하기>를 누르면 자동으로 로그인 화면으로 이동되므로 큰 문제는 아니지만 작성한 답변이 사라지는 문제가 있다.
작성한 글이 사라지는 문제를 해결하려면 로그아웃 상태에서는 아예 답변 작성을 못하게 막는 것이 좋을 것이다.
question_detail.html 파일을 다음과 같이 수정하자.
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<!--질문-->
<h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;" th:text="${question.content}"></div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2 text-start">
<div th:text="${#dates.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
</div>
</div>
<!--답변의 갯수표시-->
<h5 class="border-bottom my-3 py-2"
th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<!--#list.size(이터러블객체)-타임리프가 제공하는 유틸리티로 객체의 길이를 반환-->
<!--답변 반복 시작-->
<div class="card my-3" th:each="answer : ${question.answerList}">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;" th:text="${answer.content}"></div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2 text-start">
<div th:text="${#dates.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
</div>
</div>
<!--답변 반복 끝-->
<!--답변 작성-->
<form th:action="@{|/answer/create/${question.id}|}"
th:object="${answerForm}" method="post" class="my-3">
<div th:replace="form_errors :: formErrorsFragment"></div>
<!--th:replace - 공통템플릿을 템플릿내에 삽입가능-->
<!--div엘리먼트를 form_errors.html파일의 th:fragment속성명이 formErrorsFragment인 엘리먼트로 교체-->
<textarea sec:authorize="isAnonymous()" disabled th:field="*{content}"
class="form-control" rows="10"></textarea>
<!--로그인상태가 아닌경우 disabled 속성을 적용하여 입력을 못하게 함-->
<textarea sec:authorize="isAuthenticated()" th:field="*{content}"
class="form-control" rows="10"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
</div>
</html>
<textarea sec:authorize="isAnonymous()" disabled th:field="*{content}" class="form-control" rows="10"></textarea>
<textarea sec:authorize="isAuthenticated()" th:field="*{content}" class="form-control" rows="10"></textarea>
로그인 상태가 아닌 경우 textarea 태그에 disabled 속성을 적용하여 입력을 못하게 만들었다. sec:authorize="isAnonymous()", sec:authorize="isAuthenticated()" 속성은 현재 사용자의 로그인 상태를 체크하는 속성이다.
- sec:authorize="isAnonymous()" - 현재 로그아웃 상태
- sec:authorize="isAuthenticated()" - 현재 로그인 상태
다음은 로그아웃 상태에서 disabled가 적용된 화면이다.
'Follow Work > SpringbootBoard' 카테고리의 다른 글
[StringBoot] 수정과 삭제 (23) (0) | 2022.08.17 |
---|---|
[StringBoot] 글쓴이 표시 (22) (0) | 2022.08.16 |
[StringBoot] 로그인과 로그아웃 (20) (0) | 2022.08.16 |
[StringBoot] 회원가입 (19) (0) | 2022.08.16 |
[StringBoot] 스프링 시큐리티 (18) (0) | 2022.08.16 |