gwooden_코린이

스프링 부트 추천기능 구현해보기 본문

스프링 부트

스프링 부트 추천기능 구현해보기

gwooden22 2023. 2. 17. 14:31
728x90
/* 추천 기능 구현 */
	@GetMapping("/vote/{id}")
	public String questionVote(@PathVariable("id") Integer id, Principal principal) {
		
		//id에 해당하는 레코드에 voter컬럼부분에다가 추천자 정보를 넣어줌
		
		Question question = questionService.getQuestion(id);
		SiteUser siteUser = userService.getUser(principal.getName());
		
		//question서비스에서 메서드 불러오기
		questionService.vote(question, siteUser);
		
		
		return "redirect:/question/detail/" + id;
	}

question : 컨트롤

 

//추천 기능 메서드
	public void vote(Question question, SiteUser siteUser) {
		
		//question에 voter가져와서 siteuser 정보고 같이 추가로 넣어줌
		question.getVoter().add(siteUser);
		
		questionRepository.save(question);
	}

question : 서비스


Question question = questionService.getQuestion(id);

새로운 question 객체에

질문코드에 해당하는 데이터를 넣어줌

 

기존 데이터들이 question에 저장

 

 

SiteUser siteUser = userService.getUser(principal.getName());

추천인만 추가

question.getVoter().add(d라는 사람의 정보)

 

questionService.vote(question, siteUser);

save(question)


 

@ManyToMany //한 사람이 여러 게시글에 투표를 할 수 있다보니 ManyToMan를 사용
	Set<SiteUser> voter; //set 컬렉션으로 중복처리, 어떤 유저가 투표했는지 SiteUser 정보 가져오기

Question : VO

 

<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>

추천 기능 : view


package com.example.sb.answer;

import java.time.LocalDateTime;
import java.util.Set;

import com.example.sb.question.Question;
import com.example.sb.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.ManyToMany;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
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; //수정날짜
	
	@ManyToMany //한 사람이 여러 게시글에 투표를 할 수 있다보니 ManyToMan를 사용
	Set<SiteUser> voter; //set 컬렉션으로 중복처리, 어떤 유저가 투표했는지 SiteUser 정보 가져오기
}​
<html 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 th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start">
						<div>수정일자</div>
						<div th:text="${#temporals.format(question.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
					</div>
					
	                <div class="badge bg-light text-dark p-2 text-start">
						<div th:if="${question.author != null}" th:text="${question.author.username}"></div>
	                    <div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
	                </div>
	            </div>
	            
	            <!--질문 수정/삭제 버튼-->
	            <!--#lists.size(vo) : 자동으로 개수를 계산해줌-->
	            <div class="mb3">
					<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>
					
					<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-outline-secondary" sec:authorize="isAuthenticated()"
					th:if="${question.author != null and question.author.username == #authentication.getPrincipal().getUsername()}">
						수정
					</a>	
					
					<a href="javascript:void(0);" th:data-uri="@{|/question/delete/${question.id}|}"
					class="btn btn-outline-secondary delete" sec:authorize="isAuthenticated()"
					th:if="${question.author != null and question.author.username == #authentication.getPrincipal().getUsername()}">
						삭제
					</a>		
				</div>
	            
	        </div>
	    </div>
	    
	    <!-- 답변의 갯수 표시 -->
	    <h5 class="border-bottom my-3 py-2" 
	        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
	        
	    <!-- 답변 반복 시작 -->
	    <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 th:if="${answer.modifyDate != null}" class="badge bg-light text-dark p-2 text-start">
						<div>수정일자</div>
						<div th:text="${#temporals.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
					</div>
					
	                <div class="badge bg-light text-dark p-2 text-start">
						<div th:if="${answer.author != null}" th:text="${answer.author.username}"></div>
	                    <div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd hh:mm')}"></div>
	                </div>
	            </div>
	            
	            <!--답변 수정/삭제 버튼-->
	            <div class="mb3">
					<a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-outline-secondary" sec:authorize="isAuthenticated()"
					th:if="${answer.author != null and answer.author.username == #authentication.getPrincipal().getUsername()}">
						수정
					</a>	
					
					<a href="javascript:void(0);" th:data-uri="@{|/answer/delete/${answer.id}|}"
					class="btn btn-outline-secondary delete" sec:authorize="isAuthenticated()"
					th:if="${answer.author != null and answer.author.username == #authentication.getPrincipal().getUsername()}">
						삭제
					</a>		
				</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>
	        <textarea name="content" id="content" rows="10" class="form-control"></textarea>
	        
	        <input type="submit" value="답변등록" class="btn btn-primary my-2">
	    </form>
	</div>

