Skip to content
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,29 @@
- 로그아웃은 `GET` 요청으로도 구현 가능하나 로그아웃은 상태를 바꾸는 행위이므로 `POST`가 더 적절하다고 판단하였다. a 태그는 `POST` 요청을 보낼 수 없기 때문에 JS 함수를 onclick 이벤트로 삽입하여 구현하였다.
- 에러 페이지를 표시할 때 적절한 HTTP status code를 응답에 포함시키면 더 좋겠다.
- 지금은 로그인되지 않은 경우(401)와 로그인되어 있지만 다른 사용자의 회원정보수정 페이지에 접근하는 경우(403)를 구분하여 처리하기 어려워 생략하였다.

## 미션 5 - 게시글 권한부여

### 요구사항

- [x] 로그인한 사용자만 게시글의 세부내용을 볼 수 있다.
- 로그인하지 않은 사용자는 게시글의 목록만 볼 수 있다.
- 로그인하지 않은 사용자가 게시글의 내용에 접근하면 로그인 페이지로 이동한다.
- [x] 로그인한 사용자만 게시글을 작성할 수 있다.
- 게시물의 글쓴이 정보는 사용자 이름(name)을 사용한다. 게시물 작성 양식에서 글쓴이 필드는 제거한다.
- 로그인하지 않은 사용자가 게시물 작성 페이지에 접근하면 로그인 페이지로 이동한다.
- [x] 로그인한 사용자는 자신의 글을 수정할 수 있다.
- 글 수정 요청은 @PutMapping으로 매핑한다.
- 수정하기 폼 과 수정하기 기능은 로그인 사용자와 글쓴이의 사용자 아이디가 같은 경우에만 가능하다.
- 상황에 따라 "다른 사람의 글을 수정할 수 없다."와 같은 에러 메시지를 출력하는 페이지로 이동하도록 구현한다.
- [x] 로그인한 사용자는 자신의 글을 삭제할 수 있다.
- 글 삭제 요청은 @DeleteMapping으로 매핑한다.
- 삭제하기는 로그인 사용자와 글쓴이의 사용자 아이디가 같은 경우에만 가능하다.
- 상황에 따라 "다른 사람의 글을 수정할 수 없다."와 같은 에러 메시지를 출력하는 페이지로 이동하도록 구현한다.
- [x] 스프링 부트, 웹 MVC로 구현한다.
- API로 구현하지 않고, 템플릿 기반으로 구현한다.
- HttpSession을 활용해서 구현한다.

### 전 단계 피드백 반영

- [x] login 및 logout을 처리할 때 HttpSession 객체를 UserService 계층까지 전달하지 않도록 한다.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
id 'java'
}

group = 'com.kakao'
group = 'kr.codesquad'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

Expand Down
25 changes: 19 additions & 6 deletions src/main/java/kr/codesquad/cafe/article/Article.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public class Article {

private Long id;
private LocalDateTime timestamp;
private String writer;
private String writerUserId;
private String writerName;
private String title;
private String contents;

Expand All @@ -28,13 +29,21 @@ public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}

