userEdit 메소드 생성@GetMapping("/user/edit")
public String userEdit(Model model) {
// 로그인된 userName
String userName = SecurityContextHolder.getContext().getAuthentication().getName();;
UserDto dto = service.getUser(userName);
// 응답에 필요한 정보를 Model 객체에 담기
model.addAttribute("dto", dto);
// 타임리프 view page 에서 회원정보 수정 폼을 응답
return "user/edit";
}
edit.html 생성
{경로변수명}(경로변수 = 넣을값)
<div class="container">
<h1>가입정보 수정 양식</h1>
<form th:action="@{/user/update}" method="post" enctype="multipart/form-data" class="p-4 border rounded shadow-sm bg-light">
<div class="mb-3">
<label for="userName" class="form-label">아이디</label>
<input type="text" class="form-control" id="userName" name="userName" th:value="${dto.userName}" readonly>
</div>
<div class="mb-3">
<label for="email" class="form-label">이메일</label>
<input type="email" class="form-control" id="email" name="email" th:value="${dto.email}" placeholder="이메일을 입력하세요">
</div>
<div class="mb-3">
<label class="form-label d-block">프로필 이미지</label>
<div class="text-center mb-2">
<a href="javascript:" id="profileLink">
<i th:if="${dto.profileImage == null}" class="bi bi-person-circle text-secondary" style="font-size:100px;"></i>
<img th:unless="${dto.profileImage == null}" th:src="@{/upload/{name}(name=${dto.profileImage})}" class="rounded-circle border" style="width:100px; height:100px; object-fit:cover;">
</a>
</div>
<input type="file" class="form-control d-none" name="profileFile" accept="image/*">
<div class="form-text">클릭해서 프로필 이미지를 변경하세요.</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">수정 확인</button>
<button type="reset" class="btn btn-outline-secondary">취소</button>
</div>
</form>
</div>
<script>
document.querySelector("#profileLink").addEventListener("click", ()=>{
document.querySelector("input[name=profileFile]").click();
});
document.querySelector("input[name=profileFile]").addEventListener("change", (e)=>{
const files = e.target.files;
const reader=new FileReader();
reader.readAsDataURL(files[0]);
reader.onload = ()=>{
const img=`<img src="${reader.result}"
style="width:100px;height:100px;border-radius:50%">`;
document.querySelector("#profileLink").innerHTML = img;
};
});
</script>
UserDto 에 profileImage 메소드 생성
<input type="file" name="profileImage"> 을 처리하기 위한 필드
private MultipartFile profileFile;
updateUser 추상 메소드 추가public void updateUser(UserDto dto);
UserServiceImpl 에 코드 추가 및 오버라이드
업로드된 이미지를 저장할 위치 얻어내기
@Value("${file.location}")
private String fileLocation;
@Override
public void updateUser(UserDto dto) {
// 업로드된 이미지가 있는지 읽어와 본다
MultipartFile image = dto.getProfileFile();
// 만일 업로드된 이미지가 있다면
if(!image.isEmpty()) {
// 원본 파일명
String orgFileName = image.getOriginalFilename();
// 이미지의 확장자를 유지하기 위해 뒤에 원본 파일명을 추가
String saveFileName = UUID.randomUUID().toString() + orgFileName;
// 저장할 파일의 전체 경로 구성하기
String filePath = fileLocation + File.separator + saveFileName;
try {
// 업로드된 파일을 저장할 파일 객체 생성
File saveFile = new File(filePath);
image.transferTo(saveFile);
} catch(Exception e) {
e.printStackTrace();
}
// UserDto 에 저장된 이미지의 이름을 넣어준다
dto.setProfileImage(saveFileName);
}
// UserDao 객체를 이용해서 수정 반영 (dto 의 profileImage 는 null 일 수도 있다)
dao.update(dto);
}
id="update" 문 확인<update id="update" parameterType="com.example.spring08.dto.UserDto">
UPDATE users
SET email=#{email}, updatedAt=SYSDATE
<if test="profileImage != null">, profileImage=#{profileImage}</if>
WHERE userName=#{userName}
</update>
<if test="profileImage != null">, profileImage=#{profileImage}</if>
update 메소드 생성@PostMapping("/user/update")
public String upate(UserDto dto) {
// 서비스를 이용해서 개인 정보를 수정하고
service.updateUser(dto);
// 개인 정보 자세히 보기로 리다일렉트 이동
return "redirect:/user/info";
}
@Controller
public class BoardController {
@GetMapping("/board/list")
public String list(Model model) {
return "board/list";
}
}
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class BoardDto {
private int num;
private String writer;
private String title;
private String content;
private int viewCount;
private String createdAt;
// 페이징 처리를 위한 필드
private int startRowNum;
private int endRowNum;
// 프로필 이미지 출력을 위한 필드
private String profileImage;
// 이전글, 다음글 처리를 위한 필드
private int prevNum;
private int nextNum;
// 검색 키워드를 담기 위한 필드
private String keyword;
}
public interface BoardDao {
public List<BoardDto> selectPage(BoardDto dto);
}
@RequiredArgsConstructor // 의존 객체 생성자로 주입받기 위한 어노테이션 (lombok 의 기능)
@Repository
public class BoardDaoImpl implements BoardDao{
// 의존 객체
private final SqlSession session;
@Override
public List<BoardDto> selectPage(BoardDto dto) {
return session.selectList("board.selectPage", dto);
}
}
mybatis.type-aliases-package-com.example.spring08.**<select id="selectPage" parameterType="boardDto" resultMap="boardDto">
SELECT *
FROM
(SELECT result1.*, ROWNUM AS rnum
FROM
(SELECT num, writer, title, viewCount, createdAt
FROM board
ORDER BY num DESC) result1)
WHERE rnum BETWEEN #{startRowNum} AND #{endRowNum}
</select>
<select id="selectPageByKeyword" parameterType="boardDto" resultType="boardDto">
SELECT *
FROM
(SELECT result1.*, ROWNUM AS rnum
FROM
(SELECT num, writer, title, viewCount, createdAt
FROM board
WHERE title LIKE '%' || #{keyword} || '%' OR content LIKE '%' || #{keyword} || '%'
ORDER BY num DESC) result1)
WHERE rnum BETWEEN #{startRowNum} AND #{endRowNum}
</select>
selectPageByKeyword 메소드 생성public List<BoardDto> selectPageByKeyword(BoardDto dto);
@Override
public List<BoardDto> selectPageByKeyword(BoardDto dto) {
return session.selectList("board.selectPageByKeyword", dto);
}
BoardListResponse 클래스 생성
응답에 필요한 데이터를 하나의 객체에 담을 수 있도록 클래스를 설계
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class BoardListResponse {
// 글 목록
private List<BoardDto> list;
private int startPageNum;
private int endPageNum;
private int totalPageCount;
private int pageNum;
private int totalRow;
private String keyword;
}
getCount & getCountByKeyword 메소드 추가public int getCount();
public int getCountByKeyword(String keyword);
@Override
public int getCount() {
// resultType : int
return session.selectOne("board.getCount");
}
@Override
public int getCountByKeyword(String keyword) {
/*
* parameterType : string
* resultType : int
* */
return session.selectOne("board.getCountByKeyword", keyword);
}
<select id="getCount" resultType="int">
SELECT MAX(ROWNUM) AS count
FROM board
</select>
<select id="getCountByKeyword" parameterType="string" resultType="int">
SELECT MAX(ROWNUM) AS count
FROM board
WHERE title LIKE '%' || #{keyword} || '%' OR content LIKE '%' || #{keyword} || '%'
</select>
public interface BoardService {
public BoardListResponse getBoardList(int pageNum, String keyword);
}
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService{
private final BoardDao boardDao;
// pageNum 또는 keyword 에 해당하는 글목록과 추가 정보를 BoardListResponse 객체에 담아서
// 리턴하는 메소드
@Override
public BoardListResponse getBoardList(int pageNum, String keyword) {
//한 페이지에 몇개씩 표시할 것인지
final int PAGE_ROW_COUNT=3;
//하단 페이지를 몇개씩 표시할 것인지
final int PAGE_DISPLAY_COUNT=3;
//보여줄 페이지의 시작 ROWNUM
int startRowNum=1+(pageNum-1)*PAGE_ROW_COUNT; //공차수열
//보여줄 페이지의 끝 ROWNUM
int endRowNum=pageNum*PAGE_ROW_COUNT; //등비수열
//하단 시작 페이지 번호 (정수를 정수로 나누면 소수점이 버려진 정수가 나온다)
int startPageNum = 1 + ((pageNum-1)/PAGE_DISPLAY_COUNT)*PAGE_DISPLAY_COUNT;
//하단 끝 페이지 번호
int endPageNum=startPageNum+PAGE_DISPLAY_COUNT-1;
/*
StringUtils 클래스의 isEmpty() static 메소드를 이용하면 문자열이 비었는지 여부를 알수 있다
null 또는 "" 빈문자열은 비었다고 판정된다.
StringUtils.isEmpty(keyword)
는
keyword == null or "".equals(keyword)
를 대체할수 있다.
*/
//전체 글의 갯수
int totalRow=0;
//만일 전달된 keyword 가 없다면
if(StringUtils.isEmpty(keyword)){
totalRow=boardDao.getCount();
}else{ //있다면
totalRow=boardDao.getCountByKeyword(keyword);
}
//전체 페이지의 갯수 구하기
int totalPageCount=(int)Math.ceil(totalRow/(double)PAGE_ROW_COUNT);
//끝 페이지 번호가 이미 전체 페이지 갯수보다 크게 계산되었다면 잘못된 값이다.
if(endPageNum > totalPageCount){
endPageNum=totalPageCount; //보정해 준다.
}
//dto 에 select 할 row 의 정보를 담고
BoardDto dto=new BoardDto();
dto.setStartRowNum(startRowNum);
dto.setEndRowNum(endRowNum);
//글목록
List<BoardDto> list=null;
//만일 keyword 가 없아면
if(StringUtils.isEmpty(keyword)){
list = boardDao.selectPage(dto);
}else{//있다면
// dto 에 keyword 를 담고
dto.setKeyword(keyword);
// 키워드에 해당하는 글 목록을 얻어낸다
list = boardDao.selectPageByKeyword(dto);
}
// 한줄 coding 으로 BoardListResponse 객체를 만들어서 리턴
return BoardListResponse.builder()
.list(list)
.pageNum(pageNum)
.keyword(keyword)
.startPageNum(startPageNum)
.endPageNum(endPageNum)
.totalPageCount(totalPageCount)
.totalRow(totalRow)
.build();
}
}
BoardController 에 list 메소드 코드 추가 및 서비스 주입
@RequestParam 어노테이션을 이용하면 요청 파라미터를 추출하면서 해당 값이 없으면 defaultValue 설정 가능
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService service;
@GetMapping("/board/list")
public String list(Model model,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "") String keyword) {
// 응답에 필요한 데이터를 얻어내서
BoardListResponse listResponse = service.getBoardList(pageNum, keyword);
// 모델 객체에 담고
model.addAttribute("listResponse", listResponse);
// 타임리프 페이지에서 응답
return "board/list";
}
}
board 폴더 생성 후 그 안에 list.html 생성
GET 방식 파라미터 : (파라미터명 = 값)
ex. (num=${tmp.num})
<th:block th:insert="/include/navbar :: nav('board')"></th:block>
<div class="container pt-1">
<a class="btn btn-outline-primary btn-sm" th:href="@{/board/new-form}">
새 글 작성 <i class="bi bi-pencil-square"></i>
</a>
<h1>게시글 목록 입니다</h1>
<!-- 행을 만들고 -->
<div class="row my-3">
<!-- 4/12 인 칼럼을 만들고 margin-start:auto 를 부여해서 왼쪽 마진을 자동 부여한다 -->
<div class="col-lg-4 col-md-6 ms-auto">
<form th:action="@{/board/list}" method="get">
<div class="input-group">
<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 bg-primary-subtle btn-sm">
<i class="bi bi-search"></i>
</button>
</div>
</form>
</div>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th>글번호</th>
<th>작성자</th>
<th>제목</th>
<th>조회수</th>
<th>작성일</th>
</tr>
</thead>
<tbody>
<tr th:each="tmp : ${dto.list}">
<td th:text="${tmp.num}"></td>
<td th:text="${tmp.writer}"></td>
<td>
<a th:href="@{/board/view(num=${tmp.num})}" th:text="${tmp.title}"></a>
</td>
<td th:text="${tmp.viewCount}"></td>
<td th:text="${tmp.createdAt}"></td>
</tr>
</tbody>
</table>
<ul class="pagination">
<li th:if="${dto.startPageNum ne 1}" class="page-item">
<a class="page-link"
th:href="@{/board/list(num=${dto.startPageNum-1}, keyword=${dto.keyword})}">‹</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}, keyword=${dto.keyword})}" th:text="${i}">
</a>
</li>
<li th:id="${dto.endPageNum lt dto.totalPageCount}" class="page-item">
<a class="page-link"
th:href="@{/board/list(pageNum=${dto.endPageNum+1}, keyword=${dto.keyword})}">›</a>
</li>
</ul>
</div>
id=getCount & id=getCountByKeyword 코드 수정<select id="getCount" resultType="int">
SELECT NVL(MAX(ROWNUM), 0) AS count
FROM board
</select>
<select id="getCountByKeyword" parameterType="string" resultType="int">
SELECT NVL(MAX(ROWNUM), 0) AS count
FROM board
WHERE title LIKE '%' || #{keyword} || '%' OR content LIKE '%' || #{keyword} || '%'
</select>
newForm 메소드 생성@GetMapping("/board/new-form")
public String newForm() {
return "board/new-form";
}
toastui 추가<th:block th:fragment="toastui">
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<script src="https://uicdn.toast.com/editor/latest/i18n/ko-kr.js"></script>
</th:block>
<div class="container">
<h1>게시글 작성 양식</h1>
<form th:action="@{/board/save}" method="post" id="saveForm">
<div class="mb-2">
<label class="form-label" for="title">제목</label>
<input class="form-control" type="text" name="title" id="title" />
</div>
<div class="mb-2">
<label class="form-label" for="editor">내용</label>
<!-- Editor ui 이 출력할 div -->
<div id="editor"></div>
<textarea class="form-control" name="content" id="hiddenContent" ></textarea>
</div>
<button class="btn btn-sm bg-primary-subtle text-white" type="submit">작성하기</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'
});
document.querySelector("#saveForm").addEventListener("submit", (e)=>{
// 에디터로 작성된 문자열 읽어오기
const content = editor.getHTML();
// 테스트로 콘솔에 출력하기
console.log(content);
// 에디터로 작성된 문자열을 폼 전송이 될 수 있도록 textarea 의 value 로 넣어준다
document.querySelector("#hiddenContent").value = content;
// 테스트 하기 위해 폼 전송 막기
// e.preventDefault();
});
</script>
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
"/user/expired", "/user/signup-form", "/user/signup",
"/test/**", "/user/check-id", "/board/list", "/board/view"};
boardSave 메소드 생성@PostMapping("/board/save")
public String boardSave(BoardDto dto) {
// 글 작성자
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
dto.setWriter(userName);
// 서비스를 이용해서 글을 저장하기
return "board/save";
}
createContent 메소드 생성public void createContent(BoardDto dto);
id="insert" 문 추가<insert id="insert" parameterType="boardDto">
<selectKey keyProperty="num" resultType="int" order="BEFORE">
SELECT board_seq.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO board
(num, writer, title, content)
VALUES(#{num}, #{writer}, #{title}, #{content})
</insert>
insert 메소드 생성public void insert(BoardDto dto);
@Override
public void insert(BoardDto dto) {
session.insert("board.insert", dto);
}
BoardServiceImpl 에 추상 메소드 오버라이드
글 정보가 저장된 이후에는 dto.num 에 글번호가 들어 있다
@Override
public void createContent(BoardDto dto) {
boardDao.insert(dto);
}
BoardController 에 boardSave 메소드 완성
@ModelAttribute : view page 에서 필요한 값을 대신 Model 객체에 담아준다
@PostMapping("/board/save")
public String boardSave(@ModelAttribute("dto") BoardDto dto) {
// 글 작성자
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
dto.setWriter(userName);
// 서비스를 이용해서 글을 저장하기
service.createContent(dto);
return "board/save";
}
<div class="container">
<script>
alert("저장 완료");
location.href = "[[@{/board/view(num=${dto.num})}]]";
</script>
</div>
id="getByNum"문 생성<select id="getByNum" parameterType="int" resultMap="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>
getByNum 메소드 생성public BoardDto getByNum(int num);
@Override
public BoardDto getByNum(int num) {
return session.selectOne("board.getByNum", num);
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CommentDto {
private int num;
private String writer;
private String content;
private String targetWriter;
private int groupNum;
private int parentNum;
private String deleted;
private String createdAt;
private String profileImage; //프로필 이미지를 출력하기 위한 필드
//대댓글의 갯수를 저장하기 위한 필드
private int replyCount;
}
<mapper namespace="comment">
<select id="selectList" parameterType="int" resultType="commentDto">
<![CDATA[
SELECT c.num, c.writer, c.targetWriter, c.content, c.deleted, c.groupNum, u.profileImage,
CASE
WHEN SYSDATE - c.createdAt < 1/1440 THEN '1분 전'
WHEN SYSDATE - c.createdAt < 10/1440 THEN '10분 전'
WHEN SYSDATE - c.createdAt < 365 THEN
TO_CHAR(TRUNC(SYSDATE - c.createdAt)) || '일 전'
WHEN SYSDATE - c.createdAt < 730 THEN '1년 전'
ELSE '2년 이상'
END AS createdAt,
(SELECT COUNT(*)
FROM comments
WHERE groupNum = c.num AND num != groupNum) AS replyCount
FROM comments c
INNER JOIN users u ON c.writer = u.userName
WHERE c.parentNum = #{num}
ORDER BY c.groupNum ASC, c.num ASC
]]>
</select>
</mapper>
public interface CommentDao {
public List<CommentDto> selectList(int parentNum);
}
@RequiredArgsConstructor
@Repository
public class CommentDaoImpl implements CommentDao{
private final SqlSession session;
// 원글의 글 번호를 이용해서 원 글에 달린 댓글 목록을 리턴하는 메소드
@Override
public List<CommentDto> selectList(int parentNum) {
return session.selectList("comment.selectList", parentNum);
}
}
BoardServiceImpl 에 의존 주입
하나의 서비스에서 여러개의 dao 에 의존하는 경우도 존재
private final CommentDao commentDao;
getDetail & getComments 메소드 생성public BoardDto getDetail(int num);
public List<CommentDto> getComments(int parentNum);
@Override
public BoardDto getDetail(int num) {
return boardDao.getByNum(num);
}
@Override
public List<CommentDto> getComments(int parentNum) {
return commentDao.selectList(parentNum);
}
boardView 메소드 생성@GetMapping("/board/view")
public String boardView(int num, Model model) {
// 서비스를 이용해서 응답에 필요한 데이터를 얻어내기
BoardDto dto = service.getDetail(num);
List<CommentDto> comments = service.getComments(num);
// 모델 객체에 담고
model.addAttribute("dto", dto);
model.addAttribute("commentList", comments);
// 로그인된 userName 얻어내기
// 로그인 안 했으면 "annontmousUser" 가 리턴
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
boolean isLogin = userName.equals("annoymousUser") ? false:true;
// 위의 추가 정보고 모델 객체에 담는다
model.addAttribute("userName", userName);
model.addAttribute("isLogin", isLogin);
// 타임리프 페이지에서 응답
return "board/view";
}
이거 타임리프로 해야함 집에서..~