</html>
const btns = document.querySelectorAll('.delete');

//delete 클래스가 여러개 있을 경우 반복문을 사용해줘야 버튼들이 기능을 한다.
btns.forEach(function(btn) {
	
		btn.addEventListener('click', function() {
		
			if(confirm('정말로 삭제하시겠습니까?')) {
				location.href = this.dataset.uri;
			}
		
		});
	
});


const recommendBtns = document.querySelectorAll('.recommend');

recommendBtns.forEach(function(btn) {
	
	btn.addEventListener('click', function() {
		
		if(confirm('추천하시겠습니까?')) {
			location.href = this.dataset.uri;
		}
		
	});
	
});
package com.example.sb.user;

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

@Entity
@Setter
@Getter
public class SiteUser {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@Column(unique = true)
	private String username;
	private String password;
	private String email;

}

더보기
package com.example.sb.answer;

import java.time.LocalDateTime;
import java.util.Set;

import com.example.sb.question.Question;
import com.example.sb.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.ManyToMany;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
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; //수정날짜
	
	@ManyToMany //한 사람이 여러 게시글에 투표를 할 수 있다보니 ManyToMan를 사용
	Set<SiteUser> voter; //set 컬렉션으로 중복처리, 어떤 유저가 투표했는지 SiteUser 정보 가져오기
}

package com.example.sb.question;

import java.security.Principal;

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.GetMapping;
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.example.sb.answer.AnswerForm;
import com.example.sb.user.SiteUser;
import com.example.sb.user.UserService;

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

@RequestMapping("/question")//중복 되는 주소를 리퀘스맵핑으로 처리
@RequiredArgsConstructor
@Controller
public class QuestionController {
	
	private final QuestionService questionService;
	private final UserService userService;
	
	//question테이블에 전체레코드를 출력해주는 메서드
//	@GetMapping("/list")
//	public String list(Model model) { //model은 자바스크립트로 따지면 이벤트같은 기능, request역할을 해줌
//		//질문 목록을 뽑아내는 코드
//		List<Question> questionList = questionService.getList();
//		model.addAttribute("questionList", questionList);
//		
//		return "question_list";
//	}
	