public String getWriter() {
return writer;
public String getWriterUserId() {
return writerUserId;
}

public void setWriter(String writer) {
Assert.hasText(writer, "작성자는 공백이어선 안 됩니다.");
this.writer = writer;
public String getWriterName() {
return writerName;
}

public void setWriterName(String writerName) {
this.writerName = writerName;
}

public void setWriterUserId(String writerUserId) {
Assert.hasText(writerUserId, "작성자는 공백이어선 안 됩니다.");
this.writerUserId = writerUserId;
}

public String getTitle() {
Expand All @@ -54,4 +63,8 @@ public void setContents(String contents) {
Assert.hasText(contents, "글 내용은 공백이어선 안 됩니다.");
this.contents = contents;
}

public boolean hasId() {
return id != null;
}
}
52 changes: 47 additions & 5 deletions src/main/java/kr/codesquad/cafe/article/ArticleController.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package kr.codesquad.cafe.article;

import kr.codesquad.cafe.user.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpSession;

@Controller
public class ArticleController {
Expand All @@ -25,9 +26,11 @@ public String viewQuestions(Model model) {
}

@PostMapping("/questions")
public String processCreationForm(ArticleCreationForm form) {
public String processCreationForm(ArticleCreationForm form, HttpSession session) {
Article article = new Article();
article.setWriter(form.getWriter());
User writer = (User) session.getAttribute("currentUser");
article.setWriterUserId(writer.getUserId());
article.setWriterName(writer.getName());
article.setTitle(form.getTitle());
article.setContents(form.getContents());
service.post(article);
Expand All @@ -42,4 +45,43 @@ public String viewQuestion(@PathVariable("id") long id, Model model) {
return "qna/show";
}

@GetMapping("/questions/{id}/form")
public String viewUpdateForm(@PathVariable("id") long id, Model model) {
model.addAttribute("article", service.retrieve(id));

return "qna/updateForm";
}

@PutMapping("/questions/{id}/update")
public String processUpdateForm(@PathVariable("id") long id, ArticleCreationForm form, HttpSession session) {
User currentUser = (User) session.getAttribute("currentUser");
String writerUserId = service.retrieve(id).getWriterUserId();

if (!currentUser.userIdIs(writerUserId)) {
return "redirect:/badRequest";
}

Article article = new Article();
article.setId(id);
article.setWriterName(currentUser.getName());
article.setTitle(form.getTitle());
article.setContents(form.getContents());
service.update(article);

return "redirect:/questions/{id}";
}

@DeleteMapping("/questions/{id}")
public String deleteArticle(@PathVariable("id") long id, HttpSession session) {
User currentUser = (User) session.getAttribute("currentUser");
String writerUserId = service.retrieve(id).getWriterUserId();

if (!currentUser.userIdIs(writerUserId)) {
return "redirect:/badRequest";
}

service.deleteById(id);

return "redirect:/";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,9 @@

public class ArticleCreationForm {

private String writer;
private String title;
private String contents;

public String getWriter() {
return writer;
}

public void setWriter(String writer) {
this.writer = writer;
}

public String getTitle() {
return title;
}
Expand Down
20 changes: 17 additions & 3 deletions src/main/java/kr/codesquad/cafe/article/ArticleRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
@Repository
public class ArticleRepository {

private static final String SQL_SAVE_ARTICLE = "INSERT INTO ARTICLE(WRITER, TITLE, CONTENTS) VALUES(?, ?, ?)";
private static final String SQL_SAVE_ARTICLE =
"INSERT INTO ARTICLE(WRITER_USERID, WRITER_NAME, TITLE, CONTENTS) VALUES(?, ?, ?, ?)";
private static final String SQL_UPDATE_ARTICLE =
"UPDATE ARTICLE SET WRITER_NAME=?, TITLE=?, CONTENTS=? WHERE ID=?";
private static final String SQL_FIND_ARTICLE = "SELECT * FROM ARTICLE WHERE ID = ?";
private static final String SQL_FIND_ARTICLE_ALL = "SELECT * FROM ARTICLE";
private static final String SQL_DELETE_ARTICLE = "DELETE FROM ARTICLE WHERE ID=?";
private final JdbcTemplate jdbcTemplate;

@Autowired
Expand All @@ -23,8 +27,13 @@ public ArticleRepository(JdbcTemplate jdbcTemplate) {
}

public void save(Article article) {
if (article.hasId()) {
jdbcTemplate.update(SQL_UPDATE_ARTICLE,
article.getWriterName(), article.getTitle(), article.getContents(), article.getId());
return;
}
jdbcTemplate.update(SQL_SAVE_ARTICLE,
article.getWriter(), article.getTitle(), article.getContents());
article.getWriterUserId(), article.getWriterName(), article.getTitle(), article.getContents());
}

public Optional<Article> findOne(long id) {
Expand All @@ -40,11 +49,16 @@ private RowMapper<Article> articleRowMapper() {
Article article = new Article();
article.setId(rs.getLong("id"));
article.setTimestamp(rs.getTimestamp("timestamp").toLocalDateTime());
article.setWriter(rs.getString("writer"));
article.setWriterUserId(rs.getString("writer_userid"));
article.setWriterName(rs.getString("writer_name"));
article.setTitle(rs.getString("title"));
article.setContents(rs.getString("contents"));

return article;
});
}

public void deleteById(long id) {
jdbcTemplate.update(SQL_DELETE_ARTICLE, id);
}
}
8 changes: 8 additions & 0 deletions src/main/java/kr/codesquad/cafe/article/ArticleService.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public void post(Article article) {
repository.save(article);
}

public void update(Article article) {
repository.save(article);
}

public Article retrieve(long id) {
return repository.findOne(id)
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 게시물입니다."));
Expand All @@ -28,4 +32,8 @@ public Article retrieve(long id) {
public List<Article> retrieveAll() {
return repository.findAll();
}

public void deleteById(long id) {
repository.deleteById(id);
}
}
34 changes: 34 additions & 0 deletions src/main/java/kr/codesquad/cafe/system/MvcConfig.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package kr.codesquad.cafe.system;

import kr.codesquad.cafe.system.intercepter.LoginRequiredInterceptor;
import kr.codesquad.cafe.system.intercepter.UserAuthenticationInterceptor;
import kr.codesquad.cafe.system.intercepter.WriterAuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

Expand All @@ -15,5 +20,34 @@ public void addViewControllers(ViewControllerRegistry registry) {

registry.addViewController("/join").setViewName("users/form");
registry.addViewController("/questions/new").setViewName("qna/form");
registry.addViewController("/badRequest").setViewName("badRequest");
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginRequiredInterceptor())
.addPathPatterns("/questions/**")
.addPathPatterns("/users/**");

registry.addInterceptor(userAuthenticationInterceptor())
.addPathPatterns("/users/*/form");

registry.addInterceptor(writerAuthenticationInterceptor())
.addPathPatterns("/questions/*/form");
}

@Bean
public LoginRequiredInterceptor loginRequiredInterceptor() {
return new LoginRequiredInterceptor();
}

@Bean
public UserAuthenticationInterceptor userAuthenticationInterceptor() {
return new UserAuthenticationInterceptor();
}

@Bean
public WriterAuthenticationInterceptor writerAuthenticationInterceptor() {
return new WriterAuthenticationInterceptor();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kr.codesquad.cafe.system.intercepter;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginRequiredInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();

if (session.getAttribute("currentUser") == null) {
if ("POST".equals(request.getMethod())) {
return true;
}

session.setAttribute("destinationAfterLogin", request.getRequestURI());
response.sendRedirect("/login");

return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package kr.codesquad.cafe.system.intercepter;

import kr.codesquad.cafe.user.User;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

public class UserAuthenticationInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map pathVariable = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
String targetUserId = (String) pathVariable.get("userId");
User currentUser = (User) request.getSession().getAttribute("currentUser");

if (currentUser.userIdIs(targetUserId)) {
return true;
}

response.sendRedirect("/badRequest");

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.codesquad.cafe.system.intercepter;

import kr.codesquad.cafe.article.Article;
import kr.codesquad.cafe.user.User;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class WriterAuthenticationInterceptor implements HandlerInterceptor {

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
Article article = (Article) modelAndView.getModelMap().getAttribute("article");
String writerUserId = article.getWriterUserId();
User currentUser = (User) request.getSession().getAttribute("currentUser");

if (!currentUser.userIdIs(writerUserId)) {
response.sendRedirect("/badRequest");
}
}
}
Loading