728x90
반응형
SMALL

뷰 컨트롤러(View Controller)

뷰 컨트롤러는 뷰를 감싸고 있는 전체 부분이라고 생각하면 되는데 기본으로 설정되어 있는 크기는

사용자마다 다를 수 있다.

현재 뷰 컨트롤러의 크기는 이와 같다.
하단 부분을 클릭하여 다른 크기로도 변경이 가능하다.

 

오토 레이아웃 기본

라벨 추가하기

+ 버튼을 클릭하여 라벨 추가
추가된 라벨의 text를 'Learn to Code'로 변경.

 

Constraints 추가하기

왼쪽의 Constraints는 이전의 Hello World 라벨을 가로 세로 중앙으로 맞추기 위해 추가했던 것이다.
Learn to Code 라벨을 클릭 후 오른쪽 아래의 가운데 아이콘을 눌러 constraints를 추가할 수도 있다.

여기서 'Spacing to nearest neighbor'라고 쓰여 있는데
선택된 라벨과 가장 가까운 요소와의 거리를 나타내는 것임을 알 수 있다.
위로는 Hello World라벨과 가장 가깝기 때문에 이와의 거리는 142.33픽셀
오른쪽으로 화면 테두리와는 123 픽셀
왼쪽으로 화면 테두리와 162픽셀
아래 화면 테두리와 218픽셀 떨어져 있음을 나타낸다.

상하좌우 중 원하는 것을 선택하여 constraints를 추가한다.
이를 추가하면 빌드했을 때 어떤 디바이스에서도 그만큼 떨어트려진 상태도 보이게 할 것이다. 라는 의미

[Constrain to margins]는 여백을 적용할 것인지? 라는 뜻

중간의 width와 height은 라벨 자체의 크기를 말한다. 
여기서 체크하여 constraints를 추가할 경우
어떤 디바이스에서 빌드하든 그 크기를 고정시키겠다는 의미이다. 

Add 4 Constraints를 클릭하여 추가해보자.

Constraints가 추가된 모습. Learn to Code라벨에 선 4개가 추가됐다.

Build하여 확인해보자.

iPhone 15 Pro의 모습

 

Constraints 수정하기

라벨을 이리 저리 움직여보자.
그럼 파란색이였던 선이 주황색으로 바뀐 것을 볼 수 잇다.
이 색은 문제가 있다는 의미이므로 수정이 필요하다는 것이다.

여기서 노란색 화살표를 눌러보면 어디서 문제가 나타나는지 볼 수 있다.

문제의 노란 삼각형을 클릭해보면 현재 이슈를 수정하는 방법 4개의 메뉴얼을 알려준다.
- Update frames: constraints에 맞게 위치를 조정해라. => 원래 자리로 돌아감.
- Update constraints: 현재 옮겨진 포지션에 맞게 constraints를 변경.
- Reset to suggested constraints: 현재에 맞게 xcode가 적절한 constraints를 제공.

Update frames를 체크하고 [Fix Misplacement]를 눌러 이전으로 다시 조정해보고
또 다시 움직여서 Update constraints도 눌러보자.

라벨을 움직이고 constraints를 상하좌우를 체크하고 추가해보자.

그럼 이와 같이 빨간색 표시가 뜨는데
위의 주황색 표시는 권고사항으로 수정하면 좋겠다~ 라는 의미
빨간색은 중복되는 constraints가 있어 어디에 맞출지 모르니 수정해라! 라는 의미이다.

둘중에 하나를 제거해야 하므로
원하는 constraints를 빼고 체크하여 Delete Constraints를 클릭한다.
728x90
반응형
LIST
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

+ Recent posts