728x90
반응형
SMALL

질문이나 답변 등 글 작성 도구로 마크다운을 적용해보자.

 

마크다운 문법

목록 표시하기

여러 내용을 나열한 목록을 표시하기 위해서는 아래와 같이 작성할 수 있다.

//작성
* 자바
* 스프링 부트
* 알고리즘

//마크다운 적용
● 자바
● 스프링 부트
● 알고리즘
//작성
1. 하나
1. 둘
1. 셋

//마크다운 적용
1. 하나
2. 둘
3. 셋

 

강조 표시하기

작성한 글자에 강조 표시를 하려면 양쪽에 **를 넣어 감싸면 된다.

//작성
스프링 부트는 **자바**로 만들어진 웹 프레임워크이다.

//마크다운 적용
스프링 부트는 자바로 만들어진 웹 프레임워크이다.

 

링크 표시하기

링크 표시는 '[링크명](링크 주소)' 문법을 사용하면 된다.

//작성
스프림 홈페이지는 [https://spring.io](https://spring.io) 입니다.

//마크다운 적용
스프링 홈페이지는 https://spring.io입니다.

 

소스 코드 표시하기

소스 코드는 백쿼트(`) 3개를 연이어 위아래를 감싼다.

//작성
필요한 소스코드는 다음과 같다.
```
answerForm.setContent(answer.getContent());
```

//마크다운 적용
필요한 소스코드는 다음과 같다.
answerForm.setContent(answer.getContent());

 

인용 표시하기

>를 입력하고 1칸 띄어쓰기 후 인용구를 입력한다.

//작성
> 마크다운은 Github에서 사용하는 글쓰기 도구이다.

//마크다운 적용
| 마크다운은 Github에서 사용하는 글쓰기 도구이다.

 

 

마크다운 설치하기

build.gradle

...
//마크다운
implementation 'org.commonmark:commonmark:0.21.0'
...

build.gradle파일 우클릭 -> Gradle -> Refresh Gradle Project 클릭하면 설치된다.

서버도 재실행하자.

 

마크다운 컨포넌트 작성하기

컨트롤러에서 질문이나 답변을 조회한 후에 마크다운 라이브러리를 적용하여 변환된 HTML을 얻을 수 있다.

하지마 범용적으로 사용하기 위해 마크다운 컴포넌트를 만들고 타임리프에서 컴포넌트를 사용해보자.

com.mysite.sbb 패키지에 CommonUtil.java를 만들어보자.

CommonUtil.java

package com.mysite.sbb;

import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.stereotype.Component;


@Component
public class CommonUtil {
	public String markdown(String markdown) {
		Parser parser = Parser.builder().build();
		Node document = parser.parse(markdown);
		HtmlRenderer renderer = HtmlRenderer.builder().build();
		return renderer.render(document);
	}
}

@Component 어노테이션을 추가하여 빈으로 등록되었다. 이제 그럼 템플릿에서 이 컴포넌트를 사용할 수 있다.

markdown메서드는 String으로 받은 마크다운 텍스트를 HTML문서로 변환해준다.

 

템플릿에 마크다운 적용하기

/templates/question_detail.html

...
<!-- 질문 -->
<h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
<div class="card my-3">
	<div class="card-body">
		<div class="card-text" th:utext="${@commonUtil.markdown(question.content)}"></div>

...
<!-- 답변 반복 시작 -->
<div class="card my-3" th:each="answer : ${question.answerList}">
	<a th:id="|answer_${answer.id}|"></a>
	<div class="card-body">
		<div class="card-text" th:utext="${@commonUtil.markdown(answer.content)}"></div>
...

기존의 style속성을 제거하고 마크다운 컴포넌트를 적용했다.

 

마크다운 확인하기

**마크다운 문법으로 작성해 봅니다.**

* 리스트1
* 리스트2
* 리스트3

파이썬 홈페이지는 [여기](http://www.python.org) 를 클릭하세요.

마크다운으로 적용된 것을 확인.

 

728x90
반응형
LIST
728x90
반응형
SMALL
만약 답변이 여러 개인 상태에서 수정하거나 삭제하면 리다이렉트로 화면이 재실행되기 때문에
무조건 페이지 상단으로 이동하게 된다. 그래서 자신이 작성한 답변을 확인하려면 스크롤을 다시 내려야 하는 경우가 있다.

비동기 방식을 이용할 수도 있지만, HTML에는 URL을 호출할 때 원하는 위치로 이동해주는 앵커(anchor)태그가 있다. 즉, <a>태그를 활용하여 답변 등록, 수정, 추천 시 원하는 위치로 이동할 수 있다.

 

답변 앵커 추가하기

/templates/question_detail.html

...
<!-- 답변 반복 시작 -->
<div class="card my-3" th:each="answer : ${question.answerList}">
	<a th:id="|answer_${answer.id}|"></a>
	<div class="card-body">
...

위처럼 <a>태그를 추가하자. 여기서 id 속성은 유일한 값이어야 하므로 답변의 id값을 사용했다.

 

리다이렉트 수정하기

이제 답변 등록, 수정 시 위에서 지정한 a태그로 이동하게끔 코드를 수정해야한다.

먼저 원래 컨트롤러에서 답변 등록, 수정 후 리다이렉트하는 코드를 보면

return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());

위와 같다. 이러한 코드들을

return String.format("redirect:/question/detail/%s#answer_%s", answer.getQuestion().getId(), answer.getId());

이렇게 앵커를 추가해야한다. 이 때 수정해야 하는 곳은 총 3곳으로 답변 등록, 수정, 추천 부분이다.

추후 컨트롤러 수정 단계에서 수정해보자.

 

답변 서비스 수정하기

답변 컨트롤러에서 답변이 등록된 위치로 이동하려면 답변의 객체가 필요하다.

/answer/AnswerService.java

public Answer create(Question question, String content, SiteUser author) {
		Answer answer = new Answer();
		answer.setContent(content);
		answer.setCreateDate(LocalDateTime.now());
		answer.setQuestion(question);
		answer.setAuthor(author);
		this.answerRepository.save(answer);
		
		return answer;
	}

메서드의 리턴타입을 Answer로 수정하고 answer을 리턴하도록 했다.

 

답변 컨트롤러 수정하기

/answer/AnswerController.java

@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";
		}
		Answer answer = this.answerService.create(question, answerForm.getContent(), siteUser);
		
		return String.format("redirect:/question/detail/%s#answer_%s", 
				answer.getQuestion().getId(), answer.getId());
	}
    
    ...
    @PreAuthorize("isAuthenticated()")
	@PostMapping("/modify/{id}")
	public String answerModify(@Valid AnswerForm answerForm, BindingResult bindingResult,
			@PathVariable("id") Integer id, Principal principal) {
		if (bindingResult.hasErrors()) {
			return "answer_form";
		}
		Answer answer = this.answerService.getAnswer(id);
		if (!answer.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 업습니다.");
		}
		this.answerService.modify(answer, answerForm.getContent());
		return String.format("redirect:/question/detail/%s#answer_%s", 
				answer.getQuestion().getId(), answer.getId());
	}
    
    
    ...
    
    @PreAuthorize("isAuthenticated()")
	@GetMapping("/vote/{id}")
	public String questionVote(Principal principal, @PathVariable("id") Integer id) {
		Answer answer = this.answerService.getAnswer(id);
		SiteUser siteUser = this.userService.getUser(principal.getName());
		this.answerService.vote(answer, siteUser);
		return String.format("redirect:/question/detail/%s#answer_%s", 
				answer.getQuestion().getId(), answer.getId());
	}

answerService.create메서드를 위에서 리턴타입이 Answer로 수정했기 때문에 answer객체로 받아주고

앞서 말했듯이 return 값의 URL을 수정해준다.

 

그리고 답변 수정하는 부분과 답변 추천 기능의 return값을 수정해준다.

 

결과 확인하기

댓글을 미리 여러개 작성해놓았다. 마지막 댓글을 수저해보자.
댓글을 수정하고 저장해보자.
리다이렉트가 되어도 상단이 아닌 해당 답변위치로 이동했다. 위의 url을 보면 #answer_14를 볼 수 있다.

728x90
반응형
LIST
728x90
반응형
SMALL

Xcode 설치 및 실행

AppStore 접속
첫번째 Xcode 설치
설치 후 첫 화면

 

iOS 앱 프로젝트 생성

버튼 클릭
iOS -> App 선택 후 Next
위처럼 작성 후 Next
프로젝트를 저장할 폴더를 선택 후 Create
프로젝트 생성 후 첫 화면

라벨 추가하기

 

Main->View Controller Scene으로 이동
+버튼 클릭
Label 드래그 앤 드롭으로 화면으로 가져오기
화면에 Label 이 추가됨
라벨을 더블클릭하여 Hello World로 수정했다. 오른쪽은 한 요소의 상세 속성을 볼 수 있는 부분이다.

 

프로젝트 실행

실행 버튼을 클릭하면 빌드를 진행한다.
빌드가 완료되면 시뮬레이터가 자동으로 실행한다.

지금은 아이폰 크기에 맞춰 예상한대로 결과가 나왔다.
하지만 캔버스가 아이패드가 될 수 있고 달라질 것을 대비하여
자동으로 위치를 맞춰주는 기능이 있다.

요소를 선택하고 오른쪽 하단의 align을 클릭하면 constraints를 추가할 수 있다.

 

수직 수평 모두 체크하고 Add 2 Contraints 클릭
그럼 이렇게 가운데 위치하도록 조정됐다. 앞으로 기종이 바뀌어도 위치는 가운데로 자동으로 고정된다.

728x90
반응형
LIST
728x90
반응형
SMALL

엔티티에 속성 추가하기

질문이나 답변을 추천한 사요자를 저장해야하므로 엔티티에 속성을 추가해야한다.

/qeustion/Question.java

...
//관계주입(추천)
@ManyToMany
Set<SiteUser> voterSet;

 

/answer/Answer.java

//관계주입(추천)
@ManyToMany
Set<SiteUser> voterSet;

 

서버를 재시작하고 콘솔을 확인해보자.

 

질문 추천 기능 생성하기

추천버튼을 추가하자

/templates/question_detail.html

...
<!--추천, 수정 및 삭제 버튼-->
<div class="my-3">
	<!--추천 버튼-->
	<a href="javascript:void(0);" class="recommend btn btn-sm btn-outline-secondary"
	th:data-uri="@{|/question/vote/${question.id}|}">추천
		<span class="badge rounded-pill bg-success"
		th:text="${#lists.size(question.voter)}"></span>
	</a>

lists.size메서드를 통해 추천 수를 볼 수 있다.

추천 버튼을 클릭하면 javascript:void(0)으로 되어 있어서 아무런 동작을 하지 않는다.

대신 자바스크립트를 통해 data-uri로 이동하게 하고, 메시지 확인창으로 한 번 나타나도록 할 것이다.

 

/templates/question_detail.html

...
const recommend_elements = document.getElementsByClassName("recommend");
Array.from(recommend_elements).forEach(function(element){
	element.addEventListener('click', function(){
		if(confirm("정말로 추천하시겠습니까?")){
			location.href = this.dataset.uri;
		}
	})
})
</script>

 

기능을 서비스에 추가하자.

/question/QuestionService.java

...
public void vote(Question question, SiteUser siteUser) {
	question.getVoter().add(siteUser);
	this.questionRepostiory.save(question);
}

 

추천을 눌럿을 때 GET방식으로 작동하는 URL을 처리하도록 컨트롤러를 수정하자.

/question/QuestionController.java

@PreAuthorize("isAuthenticated()")
	@GetMapping("/vote/{id}")
	public String questionVote(Principal principal, @PathVariable("id") Integer id) {
		Question question = this.questionService.getQuestion(id);
		SiteUser siteUser = this.userService.getUser(principal.getName());
		this.questionService.vote(question, siteUser);
		return String.format("redirect:/question/detail/%s", id);
	}

 

 

 

 

답변 추천 기능 생성하기

위와 똑같으니 코드만 보자

/templates/question_detail.html

....
<!-- 추천, 수정 및 삭제 버튼 -->
			<div class="my-3">
				<!--추천 버튼-->
				<a href="javascript:void(0);" class="recommend btn btn-sm btn-outline-secondary"
				th:data-uri="@{|/answer/vote/${answer.id}|}">추천
					<span class="badge rounded-pill bg-success"
					th:text="${#lists.size(answer.voter)}"></span>
				</a>

 

/answer/AnswerService.java

...
public void vote(Answer answer, SiteUser siteUser) {
		answer.getVoter().add(siteUser);
		this.answerRepository.save(answer);
	}

 

/answer/AnswerController.java

...
	@PreAuthorize("isAuthenticated()")
	@GetMapping("/vote/{id}")
	public String questionVote(Principal principal, @PathVariable("id") Integer id) {
		Answer answer = this.answerService.getAnswer(id);
		SiteUser siteUser = this.userService.getUser(principal.getName());
		this.answerService.vote(answer, siteUser);
		return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
	}

728x90
반응형
LIST
728x90
반응형
SMALL

질문과 답변 수정 일시 표시하기

/templates/question_detail.html

...
<div class="d-flex justify-content-end">
	<div th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
		<div class="mb-2">modified at</div>
		<div th:text="${#temporals.format(question.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
	</div>
...
<div class="d-flex justify-content-end">
	<div th:if="${answer.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
		<div class="mb-2">modified at</div>
		<div th:text="${#temporals.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
	</div>

서버를 재시작하고 확인해보자.

 

수정한 질문과 답변에만 수정일시가 나타난다.

728x90
반응형
LIST
728x90
반응형
SMALL

답변 삭제 기능 추가하기

상세 템플릿에 삭제 버튼을 추가

/templates/question_detail.html

...
<!-- 수정 및 삭제 버튼 -->
<div class="my-3">
	<a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-sm btn-outline-secondary"
	sec:authorize="isAuthenticated()"
	th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
	th:text="수정"></a>
	<a href="javascript:void(0);" th:data-uri="@{|/answer/delete/${answer.id}|}"
	class="delete btn btn-sm btn-outline-secondary"
	sec:authorize="isAuthenticated()"
	th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
	th:text="삭제"></a>
</div>
<!-- 수정 및 삭제 버튼 끝 -->	
...

 

서비스 수정하기

/answer/AnswerService.java

public void delete(Answer answer) {
	this.answerRepository.delete(answer);
}

 

컨트롤러 수정하기

GET방식으로 요청되는 URL 처리하기 위해 메서드를 추가하자

/answer/AnswerController.java

@PreAuthorize("isAuthenticated()")
@GetMapping("/delete/{id}")
public String answerDelete(Principal principal, @PathVariable("id")Integer id) {
	Answer answer = this.answerService.getAnswer(id);
	if (!answer.getAuthor().getUsername().equals(principal.getName())) {
		throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제 권한이 업습니다.");
	}
	this.answerService.delete(answer);
	return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
}

 

로컬 서버를 재시작한 후 삭제를 해보자.

728x90
반응형
LIST
728x90
반응형
SMALL

답변 수정 기능 추가하기

 

[Spring Boot] 29. 게시판 질문 수정 기능 추가하기

수정 및 삭제 기능 추가수정 일시 추가하기게시판의 질문과 답변을 수정하거나 삭제 기능을 추가하기 전에 언제 수정되었는지 확인할 수 있도록 속성을 추가하자./question/Question.javapackage com.mysit

exuzii.tistory.com

 

 

[Spring Boot] 30. 게시판 질문 삭제 기능 추가하기

[Spring Boot] 29. 게시판 질문 수정 기능 추가하기수정 및 삭제 기능 추가수정 일시 추가하기게시판의 질문과 답변을 수정하거나 삭제 기능을 추가하기 전에 언제 수정되었는지 확인할 수 있도록

exuzii.tistory.com

 

위 질문 수정 삭제 기능과 매우 유사하다.

 

버튼 추가하기

/templates/question_detail.html

...
		<!-- 수정 버튼 -->
			<div class="my-3">
				<a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-sm btn-outline-secondary"
				sec:authorize="isAuthenticated()"
				th:if="${answer.author != null and #authentication.getPrincipal().getusername() == answer.author.username}"
				th:text="수정"></a>
			</div>
		<!-- 수정 버튼 끝 -->	
	</div>
</div>
<!-- 답변 반복 끝  -->

로그인한 사용자와 답변 작성자와 동일할 경우에만 수정 버튼이 활성화됟록 했다.

수정 버튼을 클릭하면 /answer/modify/답변id URL이 GET방식으로 요청된다.

 

서비스 수정하기

답변을 수정하기 위해서는 답변을 먼저 조회해야한다.

/answer/AnswerService.java

package com.mysite.sbb.answer;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.mysite.sbb.DataNotFoundException;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class AnswerService {
	private final AnswerRepository answerRepository;
	
	public void create(Question question, String content, SiteUser author) {
		Answer answer = new Answer();
		answer.setContent(content);
		answer.setCreateDate(LocalDateTime.now());
		answer.setQuestion(question);
		answer.setAuthor(author);
		this.answerRepository.save(answer);
	}
	
	public Answer getAnswer(Integer id) {
		Optional<Answer> answer = this.answerRepository.findById(id);
		if (answer.isPresent()) {
			return answer.get();
		} else {
			throw new DataNotFoundException("answer not found"); 
		}
	}
	
	public void modify(Answer answer, String content) {
		answer.setContent(content);
		answer.setModifyDate(LocalDateTime.now());
		this.answerRepository.save(answer);
	}
}

조회하기 위한  getAnswer와 modify  메서드를 추가했다.

 

컨트롤러 수정하기

다음으로 URL 처리하는 컨트롤러를 수정하자.

/answer/AnswerController.java

...
	@PreAuthorize("isAuthenticated()")
	@GetMapping("/modify/{id}")
	public String answerModify(AnswerForm answerForm, @PathVariable("id")Integer id, Principal principal) {
		Answer answer = this.answerService.getAnswer(id);
		if (!answer.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 없습니다.");
		}
		answerForm.setContent(answer.getContent());
		return "answer_form";
	}

 

답변 수정 템플릿 생성하기

/templates/answer_form.html

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
	<h5 class="my-3 border-bottom pb-2">답변 수정</h5>
	<form th:object="${answerForm}" method="post">
		<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
		<div th:replace="~{form_errors :: formErrorsFragment}"></div>
		<div class="mb-3">
			<label for="content" class="form-label">내용</label>
			<textarea th:field="*{content}" class="form-control" rows="10"></textarea>
		</div>
		<input type="submit" value="저장하기" class="btn btn-primary my-2">
	</form>
</div>
</html>

여기서도 form태그에 action속성을 사용하지 않았다.

생략하면 현재 호출된 URL로 폼이 전송된다.

 

답변 컨트롤러 재수정하기

POST방식으로 요청되는 URL을 처리하는 메서드를 추가한다.

/answer/AnswerController.java

	@PreAuthorize("isAuthenticated()")
	@PostMapping("/modify/{id}")
	public String answerModify(@Valid AnswerForm answerForm, BindingResult bindingResult,
			@PathVariable("id") Integer id, Principal principal) {
		if (bindingResult.hasErrors()) {
			return "answer_form";
		}
		Answer answer = this.answerService.getAnswer(id);
		if (!answer.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 업습니다.");
		}
		this.answerService.modify(answer, answerForm.getContent());
		return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
	}

답변 수정을 완료한 후에는 상세 페이지로 리다이렉트 한다.

 

확인하기

로컬서버 재시작한 후 확인해보자.

수정 버튼 나타남.
내용 수정
내용 수정 완료

728x90
반응형
LIST
728x90
반응형
SMALL
 

[Spring Boot] 29. 게시판 질문 수정 기능 추가하기

수정 및 삭제 기능 추가수정 일시 추가하기게시판의 질문과 답변을 수정하거나 삭제 기능을 추가하기 전에 언제 수정되었는지 확인할 수 있도록 속성을 추가하자./question/Question.javapackage com.mysit

exuzii.tistory.com

수정 기능은 위 게시글 참고!

 

질문 삭제 기능 생성하기

질문 삭제 버튼 만들기

질문 수정과 마찬가지로 상세 화면에 삭제 버튼을 추가하자.

/templates/question_detail.html

<!--수정 및 삭제 버튼-->
<div class="my-3">
	<!--수정 버튼-->
	<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
	sec:authorize="isAuthenticated()"
	th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
	th:text="수정"></a>
	<!--삭제 버튼-->
	<a href="javascript:void(0)" th:data-uri="@{|/question/delete/${question.id}|}" class="delete btn btn-sm btn-outline-secondary"
	sec:authorize="isAuthenticated()"
	th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
	th:text="삭제"></a>
</div>
<!--수정 및 삭제 버튼 끝-->

로그인한 사용자가 자신의 질문에서 삭제 버튼을 클릭하면 자바스크립트 코드가 실행되도록 했다.

또 삭제를 실행할 URL을 얻기 위해 data-uri속성을 사용했다.

class속성에는 삭제 이벤트 확인을 위해 delete 클래스를 추가했다.

이러한 이유는 바로 삭제하지 않고 "삭제하시겠습니까?"메시지를 통해 확일절차를 넣기 위해서이다.

 

삭제를 위한 자바스크립트 작성하기

/templates/layout.html

...
	<!-- 자바 스크립트 Start -->
	<th:block layout:fragment="script"></th:block>
	<!-- 자바 스크립트 End -->
</body>

 

/templates/question_detail.html

...
<script layout:fragment="script" type='text/javascript'>
const delete_elements = document.getElementsByClassName("delete");	
Array.from(delete_elements).forEach(function(element) {
	element.addEventListener('click', function(){
		if(confirm("정말로 삭제하시겠습니까?")){
			location.href = this.dataset.uri;
		};
	});
});
</script>
</html>

 

질문 서비스와 컨트롤러 수정하기

/question/QuestionService.java

...
public void delete(Question question) {
		this.questionRepostiory.delete(question);
	}

 

컨트롤러에서는 삭제 버튼을 클릭했을 때 @{|/question/delete/${question.id}|} URL을 처리하기 위해 메서드를 추가하자.

/question/QuestionController.java

@PreAuthorize("isAuthenticated()")
	@GetMapping("/delete/{id}")
	public String questionDelete(Principal principal, @PathVariable("id")Integer id) {
		Question question = this.questionService.getQuestion(id);
		if (!question.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제 권한이 없습니다.");
		}
		this.questionService.delete(question);
		return "redirect:/";
	}

 

삭제 기능 확인하기

서버를 재시작한 후 로그인해보자.

로그인하면 자신의 글에서 삭제 버튼이 나타남.
삭제 버튼을 클릭하면 확인 메시지가 뜸.
확인을 누르면 질문이 삭제되고 메인으로 이동.

728x90
반응형
LIST
728x90
반응형
SMALL

수정 및 삭제 기능 추가

수정 일시 추가하기

게시판의 질문과 답변을 수정하거나 삭제 기능을 추가하기 전에 언제 수정되었는지 확인할 수 있도록 속성을 추가하자.

/question/Question.java

package com.mysite.sbb.question;

import java.time.LocalDateTime;
import java.util.List;

import com.mysite.sbb.answer.Answer;
import com.mysite.sbb.user.SiteUser;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
public class Question {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	@Column(length = 200)
	private String subject;
	
	@Column(columnDefinition = "TEXT")
	private String content;
	
	private LocalDateTime createDate;
	
	//관계주입
	@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
	private List<Answer> answerList;
	
	//관계주입
	@ManyToOne
	private SiteUser author;
	
	private LocalDateTime modifyDate;
}

 

/answer/Answer.java

package com.mysite.sbb.answer;

import java.time.LocalDateTime;

import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
public class Answer {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	@Column(columnDefinition = "TEXT")
	private String content;
	
	private LocalDateTime createDate;
	
	//관계주입
	@ManyToOne
	private Question question;
	
	//관계주입
	@ManyToOne
	private SiteUser author;
	
	private LocalDateTime modifyDate;
}
Question과 Answer모두 동일하게 수정일시를 추가한 후, 다시 H2콘솔에 접속해보면 열이 추가된 것을 확인할 수 있다.

 

질문 수정 기능 생성하기

질문 수정 버튼 만들기

/templates/question_detail.html

...
<!--수정 버튼-->
<div class="my-3">
	<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
	sec:authorize="isAuthenticated()"
	th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
	th:text="수정"></a>
</div>
<!--수정 버튼 끝-->
...

수정 버튼이 로그인한 사용자가 글쓴이가 동일한 경우에만 보여야 하므로 th:if로 조건을 주었다.

 

질문 컨트롤러 수정하기(1)

위의 <a>태그를 통해 GET방식으로  @{|/question/modify/${question.id}|} url이 추가되었으므로 이 링크가 정상 작동하도록 수정해야 한다.

 /question/QuestionController.java

	@PreAuthorize("isAuthenticated()")
	@GetMapping("/modify/{id}")
	public String questionModify(QuestionForm questionForm, @PathVariable("id") Integer id, Principal principal){
		Question question = this.questionService.getQuestion(id);
		if (!question.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 없습니다.");
		}
		questionForm.setSubject(question.getSubject());
		questionForm.setContent(question.getContent());
		
		return "question_form";
	}

유효성 검사를 위해 QuestionForm을 선언해두었다.

먼저 해당하는 question객체를 가져오고

그에 해당하는 제목과 내용으로 QuestionForm을 세팅해준다. 이 내용들을 가지고 qeustion_form화면으로 이동한다.

 

질문 등록 템플릿 수정하기

질문을 등록하는 화면이나 수정하는 화면은 내용 기입하는 곳이 동일하므로 같은 템플릿을 사용한다.

/templates/question_form.html

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
	<h5 class="my-3 border-bottom pb-2">질문등록</h5>
	<form th:object="${questionForm}" method="post">
		<input type="hidden" th:name="${_csrf.parameterName}"
		th:value="${_csrf.token}">
		<div th:replace="~{form_errors :: formErrorsFragment}"></div>
		<div class="mb-3">
			<label for="subject" class="form-label">제목</label>
			<input type="text" th:field="*{subject}" class="form-control">		
		</div>
		<div class="mb-3">
			<label for="content" class="form-label">내용</label>
			<textarea th:field="*{content}" class="form-control" rows="10"></textarea>
		</div>
		<input type="submit" value="저장하기" class="btn btn-primary my-2">
	</form>
</div>
</html>

 

먼저 기존에 있던 form태그의 action속성을 삭제한다. 그러면 CSRF값이 자동생성되지 않아 input태그의 hidden타입으로 직접 추가해주어야 한다. 그러지 않으면 접근 거부가 나타난다.

여기서 action속성없이 submit하게되면 자동으로 여기 화면으로 접근하게된 경로를 갖고 있으므로

/question/cretae URL로 GET방식으로 접근했다면 submit할 시 자동으로 POST방식으로 /question/cretae가 action속성으로 잡힌다. 수정시에는 /question/modify/2와 같은 URL로 접근했기 때문에 POST방식으로 자동으로 /question/modify/2로 설정된다.

 

질문 서비스 수정하기

/qeustion/QuestionService.java

public void modify(Question question, String subject, String content) {
		question.setSubject(subject);
		question.setContent(content);
		question.setModifyDate(LocalDateTime.now());
		this.questionRepostiory.save(question);
	}

수정된 질문이 처리되도록 서비스를 수정했다.

 

질문 컨트롤러 수정하기(2)

내용 변경 후 저장 버튼을 클릭하게 되면 action이 /question/modify/{id}로 자동 생성되고 POST방식으로 전달할 것이다.

이를 처리하기 위해 컨트롤러를 수정하자.

/question/QuestionController.java

@PreAuthorize("isAuthenticated()")
	@PostMapping("/modify/{id}")
	public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult,
			Principal principal, @PathVariable("id") Integer id) {
		if (bindingResult.hasErrors()) {
			return "question_form";
		}
		Question question = this.questionService.getQuestion(id);
		if (!question.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 없습니다.");
		}
		this.questionService.modify(question, questionForm.getSubject(), questionForm.getContent());
		return String.format("redirect:/question/detail/%s", id);
	}

유효성 검사를 통해 만약 오류가 있다면 다시 question_form 으로 돌아가고

작성자와 동일한지도 검사한다.

이상이 없다면 위의 Service에서 만들었던 modify메서드를 호출하여 question객체와 수정된 제목과 내용을 넘겨 수정하게 한다.

이후에는 상세화면으로 돌아가게끔 했다.

 

수정 기능 확인하기

로그인 했을 경우 내가 작성한 글에서만 수정 버튼 활성화.
내용 변경 후 저장하기 버튼 클릭
내용이 변경된 것을 확인

728x90
반응형
LIST
728x90
반응형
SMALL

질문이나 답변을 작성할 때 사용자 정보도 DB에 같이 저장해보자.

엔티티에 속성 추가하기

질문 엔티티에 속성 추가하기

먼저 기존의 질문과 답변 엔티티에 글쓴이에 해당하는 속성을 추가하자.

/question/Question.java

...
//관계주입
@ManyToOne
private SiteUser author;
...

ManyToOne으로 관계주입을 했는데 이는 사용하 한 명이 여러 질문을 생성할 수 있다는 의미이다.

 

답변 엔티티에 속성 추가하기

위와 같은 방법으로 답변에도 속성을 추가하자.

/answer/Answer.java

...
//관계주입
@ManyToOne
private SiteUser author;
...

 

테이블 확인하기

서버를 재접속하고 H2 서버에 접속하여 QUESTION과 ANSWER 테이블을 확인해보자.

 

질문과 답변 테이블에 작성자가 추가되었다.
author_id의 기본값들이 null로 들어감.

 

author_id의 기본값들이 null로 들어감.

 

글쓴이 저장하기

이제 각 엔티티에 속성이 추가됐으므로 질문이나 답변 데이터를 저장할 때 글쓴이도 함께 저장할 수 있다.

그러기 위해서 컨트롤러와 서비스를 수정해야한다.

 

답변 컨트롤러와 서비스 업데이트하기

/answer/AnswerController.java

package com.mysite.sbb.answer;

import java.security.Principal;

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 com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
	private final QuestionService questionService;
	private final AnswerService answerService;
	
	@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);
		
		if (bindingResult.hasErrors()) {
			model.addAttribute("question",question);
			return "question_detail";
		}
		this.answerService.create(question, answerForm.getContent());
		
		return String.format("redirect:/question/detail/%s", id);
	}
}

createAnswer 메서드의 매개변수에 시큐리티가 제공하는 Principal principal을 추가했다. 

이 객체는 현재 로그인한 사용자의 정보를 제공한다.

principal.getName()을 호출하면 로그인한 사용자의 사용자명(ID)를 알 수 있다.

 

principal 객체를 사용하면 사용자명을 알 수 있으므로 그 사용자명으로 SiteUser객체를 조회할 수 있다.

UserService.java

....
	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메서드를 통해서 쉽게 사용자를 찾을 수 있다.

만약 없을 경우 예외를 발생시킨다.

 

답변 내용을 저장할 때 글쓴이도 저장할 수 있다.

/answer/AnswerService.java

package com.mysite.sbb.answer;

import java.time.LocalDateTime;
import org.springframework.stereotype.Service;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class AnswerService {
	private final AnswerRepository answerRepository;
	
	public void create(Question question, String content, SiteUser author) {
		Answer answer = new Answer();
		answer.setContent(content);
		answer.setCreateDate(LocalDateTime.now());
		answer.setQuestion(question);
		answer.setAuthor(author);
		this.answerRepository.save(answer);
	}
}

매개변수로 SiteUser 객체를 받고 answer.setAuthor() 메서드로 함께 저장하도록 수정했다.

 

다시 컨트롤러로 돌아가서 메서드를 완성하자.

/answer/AnswerController.java

package com.mysite.sbb.answer;

import java.security.Principal;

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 com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;

import groovyjarjarantlr4.v4.parse.ANTLRParser.finallyClause_return;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
	private final QuestionService questionService;
	private final AnswerService answerService;
	private final UserService userService;
	
	@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);
	}
}

먼저 userService를 사용하기 위해 상단에 선언을 했다.

createAnswer메서드에서 매개변수에 선언해놓은 Principle객체를 이용해서 SiteUser를 선언했고,

답변을 생성하기 위한 this.answerService.create메서드에 siteUser 객체를 추가하여 답변을 등록하도록 했다.

 

질문 컨트롤러와 서비스 업데이트 하기

앞서 답변처럼 똑같이 하면된다.

먼저 글쓴이를 저장하기 위해 QuestionService를 수정한다.

/question/QuestionService.java

...
public void create(String subject, String content, SiteUser siteUser) {
		Question q = new Question();
		q.setSubject(subject);
		q.setContent(content);
		q.setCreateDate(LocalDateTime.now());
		q.setAuthor(siteUser);
		this.questionRepostiory.save(q);
	}
...

create메서드의 수정이 필요하다.

매개변수에 SiteUser객체를 선언하고 q.setAuthor()를 통해 질문에 글쓴이도 포함하여 데이터를 생성할 수 있도록 했다.

 

/qeustion/QuestionController.java

...
private final UserService userService;
...
	@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";
	}
...

글쓴이를 생성하기 위해 상단에 userService를 추가했고

questionCreate메서드의 매개변수에 Principle객체를 선언했다.

그리고 SiteUser객체를 생성 후 this.questionService.create()메서드의 매개변수에 siteUser를 추가했다.

 

로컬 서버를 재시작 후 로그인 한 다음 질문과 답변을 등록해보자.

테스트 코드에 오류가 있으므로 수정한다.

SbbApplicationTests.java

@Test
	void testJpa() {
		for(int i=1; i<=300; i++) {
			String subject = String.format("테스트 데이터입니다:[%03d]", i);
			String content = "내용 없음";
			this.questionService.create(subject, content, null);
		}
	}

create메서드의 매개변수에 사용자를 넣어야하므로 그냥 null로 채워준다.

 

로그인 페이지로 이동시키기

로그아웃 상태에서 질문이나 답변을 등록하려고 하면 에러가 난다.

principal 객체가 널이라서 500에러 발생.

 

따라서 create메서드는 로그인한 상태에서만 호출할 수 있어야한다.

만약 로그아웃 상태에서 create메서드를 호출하면 로그인 페이지로 강제 이동하게끔 해야한다.

/question/QuestionController.java

...
	@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()")어노테이션을 추가했다.

 

똑같이 답변컨트롤러도 수정하자.

/answer/AnswerController.java

...
	@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.java

...
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
...

 

이렇게 모두 수정 후 로그아웃 상태에서 질문이나 답변을 등록하면 자동으로 로그인 화면으로 이동한다.

 

답변 작성 막아두기

현재 로그인하지 않은 상태에서 '질문 등록하기' 버튼을 클릭하면 바로 로그인 페이지로 이동하지만,

답변은 답변을 작성한 후 '답변등록'버튼을 클릭해야 로그인 페이지로 이동해버리는 바람에, 애써 작성한 내용이 모두 사라지는 상황이 발생한다.

따라서 로그아웃 상태일 때는 아예 답변 작성을 하지 못하도록 해야한다.

 

/templates/question_detail.html

...
	<!-- 답변 작성 -->
	<form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">
		<div th:replace="~{form_errors :: formErrorsFragment}"></div>
		
		<textarea sec:authorize="isAnonymous()" disabled
			th:field="*{content}" rows="10" class="form-control"></textarea>
		<textarea sec:authorize="isAuthenticated()"
			th:field="*{content}" rows="10" class="form-control"></textarea>
			
		<input type="submit" value="답변등록" class="btn btn-primary my-2">
	</form>
	<!-- 답변 작성 끝-->
...

위의 2개의 textarea 코드를 보면 사용자의 로그인 상태에 따라 입력 유무를 판단하게 했다.

 

이처럼 로그인 하지 않은 경우에는 textarea가 비활성화된다.

 

화면에 글쓴이 나타내기

질문 목록에 글쓴이 표시하기

/templates/question_list.html

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
	<thead class="table-dark">
		<tr class="text-center">
			<th>번호</th>
			<th style="width: 50%;">제목</th>
			<th>글쓴이</th>
			<th>작성일시</th>
		</tr>
	</thead>
	<tbody>
		<tr class="text-center" th:each="question, loop : ${paging}">
			<td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}"></td>
			<td class="text-start">
				<a th:href="@{|/question/detail/${question.id}|}"
				th:text="${question.subject}"></a>
				<span class="text-danger small ms-2"
					th:if="${#lists.size(question.answerList) > 0 }"
					th:text="${#lists.size(question.answerList)}"></span>
			</td>
			<td>
				<span th:if="${question.author != null}"
				th:text="${question.author.username}"></span>
			</td>
			
			<td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td>
		</tr>
	</tbody>
</table>

<!-- 페이징 처리 시작 -->
<div th:if="${!paging.isEmpty()}">
	<ul class="pagination justify-content-center">
		<li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
			<a class="page-link" th:href="@{|?page=${paging.number-1}|}">
				<span>이전</span>
			</a>
		</li>
		<li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
		th:if="${page >= paging.number-5 and page <= paging.number+5}"
		th:classappend="${page == paging.number}? 'active'" class="page-item">
			<a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
		</li>
		<li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
			<a class="page-link" th:href="@{|?page=${paging.number+1}|}">
				<span>다음</span>
			</a>
		</li>
	</ul>
</div>
<!-- 페이징 처리 끝 -->

<a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>

테이블에 글쓴이를 추가했다.

 

질문에 글쓴이를 추가한 상태

 

질문 상세에 글쓴이 표시하기

/templates/question_detail.html

...
<!-- 질문 -->
	<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 class="mb-2">
						<span th:if="${question.author != null}"
						th:text="${question.author.username}">
						</span>
					</div>
					<div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
				</div>
			</div>
		</div>
	</div>
	<!-- 질문 끝-->
...

작성일자 바로 위에 사용자 id가 나타나도록 했다.

 

답변 부분에도 글쓴이를 추가해보자.

...
	<!-- 답변 반복 시작 -->
	<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 class="mb-2">
						<span th:if="${answer.author != null}"
						th:text="${answer.author.username}">
						</span>
					</div>
					<div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
				</div>
			</div>
		</div>
	</div>
	<!-- 답변 반복 끝  -->
...

위와 똑같이 답변의 글쓴이를 추가했다.

완성된 화면.

728x90
반응형
LIST

+ Recent posts