	/* 등록한 질문 리스트 페이지로 이동 */
	@GetMapping("/list")
	public String list(Model model, @RequestParam(value="page", defaultValue = "0") int page) {
		
		Page<Question> paging = questionService.getList(page);
		
		model.addAttribute("paging", paging);
		
//		return "test";
		return "question_list";
	}
	
	
	/* 질문 상세 보기 */
	@GetMapping(value = "/detail/{id}")
	public String detail(Model model, @PathVariable("id") Integer id, AnswerForm answerForm) {
		
		//id에 해당하는 레코드를 가져와야 함
		Question q = questionService.getQuestion(id);
		//모델에 추가
		model.addAttribute("question", q);
		
		return "question_detail";
	}
	
	
	/* 질문 등록 페이지 이동 */
	@PreAuthorize("isAuthenticated()") //메서드가 호출되기전에 권한이 있는지 검사를 먼저한 후 그다음 진행
	@GetMapping("/create")
	public String questionCreate(QuestionForm questionForm) {
		return "question_form";
	}
	
	
	/* 질문 등록 */
	@PostMapping("/create")
	 public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal) {
		
		SiteUser siteUser = userService.getUser(principal.getName());
	      
	    if(bindingResult.hasErrors()) {
	       return "question_form";
	    }
	      
	    //입력한 질문제목과 내용을 Question 테이블에 추가 하는 코드
	    questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
	      
	    return"redirect:/question/list";
	 }
	
	
	/* 수정 페이지로 이동 */
	@GetMapping("/modify/{id}")
	public String questionModify(QuestionForm questionForm, @PathVariable("id") Integer id) {
		
		Question question = questionService.getQuestion(id);
		
		questionForm.setSubject(question.getSubject());
		questionForm.setContent(question.getContent());
		
		return "question_form";
	}
	
	
	/* 수정 기능 */
	@PostMapping("/modify/{id}")
	public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult
			, @PathVariable("id") Integer id) {
		
		Question question = questionService.getQuestion(id); //원본코드
		
		if(bindingResult.hasErrors()) {
			return "question_form";
		}
		
		//원본에서 제목과 내용만 수정해서 테이블에 update
		questionService.modify(question, questionForm.getSubject(), questionForm.getContent());
		
		return "redirect:/question/detail/" + id;
	}
	
	
	
	/* 삭제 기능 구현 중*/
	@GetMapping("/delete/{id}")
	public String questionDelete(@PathVariable("id") Integer id ) {
		
		Question question = questionService.getQuestion(id);
		
		questionService.delete(question);
		
//		questionService.delete(id); QuestionService에서 deleteByid를 이용했을 경우 이 코드만으로도 가능하다
		
		return "redirect:/";
	}
	
	
	/* 추천 기능 구현 */
	@GetMapping("/vote/{id}")
	public String questionVote(@PathVariable("id") Integer id, Principal principal) {
		
		//id에 해당하는 레코드에 voter컬럼부분에다가 추천자 정보를 넣어줌
		
		Question question = questionService.getQuestion(id);
		SiteUser siteUser = userService.getUser(principal.getName());
		
		//question서비스에서 메서드 불러오기
		questionService.vote(question, siteUser);
		
		
		return "redirect:/question/detail/" + id;
	}

}
package com.example.sb.question;

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

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 com.example.sb.DataNotFoundException;
import com.example.sb.user.SiteUser;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class QuestionService {
	
	private final QuestionRepository questionRepository;
	
	// question테이블에 모든 레코드를 가져오는 메서드
//	public List<Question> getList() {
//		return questionRepository.findAll();
//	}
	
	//question테이블에 레코드를 페이지별로 가져오는 메서드
	public Page<Question> getList(int page) {
		
		List<Sort.Order> sort = new ArrayList<>();
		
		//내림차순
		sort.add(Sort.Order.desc("createDate"));
		
		Pageable pageable = PageRequest.of(page, 10, Sort.by(sort));
		
		return questionRepository.findAll(pageable);
	}
	
	//question테이블에서 전달받은 id에 해당하는 레코드를 가져오는 메서드
	public Question getQuestion(Integer id) {
		
		Optional<Question> q = questionRepository.findById(id);
		
		if(q.isPresent()) {
			return q.get();
		} else {
			// 검색도니 결과가 없을때 처리할 코드 - 에러페이지
			throw new DataNotFoundException("해당 질문이 존재하지 않습니다.");
		}
	}
	
	//질문 추가 메서드
	public void create(String subject, String content, SiteUser author) {
		Question quest = new Question();
		
		quest.setSubject(subject);
		quest.setContent(content);
		quest.setCreateDate(LocalDateTime.now());
		quest.setAuthor(author);
		
		
		questionRepository.save(quest);
	}
	
	//수정 기능 메서드
	//question 원본 레코드, subject 수정할 제목, content 수정할 내용
	public void modify(Question question, String subject, String content) {
		
		question.setSubject(subject);
		question.setContent(content);
		question.setModifyDate(LocalDateTime.now());
		
		questionRepository.save(question);
		
	}
	
	
	//삭제 메서드 (기능 구현중)
	public void delete(Question question) {
		//자바스크리브에서 요청을 날려서 직접 찾아서 삭제 처리
		questionRepository.delete(question);
		
	}
	
	/*
	 * id값을 jpa를 통해 알아서 찾게 하고 그 다음 delete를 하기 때문에 위 로직과 비교 했을때 아래 로직이 좀 더 느리다. 
	 * 단, 컨트롤러에서 코드가 최소화가 된다.
	 */
//	public void delete(Integer id) {
//		questionRepository.deleteById(id);
//	}
	
	
	//추천 기능 메서드
	public void vote(Question question, SiteUser siteUser) {
		
		//question에 voter가져와서 siteuser 정보고 같이 추가로 넣어줌
		question.getVoter().add(siteUser);
		
		questionRepository.save(question);
	}

}
728x90
Comments