728x90
반응형
SMALL
만약 질문을 등록할 때 질문 내요을 비어 있는 값으로도 등록할 수 있다.
하지만 이건 옳지 않는 방법이므로 아무것도 입력하지 않은 상태에서 질문이 등록될 수 없도록 해야한다.
따라서 폼 클래스를 사용하여 입력값을 체크하는 방법을 이요해보자.

 

Spring Boot Validation 라이브러리 설치하기

build.gradle

...
//SpringBoot Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
...
항목 설명
@Size 문자 길이 제한
@NotNull Null을 허용하지 않음
@NotEmpty Null 또는 빈 문자열("")을 허용하지 않음
@Past 과거 날짜만 입력 가능
@Future 미래 날짜만 입력 가능
@FutureOrPresent 미래 또는 오늘 날짜만 입력 가능
@Max 최댓값 이하의 값만 입력할 수 있도록 제한
@Min 최솟값 이상의 값만 입력할 수 있도록 제한
@Pattern 입력값을 정규식 패턴으로 검증

 

폼 클래스 만들기

질문 등록 페이지에서 사용자로부터 입력받은 값을 검증하는데 필요한 폼 클래스를 만들어보자.

/question/QuestionForm.java

package com.mysite.sbb.question;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class QuestionForm {
	@NotEmpty(message="제목은 필수 항목입니다.")
	@Size(max=200)
	private String subject;
	
	@NotEmpty(message="내용은 필수 항목입니다.")
	private String content;
}

 

컨트롤러에 전송하기

/question/QuestionController.java

...
@PostMapping("/create")
	public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult){
		if (bindingResult.hasErrors()) {
			return "question_form";
		}
		this.questionService.create(questionForm.getSubject(), questionForm.getContent());
		return "redirect:/question/list";
}
...

 

이 메서드의 매개변수를 subject, content 대신 QuestionForm 객체로 변경했다. 

이미 class QuestionForm에는 subject, content  항목을 포함하고 있기때문에 자동으로 바인딩 된다.

앞의 @Valid 어노테이션은 검증 기능이 동작한다.

bindingResult는 검증 결과를 의미한다. 

 

템플릿 수정하기

검증에 실패했다는 오류 메시지를 보여주기 위해 템플릿을 수정한다.

/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:action="@{/question/create}" th:object="${questionForm}" method="post">
		<div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}"> 
			<div th:each="err: ${#fields.allErrors()}" th:text="${err}"></div>
		</div>
	
		<div class="mb-3">
			<label for="subject" class="form-label">제목</label>
			<input type="text" name="subject" id="subject" class="form-control">		
		</div>
		<div class="mb-3">
			<label for="content" class="form-label">내용</label>
			<textarea name="content" id="content" class="form-control" rows="10"></textarea>
		</div>
		<input type="submit" value="저장하기" class="btn btn-primary my-2">
	</form>
</div>
</html>

form의 속성 th:object는 <form>의 입력 항목들이 QuestionForm과 연결된다는 점을 타임리프에 알려준다.

그런데 여기 th:object 속성을 추가했으므로 controller에서 getMapping으로 매핑한 메서드에서도 QuestionForm 객체를 넘겨주어야 한다.

 

/question/QuestionController.java

@GetMapping("/create")
	public String questionCreate(QuestionForm questionForm) {
		return "question_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:action="@{/question/create}" th:object="${questionForm}" method="post">
		<div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}"> 
			<div th:each="err: ${#fields.allErrors()}" th:text="${err}"></div>
		</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>

입력값을 담는 input태그와 textarea에 name속성 대신 th:field로 수정했다. 

그럼 태그의 id, name, value 속성이 모두 자동으로 생성되고 타임리프가 value 속성에 기존에 입력된 값을 채워 넣어준다.

 

그럼 제목만 입력 후 저장하기 버튼을 클릭하면 기존 값이 유지된다.

 

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

질문 등록 버튼 및 화면 생성

/templates/question_list.html

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

 

localhost:8080/question/list

질문 등록하기 버튼 생성.

 

URL 매핑하기

/question/create에 해당하는 URL 매핑 추가.

/question/QuestionController.java

