list.html 에 검색 버튼 수정 및 초기화 버튼 생성
visually-hidden : 시각적으로 숨김
<input th:value="${#strings.isEmpty(dto.keyword) ? '' : dto.keyword}" type="text" name="keyword" class="form-control" placeholder="검색어 입력..."/>
<button type="submit" class="btn btn-outline-secondary">
<i class="bi bi-search"></i>
<span class="visually-hidden">검색</span>
</button>
<a class="btn btn-outline-danger" th:href="@{/board/list}">
<i class="bi bi-arrow-clockwise"></i>
<span class="visually-hidden">새로고침</span>
</a>
query = ... 에 & 추가query="" 빈 문자열query="&search=검색조건&keyword=검색어" 형식의 문자열String query = "";
if(dto.getKeyword() != null) {
query = "&search="+dto.getSearch()+"&keyword="+dto.getKeyword();
}
pagination 페이징 처리 수정 @{'/board/list?pageNum='+${dto.startPageNum-1} + ${dto.query}}@{|/board/list?pageNum=${dto.startPageNum-1}${dto.query}|}<ul class="pagination">
<li th:if="${dto.startPageNum ne 1}" class="page-item">
<a class="page-link"
th:href="@{|/board/list?pageNum=${dto.startPageNum-1}${dto.query}|}">‹</a>
</li>
<li th:each="i : ${#numbers.sequence(dto.startPageNum, dto.endPageNum)}" class="page-item">
<a class="page-link" th:classappend="${i eq dto.pageNum ? 'active' : null}"
th:href="@{|/board/list?pageNum=${i}${dto.query}|}" th:text="${i}">
</a>
</li>
<li th:if="${dto.endPageNum lt dto.totalPageCount}" class="page-item">
<a class="page-link"
th:href="@{|/board/list?pageNum=${dto.endPageNum+1}${dto.query}|}">›</a>
</li>
</ul>
th:each="tmp : ${dto.list}" 코드 수정<td>
<a th:href="@{|/board/view?num=${tmp.num}${dto.query}|}" th:text="${tmp.title}"></a>
</td>
BoardService 에 getDetail 메소드 수정
글 자세히 보기를 보여주기 위한 서비스 메소드
public BoardDto getDetail(BoardDto dto);
getDetail 오버라이드 수정@Override
public BoardDto getDetail(BoardDto dto) {
return boardDao.getByDto(dto);
}
getByDto 메소드 생성public BoardDto getByDto(BoardDto dto);
@Override
public BoardDto getByDto(BoardDto dto) {
return session.selectOne("board.getByDto", dto);
}
getByDto 문 추가<select id="getByDto" parameterType="boardDto" resultType="boardDto">
SELECT *
FROM
(SELECT b.num, writer, title, content, viewCount,
TO_CHAR(b.createdAt, 'YY"년" MM"월" DD"일" HH24:MI') AS createdAt,
profileImage,
LAG(b.num, 1, 0) OVER (ORDER BY b.num DESC) AS prevNum,
LEAD(b.num, 1, 0) OVER (ORDER BY b.num DESC) AS nextNum
FROM board b
INNER JOIN users u ON b.writer = u.userName)
WHERE num = #{num}
</select>
sql 문 search 생성<sql id="search">
<choose>
<when test="search == 'writer'">
WHERE writer LIKE '%' || #{keyword} || '%'
</when>
<when test="search == 'title'">
WHERE title LIKE '%' || #{keyword} || '%'
</when>
<when test="search == 'title_content'">
WHERE title LIKE '%' || #{keyword} || '%' OR content LIKE '%' || #{keyword} || '%'
</when>
</choose>
</sql>
selectPage & getCount & getByDtosearch 문으로 변경<select id="selectPage" parameterType="boardDto" resultType="boardDto">
SELECT *
FROM
(SELECT result1.*, ROWNUM AS rnum
FROM
(SELECT num, writer, title, viewCount, createdAt
FROM board
<include refid="search"/>
ORDER BY num DESC
) result1)
WHERE rnum BETWEEN #{startRowNum} AND #{endRowNum}
</select>
<select id="getCount" parameterType="boardDto" resultType="int">
SELECT NVL(MAX(ROWNUM), 0) AS count
FROM board
<include refid="search"/>
</select>
<select id="getByDto" parameterType="boardDto" resultType="boardDto">
SELECT *
FROM
(SELECT b.num, writer, title, content, viewCount,
TO_CHAR(b.createdAt, 'YY"년" MM"월" DD"일" HH24:MI') AS createdAt,
profileImage,
LAG(b.num, 1, 0) OVER (ORDER BY b.num DESC) AS prevNum,
LEAD(b.num, 1, 0) OVER (ORDER BY b.num DESC) AS nextNum
FROM board b
INNER JOIN users u ON b.writer = u.userName
<include refid="search"/>
)
WHERE num = #{num}
</select>
view 메소드 수정@GetMapping("/board/view")
public String boardView(BoardDto requestDto, Model model) {
/*
* requestDto 에는 자세히 보여줄 글의 num 와
* search (검색 조건), keyword (검색어) 가 들어 있을 수도 있다
* 검색어가 없는 경우는 search 와 keyword 에는 null 이 들어 있다
* */
// 서비스를 이용해서 응답에 필요한 데이터를 얻어내서
BoardDto dto = service.getDetail(requestDto);
SString query = "";
if(requestDto.getKeyword() != null) {
query = "&search="+requestDto.getSearch()+"&keyword="+requestDto.getKeyword();
}
// 검색 query 정보도 view page 에 전달
model.addAttribute("query", query);
// 댓글 목록은 원글의 글번호를 전달해서 얻어낸다
List<CommentDto> comments = service.getComments(requestDto.getNum());
// 모델 객체에 담고
model.addAttribute("dto", dto);
model.addAttribute("commentList", comments);
// 로그인된 userName 얻어내기
// 로그인 안 했으면 "annontmousUser" 가 리턴
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
boolean isLogin = userName.equals("anonymousUser") ? false:true;
// 위의 추가 정보고 모델 객체에 담는다
model.addAttribute("userName", userName);
model.addAttribute("isLogin", isLogin);
// 타임리프 페이지에서 응답
return "board/view";
}
<div class="btn-group mb-2">
<a class="btn btn-outline-secondary bg-primary-subtle btn-sm"
th:classappend="${dto.prevNum eq 0 ? 'disabled' : null}"
th:href="@{|/board/view?num=${dto.prevNum}${query}|}">
<i class="bi bi-arrow-left"></i>
Prev
</a>
<a class="btn btn-outline-secondary bg-primary-subtle btn-sm"
th:classappend="${dto.nextNum eq 0 ? 'disabled' : null}"
th:href="@{|/board/view?num=${dto.nextNum}${query}|}">
Next
<i class="bi bi-arrow-right"></i>
</a>
</div>
${param.파라미터명}${#strings } : 문자열에 관련된 유틸리티 객체를 활용 가능${!#strings.isEmpty(param.search)} : search 라는 파라미터명으로 전달된 값이 있는지 확인<p th:if="${!#strings.isEmpty(param.search)}"
class="alert alert-info px-3 py-2 rounded-3 shadow-sm">
<i class="bi bi-search me-2"></i>
<strong th:text="${param.search}"></strong> 조건
<strong th:text="${param.keyword}"></strong> 검색 결과 입니다
</p>
boardDelete 메소드 생성@GetMapping("/board/delete")
public String boardDelete(int num) {
return "board/delete";
}
deleteContent 메소드 생성public void deleteContent(int num);
@Override
public void deleteContent(int num) {
// 글 작성자와 로그인된 userName 이 동일한지 비교해서 동일하지 않으면 예외를 발생시킨다
String writer = boardDao.getByNum(num).getWriter();
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
if(!writer.equals(userName)) {
throw new RuntimeException("남의 글을 지울 수 없습니다");
}
}
delete 메소드 생성public int delete(int num);
@Override
public int delete(int num) {
return session.delete("board.delete", num);
}
delete 문 추가<delete id="delete" parameterType="int">
DELETE FROM board
WHERE num = #{num}
</delete>
deleteContent 메소드 완성@Override
public void deleteContent(int num) {
// 글 작성자와 로그인된 userName 이 동일한지 비교해서 동일하지 않으면 예외를 발생시킨다
String writer = boardDao.getByNum(num).getWriter();
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
if(!writer.equals(userName)) {
throw new RuntimeException("남의 글을 지울 수 없습니다");
}
// 삭제하고 삭제된 row 의 갯수를 리턴받는다 정상 삭제 시 1, 삭제 실패 시 0 이 리턴
int rowCount = boardDao.delete(num);
if(rowCount == 0) {
throw new RuntimeException("삭제 실패");
}
}
boardDelete 메소드 완성@GetMapping("/board/delete")
public String boardDelete(int num) {
service.deleteContent(num);
return "board/delete";
}
<script>
alert("[[${param.num}]] 번 글 삭제 완료");
location.href = "[[@{/board/list}]]";
</script>
boardEdit 메소드 생성@GetMapping("/board/edit")
public String boardEdit(int num, Model model) {
return "board/edit";
}
BoardService 에 getData 메소드 생성
수정할 글 정보를 보여주기 위한 서비스 메소드
public BoardDto getData(int num);
@Override
public BoardDto getData(int num) {
return boardDao.getByNum(num);
}
boardEdit 메소드 완성@GetMapping("/board/edit")
public String boardEdit(int num, Model model) {
model.addAttribute("dto", service.getData(num));
return "board/edit";
}
<div class="container pt-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="@{/}/">Home</a>
</li>
<li class="breadcrumb-item">
<a href="@{/board/list}">Board</a>
</li>
<li class="breadcrumb-item active">Edit</li>
</ol>
</nav>
<h1>글 수정 페이지</h1>
<form th:action="@{/board/update}" method="post" id="editForm">
<div>
<label class="form-label" for="num">글번호</label>
<input class="form-control" type="text" name="num" id="num"
th:value="${dto.num}" readonly />
</div>
<div>
<label class="form-label" for="writer">작성자</label>
<input type="text" class="form-control" name="writer" id="writer"
th:value="${dto.writer}" readonly />
</div>
<div>
<label class="form-label" for="title">제목</label>
<input type="text" class="form-control" name="title" id="title"
th:value="${dto.title}" />
</div>
<div>
<label class="form-label" for="editor">내용</label>
<div id="editor"></div>
<textarea class="form-control d-none" name="content" id="hiddenContent"></textarea>
</div>
<button class="btn btn-success btn-sm">수정</button>
<button class="btn btn-danger btn-sm">Reset</button>
</form>
</div>
<script>
// 위에 toast ui javascript 가 로딩되어 있으면 toastui.Editor 클래스를 사용할 수 있다
// 해당 클래스를 이용해서 객체 생성하면서 {} object 로 ui 에 관련된 옵션을 잘 전달하면
// 우리가 원하는 모양의 텍스트 편집기를 만들 수 있다
const editor = new toastui.Editor({
el: document.querySelector('#editor'),
height: '500px',
initialEditType: 'wysiwyg',
previewStyle: 'vertical',
language: 'ko',
initialValue: `[(${dto.content})]` // unescape 된 text 를 출력
});
document.querySelector("#editForm").addEventListener("submit", (e) => {
// 에디터로 작성된 문자열 읽어오기
const content = editor.getHTML();
// 테스트로 콘솔에 출력하기
console.log(content);
// 에디터로 작성된 문자열을 폼 전송이 될수 있도록 textarea 의 value 로 넣어준다.
document.querySelector("#hiddenContent").value = content;
// 테스트 하기 위해 폼 전송 막기
// e.preventDefault();
// 폼전송을 막지 않으면 전송이 된다.
});
</script>
BoardController 에 boardUpdate 메소드 생성
게시글 수정 반영 요청 처리
@PostMapping("/board/update")
public String boardUpdate(BoardDto dto, RedirectAttributes ra) {
// 글 수정 반영하고
// 글 자세히 보기로 리다일렉트 이동
return "redirect:/board/view?num="+dto.getNum();
}
updateContent 메소드 생성public void updateContent(BoardDto dto); // 게시글 수정
@Override
public void updateContent(BoardDto dto) {
// 글 작성자와 로그인된 userName 이 동일한지 비교해서 동일하지 않으면 예외를 발생시킨다
String writer = boardDao.getByNum(dto.getNum()).getWriter();
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
if(!writer.equals(userName)) {
throw new RuntimeException("본인이 작성한 글만 수정이 가능합니다");
}
}
update 메소드 생성public int update(BoardDto dto);
@Override
public int update(BoardDto dto) {
return session.update("board.update", dto);
}
update 문 추가<update id="update" parameterType="boardDto">
UPDATE board
SET title = #{title}, content = #{content}
WHERE num = #{num}
</update>
@Override
public void updateContent(BoardDto dto) {
// 글 작성자와 로그인된 userName 이 동일한지 비교해서 동일하지 않으면 예외를 발생시킨다
String writer = boardDao.getByNum(dto.getNum()).getWriter();
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
if(!writer.equals(userName)) {
throw new RuntimeException("본인이 작성한 글만 수정이 가능합니다");
}
int rowCount = boardDao.update(dto);
if(rowCount == 0) {
throw new RuntimeException("수정 실패");
}
}
boardUpdate 메소드 완성RedirectAttributes 매개변수 전달addFlashAttribute("message", ...) : view page 에서 ${message} 로 참조 가능@PostMapping("/board/update")
public String boardUpdate(BoardDto dto, RedirectAttributes ra) {
// 글 수정 반영하고
service.updateContent(dto);
// 리다일렉트 이동해서 출력할 메세지도 담는다
ra.addFlashAttribute("message", "게시글을 성공적으로 수정 완료");
// 글 자세히 보기로 리다일렉트 이동
return "redirect:/board/view?num="+dto.getNum();
}
th:if="${message != null}" th:if="${!#strings.isEmpty(message)}" 대신에th:if="${message}" 로 사용하면 편리<p th:if="${message}" class="alert alert-success p-3" th:text="${message}"></p>
MemberDao, MemberDaoImpl, MemberMapper 을 만들지 않고
@Mapper
public interface MemberMapper{ } 로 검색해서 구현
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class MemberDto {
private int num;
private String name;
private String addr;
}
Select & Insert & Update & Delete & SelectKey 어노테이션 추가@Mapper
public interface MemberMapper {
@Select("SELECT num, name, addr FROM member ORDER BY num ASC")
public List<MemberDto> selectAll();
@Select("SELECT num, name, addr FROM member WHERE num = #{num}")
public MemberDto getByNum(int num);
@Insert("""
INSERT INTO member(num, name, addr)
VALUES(#{num}, #{name}, #{addr})
""")
@SelectKey(
statement = "SELECT member_seq.NEXTVAL FROM DUAL",
keyProperty = "num",
resultType = int.class,
before = true
)
public void insert(MemberDto dto);
@Update("UPDATE member SET name = #{name}, addr = #{addr} WHERE num = #{num}")
public int update(MemberDto dto);
@Delete("DELETE FROM member WHERE num = #{num}")
public int deleteByNum(int num); // delete 된 rowCount 가 리턴
}
public class MemberException extends RuntimeException{
// 열거형 클래스
public enum Reason{
NOT_FOUND, UPDATE_FAILED, DELETE_FAILED
}
// 필드
public Reason reason;
// 생성자
public MemberException(Reason reason, String message) {
// 부모 생성자에 예외 메세지를 전달한다
super(message);
// 필드에 예외의 원인을 저장한다
this.reason = reason;
}
// 예외 객체를 편리하게 만들어서 리턴해주는 편의 메소드
public static MemberException notFound(int num) {
return new MemberException(Reason.NOT_FOUND, num+"번 회원이 없습니다");
}
public static MemberException updateFailed(int num) {
return new MemberException(Reason.UPDATE_FAILED, num+"번 회원 수정 실패");
}
public static MemberException deleteFailed(int num) {
return new MemberException(Reason.DELETE_FAILED, num+"번 회원이 없습니다");
}
}
memberException 메소드 생성@ExceptionHandler(MemberException.class)
public String memberException(MemberException me, Model model) {
model.addAttribute("title", "Member 관련 작업 중에 예외가 발생");
model.addAttribute("message", me.getMessage());
model.addAttribute("reason", me.reason.name());
return "error/member-exception";
}
public interface MemberService {
public List<MemberDto> getAll();
public MemberDto getMember(int num);
public void addMember(MemberDto dto);
public void updateMember(MemberDto dto);
public void deleteMember(int num);
}
MemberServiceImpl 클래스 생성
MemberMapper type 을 주입 받아서 사용
@RequiredArgsConstructor
@Service
public class MemberServiceImpl implements MemberService{
private final MemberMapper mapper;
@Override
public List<MemberDto> getAll() {
return mapper.selectAll();
}
@Override
public MemberDto getMember(int num) {
MemberDto dto = mapper.getByNum(num);
if(dto==null) {
throw MemberException.notFound(num);
}
return dto;
}
@Override
public void addMember(MemberDto dto) {
mapper.insert(dto);
}
@Override
public void updateMember(MemberDto dto) {
int rowCount = mapper.update(dto);
if(rowCount == 0) {
throw MemberException.updateFailed(dto.getNum());
}
}
@Override
public void deleteMember(int num) {
int rowCount = mapper.deleteByNum(num);
if(rowCount == 0) {
throw MemberException.deleteFailed(num);
}
}
}
@RequiredArgsConstructor
@Controller
public class MemberController {
// 필요한 의존 객체를 주입 받는다
private final MemberService service;
@PostMapping("/member/update")
public String update(MemberDto dto) {
service.updateMember(dto);
return "member/update";
}
@GetMapping("/member/edit")
public String edit(int num, Model model) {
MemberDto dto = service.getMember(num);
model.addAttribute("dto", dto);
return "member/edit";
}
@GetMapping("/member/delete")
public String delete(int num) {
service.deleteMember(num);
return "member/delete";
}
@PostMapping("/member/save")
public String save(MemberDto dto) {
service.addMember(dto);
return "redirect:/member/list";
}
@GetMapping("/member/new-form")
public String newForm() {
return "member/new-form";
}
@GetMapping("/member/list")
public String list(Model model) {
List<MemberDto> list = service.getAll();
model.addAttribute("list", list);
return "member/list";
}
}
<div class="container">
<a th:href="@{/member/new-form}">회원 추가</a>
<h1>회원 목록 입니다</h1>
<table>
<thead>
<tr>
<th>번호</th>
<th>이름</th>
<th>주소</th>
<th>수정</th>
<th>삭제</th>
</tr>
</thead>
<tbody>
<tr th:each="tmp : ${list}">
<td>[[${tmp.num }]]</td>
<td>[[${tmp.name }]]</td>
<td>[[${tmp.addr }]]</td>
<td>
<a th:href="@{/member/edit(num=${tmp.num})}">수정</a>
</td>
<td>
<a th:href="@{/member/delete(num=${tmp.num})}">삭제</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="container">
<h1>회원 추가 양식</h1>
<form th:action="@{/member/save}" method="post">
<div>
<label for="name">이름</label>
<input type="text" name="name" id="name"/>
</div>
<div>
<label for="addr">주소</label>
<input type="text" name="addr" id="addr"/>
</div>
<button type="submit">저장</button>
</form>
</div>
<div class="container">
<h1>회원 수정 폼</h1>
<form th:action="@{/member/update}" method="post">
<input type="hidden" name="num" th:value="${dto.num}"/>
<div>
<label for="name">이름</label>
<input type="text" name="name" id="name" th:value="${dto.name}"/>
</div>
<div>
<label for="addr">주소</label>
<input type="text" name="addr" id="addr" th:value="${dto.addr}"/>
</div>
<button type="submit">수정 확인</button>
<button type="reset">취소</button>
</form>
</div>
<div class="container">
<p>
<strong>[[${param.name}]]</strong>님의 정보 수정 완료
<a th:href="@{/member/list}">목록 보기</a>
</p>
</div>
<script th:inline="javascript">
alert( [[${param.num}]] + " 번 회원의 정보를 삭제 했습니다");
location.href=[[@{/member/list}]] ;
</script>
<div class="container">
<h1>[[${title}]]</h1>
<p>[[${message}]]</p>
<p>상태 코드 : <strong>[[${status}]]</strong></p>
<a th:href="@{/}">인덱스로</a>
</div>
<div class="container">
<h1 th:text="${title}"></h1>
<p th:text="${message}"></p>
<p>원인 : <strong th:text="${reason}"></strong></p>
</div>
따로 xml 문서로 빼는 것이 편하다
insert 메소드 수정namespace = "com.example.spring08.repository.MemberMapper"<insert id="insert" parameterType="memberDto">public void insert(MemberDto dto);
@Mapper 어노테이션이 붙어 있는 인터페이스의namespace 에 명시하public void insert(MemberDto dto); 에서 메소드명과sql id 를 일치 시킨다parameterType 이 된다<mapper namespace="com.example.spring08.repository.MemberMapper">
<insert id="insert" parameterType="memberDto">
<selectKey keyProperty="num" resultType="int" order="BEFORE">
SELECT member_seq.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO member
(num, name, addr)
VALUES(#{num}, #{name}, #{addr})
</insert>
</mapper>
관점지향 프로그래밍
횡단 관심사란?
여러 곳에 반복되는 공통 기능을 한 곳에 모아 처리하는 것
spring08.test 패키지 생성
WritingUtil 클래스 생성
@Component 어노테이션을 붙여서 spring 이 관리하는 bean으로 등록
@Component
public class WritingUtil {
public void writeLetter() {
System.out.println("편지를 써요");
try {
Thread.sleep(1000);
}catch(Exception e) {}
}
public void writeReport() {
System.out.println("보고서를 써요");
try {
Thread.sleep(1000);
}catch(Exception e) {}
}
public void writeDiary() {
System.out.println("일기를 써요");
try {
Thread.sleep(1000);
}catch(Exception e) {}
}
}
testAop 메소드 생성// 의존 객체 주입 받기
@Autowired
private WritingUtil util;
// spring 프레임 워크가 준비되고 난 이후에 호출할 메소드
@PostConstruct
public void testAop() {
util.writeLetter();
util.writeReport();
util.writeDiary();
}
편지를 써요
보고서를 써요
일기를 써요
spring08.aop 패키지 생성
pom.xml 에 dependency 추가
aop (Aspect Oriented Programming) 관련 dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId></dependency>
⭐ maven ➜ pom.xml
gradle ➜ build.gradle
@Before("execution(...)") , @After("execution(...)") 어노테이션 생성@Aspect 어노테이션을 이용해서 만든다@Aspect
@Component
public class TimeAspect {
@Before("execution(void writeLetter())")
public void start() {
Date start = new Date();
long startTime = start.getTime();
System.out.println("시작 시간 : " +startTime);
}
@After("execution(void writeLetter())")
public void end() {
Date start = new Date();
long startTime = start.getTime();
System.out.println("종료 시간 : " +startTime);
}
}
시작 시간 : 1756278811756 ➜ 1/1000 초 단뒤
편지를 써요
종료 시간 : 1756278812757
보고서를 써요
일기를 써요
write*() 로 변경매개변수가 없고
리턴 타입이 void이며
메소드 이름이 write로 시작하는
모든 메소드의 시작 전과 종료 후에 AOP가 개입
원하는 위치에 aop 를 적용하려면 이 부분을 잘 작성하면 된다
여기의 작성 문법이 AspectJ Expression
@Before("execution(void write*())")
public void start() {
Date start = new Date();
long startTime = start.getTime();
System.out.println("시작 시간 : " +startTime);
}
@After("execution(void write*())")
public void end() {
Date start = new Date();
long startTime = start.getTime();
System.out.println("종료 시간 : " +startTime);
}
GalleryService 에서 GalleryDto 목록을 얻어오는 작업
// 갤러리 목록을 읽어온 다음
List<GalleryDto> list = mapper.getList();
// GalleryDto 에 Gallery 이미지 목록을 하나씩 반복문 돌면서 넣어준다
for(GalleryDto tmp : list){
List<GalleryImageDto> imageList = mapper.getImageLIst(tmp.getNum());
tmp.setImageList(imageList);
}