...
@GetMapping("/create")
	public String questionCreate() {
		return "question_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:action="@{/question/create}" method="post">
		<div class="mb-3">
			<label for="subject" class="form-label">제목</label>
			<input type="text" name="subject" id="subject" class="form-control">		
		</div>
		<div class="mb-3">
			<label for="content" class="form-label">내용</label>
			<textarea name="content" id="content" class="form-control" rows="10"></textarea>
		</div>
		<input type="submit" value="저장하기" class="btn btn-primary my-2">
	</form>
</div>
</html>

질문 내용은 글자 수 제한이 없는 textarea 사용.

 

질문 저장 POST 요청 처리 

질문등록 화면에서 저장하기 버튼을 클릭하면 form의 action url로 이동하며 POST방식으로 전송된다. 또한 제목과 내용의 값이 담기는 곳에 name 속성을 지정해놨기 때문에 그 변수에 담아 값을 전달한다.

/question/QuestionController.java

@PostMapping("/create")
public String questionCreate(@RequestParam(value="subject") String subject, 
	@RequestParam(value="content") String content) {
	//TODO: 질문 저장.
	return "redirect:/question/list";
}

 

=>저장하기 버튼을 클릭하면 question/list로 돌아가는지 확인.

 

서비스 수정하기

질문 데이터를 저장하기 위해 서비스를 수정해보자.

/qeustion/QuestionService.java

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

 

다음으로 이 생성한 서비스를 사용할 수 있도록 컨트롤러도 수정해보자.

/question/QuestionController.java

@PostMapping("/create")
	public String questionCreate(@RequestParam(value="subject") String subject, @RequestParam(value="content") String content) {
		this.questionService.create(subject, content);
		return "redirect:/question/list";
}

 

테스트를 해보면

질문의 제목과 내용을 입력하고 저장하기 버튼을 클릭한다.
question/list로 다시 이동하고 방금 등록한 질문이 등록된 것을 확인할 수 있다.

 

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

템플릿 상속하기

각 템플릿의 반복되는 내용을 담아 기본 틀이 되는 템플릿을 만든다.
모든 템플릿은 이 기본 템플릿을 상속받아야 한다.

layout.html

<!DOCTYPE html>
<html lang="ko">
	<head>
		<!-- Required meta tags -->
		<meta charset="utf-8">
		<meta name ="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		
		<!-- Bootstrap CSS -->
		<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">

		<!-- sbb CSS -->
		<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
		<title>Hello, sbb!</title>
	</head>
	<body>
		<!-- 기본 템플릿 안에 삽입될 내용 Start -->
		<th:block layout:fragment="content"></th:block>
		<!-- 기본 템플릿 안에 삽입될 내용 End -->
	</body>
</html>

 

question_list.html

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
	<thead class="table-dark">
		<tr>
			<th>번호</th>
			<th>제목</th>
			<th>작성일시</th>
		</tr>
	</thead>
	<tbody>
		<tr th:each="question, loop : ${questionList}">
			<td th:text="${loop.count}"></td>
			<td>
				<a th:href="@{|/question/detail/${question.id}|}"
				th:text="${question.subject}"></a>
			</td>
			<td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td>
		</tr>
	</tbody>
</table>
</div>
</html>

 

question_detail.html

<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 class="badge bg-light text-dark p-2 text-start">
					<div th:text="${#temporals.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>
	<!-- 답변 반복 시작 -->
	<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="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
				</div>
			</div>
		</div>
	</div>
	<!-- 답변 반복 끝  -->
	<!-- 답변 작성 -->
	<form th:action="@{|/answer/create/${question.id}|}" method="post" class="my-3">
		<textarea name="content" id="content" rows="10" class="form-control"></textarea>
		<input type="submit" value="답변등록" class="btn btn-primary my-2">
	</form>
</div>
</html>
728x90
반응형
LIST
728x90
반응형
SMALL

부트스트랩 설치

https://getbootstrap.com/docs/5.3/getting-started/download/

bootstrap-5.3.3-dist.zip파일을 다운받았다.

 

압축을 풀면 css폴더에 bootstrap.min.css파일이 있는데 이 파일을 static폴더 아래에 복사하자.

 

질문 목록에 부트스트랩 적용하기

question_list.html

<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<div class="container my-3">
<table class="table">
	<thead class="table-dark">
		<tr>
			<th>번호</th>
			<th>제목</th>
			<th>작성일시</th>
		</tr>
	</thead>
	<tbody>
		<tr th:each="question, loop : ${questionList}">
			<td th:text="${loop.count}"></td>
			<td>
				<a th:href="@{|/question/detail/${question.id}|}"
				th:text="${question.subject}"></a>
			</td>
			<td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td>
		</tr>
	</tbody>
</table>
</div>

결과

질문 상세에 부트스트랩 적용하기

question_detail.html

<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<div 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="${#temporals.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>
	<!-- 답변 반복 시작 -->
	<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="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
				</div>
			</div>
		</div>
	</div>
	<!-- 답변 반복 끝  -->
	<!-- 답변 작성 -->
	<form th:action="@{|/answer/create/${question.id}|}" method="post" class="my-3">
		<textarea name="content" id="content" rows="10" class="form-control"></textarea>
		<input type="submit" value="답변등록" class="btn btn-primary my-2">
	</form>
</div>

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

스태틱 디렉터리와 스타일시트

스타일시트, 즉 css파일은 static 디렉터리에 저장해야 한다.

\

CSS 파일 만들기

static 폴더 우클릭 후 [New -> File] 클릭

style.css

textarea {
	width:100%;
}

input[type=submit]{
	margin-top:10px;
}

 

템플릿에 스타일 적용

이전에 만든 html에 위의 style.css를 적용해보자.

question_detail.html

<link rel="stylesheet" type="text/css" th:href="@{/style.css}">

<h1 th:text="${question.subject}">제목</h1>
<div th:text="${question.content}">내용</div>

<h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<div>
	<ul>
		<li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li>
	</ul>
</div>


<form th:action="@{|/answer/create/${question.id}|}" method="post">
	<textarea name="content" id="content" rows="15"></textarea>
	<input type="submit" value="답변 등록">
</form>

 

적용된 결과

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

답변 기능 만들기

답변 텍스트 창과 등록 버튼 만들기

question_detail.html

<h1 th:text="${question.subject}">제목</h1>
<div th:text="${question.content}">내용</div>

<form th:action="@{|/answer/create/${question.id}|}" method="post">
	<textarea name="content" id="content" rows="15"></textarea>
	<input type="submit" value="답변 등록">
</form>

 

답변 등록 컨트롤러 만들기

AnswerController.java

package com.mysite.sbb.answer;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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 lombok.RequiredArgsConstructor;

@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
	private final QuestionService questionService;
	
	@PostMapping("/create/{id}")
	public String createAnswer(Model model, @PathVariable("id") Integer id,
			@RequestParam(value = "content") String content) {
		Question question = this.questionService.getQuestion(id);
		// TODO : 답변을 저장한다.
		return String.format("redirect:/question/detail/%s", id);
	}
}

 

답변 서비스 만들기

AnswerService.java

package com.mysite.sbb.answer;

import java.time.LocalDateTime;
import org.springframework.stereotype.Service;
import com.mysite.sbb.question.Question;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class AnswerService {
	private final AnswerRepository answerRepository;
	
	public void create(Question question, String content) {
		Answer answer = new Answer();
		answer.setContent(content);
		answer.setCreateDate(LocalDateTime.now());
		answer.setQuestion(question);
		this.answerRepository.save(answer);
	}
}
이제 만든 AnswerService를 AnswerController에서 사용해보자.

AnswerService.java

package com.mysite.sbb.answer;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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 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,
			@RequestParam(value = "content") String content) {
		Question question = this.questionService.getQuestion(id);
		
		// TODO : 답변을 저장한다.
		this.answerService.create(question, content);
		
		return String.format("redirect:/question/detail/%s", id);
	}
}

 

답변 등록하기 결과

 

질문 상세페이지에 답변 표시하기

원래 있던 답변들을 질문 아래에 표시하기 위해 question_detail.html 을 수정해보자.

question_detail.html

<h1 th:text="${question.subject}">제목</h1>
<div th:text="${question.content}">내용</div>

<h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<div>
	<ul>
		<li th:each="answer : @{question.answerList}" th:text="${answer.content}"></li>
	</ul>
</div>


<form th:action="@{|/answer/create/${question.id}|}" method="post">
	<textarea name="content" id="content" rows="15"></textarea>
	<input type="submit" value="답변 등록">
</form>

 

결과

728x90
반응형
LIST
728x90
반응형
SMALL
@GetMapping("/question//list")
@GetMapping(value="/question/detail/{id}")

위의 url의 프리픽스가 /question으로 시작한다.

이렇게 중복될 때 해당 컨트롤러에 매핑되는 url은 항상 /question 으로 시작할 수 있도록 설정할 수 있다.

 

QuestionController.java

@RequestMapping("/question")
@RequiredArgsConstructor
@Controller
public class QuestionController {
	
	private final QuestionService questionService;
	
	@GetMapping("/list")
	public String list(Model model) {
		List<Question> questionList = this.questionService.getList();
		model.addAttribute("questionList",questionList);
		return "question_list";
	}
	
	@GetMapping(value = "/detail/{id}")
	public String detail(Model model, @PathVariable("id") Integer id) {
		Question question = this.questionService.getQuestion(id);
		model.addAttribute("question", question);
		return "question_detail";
	}
}
Controller에 @RequestMapping("/question") 어노테이션을 추가하고 아래의 매핑 url의 중복되는 부분을 삭제한다.
728x90
반응형
LIST
728x90
반응형
SMALL

상세 페이지 만들기

질문 목록에서 질문 제목을 클릭하면 상세페이지로 이동하게끔 하려고 한다.

 

질문 목록에 링크 추가

question_list.html

<table>
	<thead>
		<tr>
			<th>제목</th>
			<th>작성일시</th>
		</tr>
	</thead>
	<tbody>
		<tr th:each="question : ${questionList}">
			<td>
				<a th:href="@{|/question/detail/${question.id}|}"
				th:text="${question.subject}"></a>
			</td>
			<td th:text="${question.createDate}"></td>
		</tr>
	</tbody>
</table>

 

 

상세 페이지 컨트롤러 만들기

위의 링크를 클릭하면 url이 http://localhost:8080/question/detail/2 로 잘 이동한다. 하지만 이에 매핑된 url이 없어 404 오류가 뜬다.

 

QuestionController.java

package com.mysite.sbb.question;

import java.util.List;
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 lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class QuestionController {
	
	private final QuestionService questionService;
	
	@GetMapping("/question/list")
	public String list(Model model) {
		List<Question> questionList = this.questionService.getList();
		model.addAttribute("questionList",questionList);
		return "question_list";
	}
	
	@GetMapping(value = "/question/detail/{id}")
	public String detail(Model model, @PathVariable("id") Integer id) {
		return "question_detail";
	}
}

 

question_detail.html

<h1>제목</h1>
<div>내용</div>

이제 링크 매핑은 됐지만 해당 질문에 맞는 데이터를 가져와야 한다.

 

상세 페이지에 서비스 사용하기

 

QuestionService.java

package com.mysite.sbb.question;

import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class QuestionService {

	private final QuestionRepostiory questionRepostiory;
	
	public List<Question> getList(){
		return this.questionRepostiory.findAll();
	}
	
	public Question getQuestion(Integer id) {
		Optional<Question> question = this.questionRepostiory.findById(id);
		if (question.isPresent()) {
			return question.get();
		} else {
			throw new DataNotFoundException("question not found");
		}
	}
}

 

DataNotFoundException.java

package com.mysite.sbb;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "entity not found")
public class DataNotFoundException extends RuntimeException{
	private static final long serialVersionUID = 1L;
	public DataNotFoundException(String message) {
		super(message);
	}
}

 

QuestionController.java

package com.mysite.sbb.question;

import java.util.List;
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 lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class QuestionController {
	
	private final QuestionService questionService;
	
	@GetMapping("/question/list")
	public String list(Model model) {
		List<Question> questionList = this.questionService.getList();
		model.addAttribute("questionList",questionList);
		return "question_list";
	}
	
	@GetMapping(value = "/question/detail/{id}")
	public String detail(Model model, @PathVariable("id") Integer id) {
		Question question = this.questionService.getQuestion(id);
		model.addAttribute("question", question);
		return "question_detail";
	}
}

 

상세 페이지 출력하기

question_detail.html

<h1 th:text="${question.subject}">제목</h1>
<div th:text="${question.content}">내용</div>

해당 id값에 맞는 질문 내용이 정상적으로 보이게 됨.

 

http://localhost:8080/question/detail/20 와 같은 없는 id값을 요청할 경우 404에러 발생.

 

 

https://exuzii.tistory.com/75

 

스프링 부트 URL 프리픽스 / 매핑 url 중복 제거

@GetMapping("/question//list") @GetMapping(value="/question/detail/{id}") 위의 url의 프리픽스가 /question으로 시작한다. 이렇게 중복될 때 해당 컨트롤러에 매핑되는 url은 항상 /question 으로 시작할 수 있도록 설정

exuzii.tistory.com

 

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

서비스 활용

서비스가 필요한 이유?

복잡한 코드를 모듈화 할 수 있으며, 엔티티 객체를 DTO 객체로 변환할 수 있다.

 

서비스 만들기

원래 컨트롤러에서 리포지토리를 사용했다.
이제 컨트롤러에서 리포지토리 대신 사용할 서비스를 만들어보자.

 

QuestionService.java

package com.mysite.sbb.question;

import java.util.List;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class QuestionService {

	private final QuestionRepostiory questionRepostiory;
	
	public List<Question> getList(){
		return this.questionRepostiory.findAll();
	}
}

 

컨트롤러에서 서비스 사용하기

이전에 리포지토리를 사용했다면 그 중간을 이어주는 서비스를 사용하도록 변경해주자.

 

QuestionController.java

package com.mysite.sbb.question;

import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class QuestionController {
	
	private final QuestionService questionService;
	
	@GetMapping("/question/list")
	public String list(Model model) {
		List<Question> questionList = this.questionService.getList();
		model.addAttribute("questionList",questionList);
		return "question_list";
	}
}

똑같이 동일한 결과를 볼 수 있다.

 

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

질문 목록 만들기

http://localhost:8080/question/list 요청

 

=> 404에러 발생. 저 url과 매핑되는 것이 없기 때문.

 

질문 목록 URL 매핑하기

com.mysite.sbb.question 패키지 안에
QuestionController.java 클래스 생성.

 

QuestionController.java

package com.mysite.sbb.question;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class QuestionController {

	@GetMapping("/question/list")
	@ResponseBody
	public String list() {
		return "question list";
	}
}

재실행 하면 문자열이 출력된 것을 확인 가능.

URL 매핑에 성공한 것이다.

 

템플릿 설정하기

앞서 'question list'를 직접 자바 코드로 작성하여 문자열로 리턴하였다.

하지만 보통 자바 코드에서 직접 만들지 않고 템플릿을 사용한다.

템플릿이란? 자바 코드를 삽입할 수 있는 HTML 형식의 파일을 말한다.

 

템플릿으로는 타임리프(Thymeleaf)를 사용할 것이다.

 

타임리프 설치

//thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'

build.gradle 파일에 추가하고 마우스 우클릭 후 [Gradle] -> [Refresh...] 클릭.

 

템플릿 사용하기

html파일 생성

src/main/resources/template 을 선택 후 우클릭을 눌러 [New -> File]을 클릭한다..
파일이름은 "question_list.html"로 입력한다.

<h2>Hello Template</h2>

Controller 수정

package com.mysite.sbb.question;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class QuestionController {

	@GetMapping("/question/list")
	public String list() {
		return "question_list";
	}
}
@ResponseBody 어노테이션을 삭제하고, return값은 html의 파일명으로 수정한다.

실행 후 화면.

 

데이터를 템플릿에 전달하기

이제 질문 목록의 데이터를 조회하여 템플릿을 통해 화면에 출력하려고 한다..
질문 관련 데이터를 조회하기 위해서는 QuestionRepository를 사용해야 한다.

 

Controller 수정

QuestionController.java

package com.mysite.sbb.question;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class QuestionController {
	private final QuestionRepostiory questionRepostiory;

	@GetMapping("/question/list")
	public String list(Model model) {
		List<Question> questionList = this.questionRepostiory.findAll();
		model.addAttribute("questionList",questionList);
		return "question_list";
	}
}
@RequiredArgscontructor 어노테이션은 생성자 방식으로 questionRepository 객체를 주입한다. final이 붙은 속성을 포함하는 생성자를 자동으로 만들어주는 역할을 한다.

또 finalAll() 메서드를 이용하여 질문 목록을 가져와 questionList에 저장해준다.
list메서드의 매개변수에 있는 model을 통해서 템플릿으로 데이터를 전달해준다.

 

 

데이터 화면에 출력하기

html 템플릿 수정

question_list.html

<table>
	<thead>
		<tr>
			<th>제목</th>
			<th>작성일시</th>
		</tr>
	</thead>
	<tbody>
		<tr th:each="question : ${questionList}">
			<td th:text="${question.subject}"></td>
			<td th:text="${question.createDate}"></td>
		</tr>
	</tbody>
</table>
tbody 안의 내용이 가장 중요하다.
th:는 타임리프에서 사용하는 속성인데 이 곳에서 자바 코드와 연결된다. 자바 코드에서 model에 담아 전달했던 questionList를 ${questionList}를 통해 받는다.
th:each문을 통해 questionList에서 값을 하나씩 꺼내 출력한다.

실행결과.

 

루트 URL 사용하기

http://localhost:8080 으로 접속해보자.

 

현재 루트 URL을 매핑하지 않아서 404 오류가 뜬다.

 

MainController.java

package com.mysite.sbb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MainController {
	@GetMapping("/sbb")
	@ResponseBody
	public String index() {
		return "안녕하세요. sbb에 오신 것을 환영합니다.";
	}
	
	@GetMapping("/")
	public String root() {
		return "redirect:/question/list";
	}
}
"/"url과 매핑을 시킨다.
여기서 "redirect:/question/list" 이 부분은 루트로 들어오면 /question/list 페이지로 리다이렉트 하라는 명령어이다.

따라서 localhost:8080을 입력해도 localhost:8080/question/list로 자동으로 이동한다.

728x90
반응형
LIST

+ Recent posts