
📌 TODO
- 마이페이지 구현
- 내가 푼 테스트 조회 페이지 구현
- 내가 만든 테스트 조회 페이지 구현
☑️ mypage
타임리프와 부트스트랩을 활용하여 html을 구현했다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragment/base :: common_header (~{::title},~{::link})}">
<meta charset="UTF-8">
<title>짜툴 : 마이페이지</title>
<link rel="stylesheet" th:href="@{/css/member/mypage.css}"/>
</head>
<script th:src="@{/js/member/mypage.js}"></script>
<body>
<div th:insert="~{fragment/navigation :: copy}"></div>
<div class="mypage-bar">
회원 정보 수정
</div>
<div class="mypage-content">
<div class="mypage-menu-bar">
<div class="mypage-menu" th:onclick="reload([[${link}]], [[${memberId}]])">
회원 정보 수정
</div>
<div class="mypage-menu" th:onclick="goResult([[${link}]], [[${memberId}]], '1')">
내가 푼 테스트
</div>
<div class="mypage-menu" th:onclick="goTest([[${link}]], [[${memberId}]], 'date', '1')">
내가 만든 테스트
</div>
<div class="mypage-menu">
회원 탈퇴
</div>
</div>
<div class="mypage-content-page">
<div class="member-info-form">
회원 정보 수정 폼 넣어주세요
</div>
</div>
</div>
<footer>푸터 가져오기</footer>
</body>
</html>
다음은 css 이다.
.mypage-bar {
background-color: #0066CC;
text-align: right;
padding-right: 10px;
margin-left: 20px;
margin-right: 25px;
margin-top: 10px;
height: 50px;
line-height: 48px;
font-size: x-large;
border: 1px solid #0066CC;
border-radius: 10px;
color: white;
cursor: default;
}
.mypage-content {
cursor: default;
margin-top: 10px;
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.mypage-menu-bar {
width: 15%;
height: 300px;
margin-left: 20px;
padding-top: 15px;
border: 1px solid #0066CC;
border-radius: 5px;
}
.mypage-menu {
height: 65px;
line-height: 65px;
text-align: center;
font-size: large;
cursor: pointer;
}
.mypage-content-page {
width: 79%;
height: 600px;
margin-right: 20px;
}
js 파일도 따로 생성했다.
function reload(url, memberId) {
location.href = url + '/' + memberId;
}
function goResult(url, memberId, page) {
location.href = url + '/' + memberId + '/result/' + page;
}
function goTest(url, memberId, order, page) {
location.href = url + '/' + memberId + '/test/' + order + '/' + page;
}
function goResultInfo(resultId) {
console.log(resultId)
}
function goTestInfo(testId) {
console.log(testId)
}
여기서 아직 goResultInfo, goTestInfo 페이지는 생성되지 않은 상태라 번호만 잘 받아오는지 확인하기 위해 콘솔 출력을 했다.
다음 마이페이지를 return 하는 memberController 이다.
package com.timekiller.zzatool.member.control;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
@RequiredArgsConstructor
public class MemberController {
/* 마이페이지 이동 */
@GetMapping("/mypage/{memberId}")
public String mypage(@PathVariable("memberId") Long memberId, Model model) {
model.addAttribute("link", "/mypage");
model.addAttribute("memberId", memberId);
return "member/mypage";
}
}
마이페이지의 default 페이지는 회원 정보 페이지인데, 내가 맡은 기능이 아니기 때문에 틀만 구현해두었다.
이제 마이페이지의 틀은 대강 만들었으니, 마이페이지에 있는 내가 푼 테스트 조회와 내가 만든 테스트 조회 페이지를 구현할 수 있다.
☑️ myResult
html 코드이다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragment/base :: common_header (~{::title},~{::link})}">
<meta charset="UTF-8">
<title>짜툴 : 마이페이지</title>
<link rel="stylesheet" th:href="@{/css/member/mypage.css}"/>
<link rel="stylesheet" th:href="@{/css/member/myResult.css}"/>
</head>
<script th:src="@{/js/member/mypage.js}"></script>
<body>
<div th:insert="~{fragment/navigation :: copy}"></div>
<div class="mypage-bar">
내가 푼 테스트
</div>
<div class="mypage-content">
<div class="mypage-menu-bar">
<div class="mypage-menu" th:onclick="reload([[${link}]], [[${memberId}]])">
회원 정보 수정
</div>
<div class="mypage-menu" th:onclick="goResult([[${link}]], [[${memberId}]], '1')">
내가 푼 테스트
</div>
<div class="mypage-menu" th:onclick="goTest([[${link}]], [[${memberId}]], 'date', '1')">
내가 만든 테스트
</div>
<div class="mypage-menu">
회원 탈퇴
</div>
</div>
<div class="mypage-content-page">
<span class="card" th:each="result : ${results}">
<div class="card-body">
<h4 class="result-title" th:text="${result.testTitle}" th:title="${result.testTitle}"></h4>
<h5 class="result-score" th:text="${result.resultScore}+'점'"></h5>
<span class="result-date"
th:text="${#dates.format(result.resultDate, 'yyyy-MM-dd HH:mm')}"></span>
</div>
<div class="btn-area">
<button class="btn btn-primary" th:onclick="goResultInfo([[${result.resultId}]])">결과 다시 보기</button>
</div>
</span>
</div>
</div>
<nav aria-label="Page navigation example" id="page-button">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${isFirstPage}? 'disabled' : ''">
<a class="page-link"
th:onclick="goResult([[${link}]], [[${memberId}]], [[${page} - 5 - ${page}%5]])">이전</a>
</li>
<li class="page-item" th:each="pageIndex: ${#numbers.sequence(startPage, endPage)}">
<a class="page-link" th:classappend="${page} == ${pageIndex}? 'active' : ''"
th:onclick="goResult([[${link}]], [[${memberId}]], [[${pageIndex}]])"
th:text="${pageIndex}">0</a>
</li>
<li class="page-item" th:classappend="${isLastPage}? 'disabled' : ''">
<a class="page-link"
th:onclick="goResult([[${link}]], [[${memberId}]], [[${page} + 5 - ${page}%5]])">다음</a>
</li>
</ul>
</nav>
<footer>푸터 가져오기</footer>
</body>
</html>
왼쪽 탭 (마이페이지, 내가 푼 테스트 조회, 내가 만든 테스트 조회)은 마이페이지에서 그대로 가지고 올 수 있었다.
controller에서 model에 페이지 정보 및 테스트 데이터 정보를 attribute 해준 것을 토대로 테스트 목록을 출력했다.
css는 아래와 같다. mypage.css에 없는 부분을 따로 myResult.css를 추가하여 적용해주었다.
.card {
width: 20rem;
padding: 10px;
font-size: large;
display: inline-block;
margin-right: 30px;
margin-bottom: 20px;
}
.btn-area {
text-align: center;
}
.result-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#page-button {
margin-top: 30px;
}
js는 mypage.js (위에 있다) 에 다 넣고 함께 쓰기 때문에 생략한다.
마지막으로 Controller 파일이다.
package com.timekiller.zzatool.result.control;
import com.timekiller.zzatool.result.dto.ResultDTO;
import com.timekiller.zzatool.result.service.ResultService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Controller
@RequiredArgsConstructor
public class ResultController {
private static final int CONTENT_SIZE = 9;
private static final int PAGE_SIZE = 5;
private final ResultService resultService;
private int totalPage;
private long totalCount;
/* 내가 푼 테스트 조회 페이지 이동 */
@GetMapping("/mypage/{memberId}/result/{page}")
public String myResultList(
@PathVariable("memberId") Long memberId,
@PathVariable("page") Integer page,
Model model) {
Pageable pageable = PageRequest.of(page - 1, 9);
Page<ResultDTO> resultList = resultService.findResultListByMemberId(memberId, pageable);
this.totalPage = resultList.getTotalPages();
this.totalCount = resultList.getTotalElements();
model.addAttribute("link", "/mypage");
model.addAttribute("memberId", memberId);
model.addAttribute("page", page);
model.addAttribute("results", resultList.getContent());
model.addAttribute("startPage", 1);
model.addAttribute("isFirstPage", true);
int endPage = 5;
if (PAGE_SIZE * CONTENT_SIZE >= totalCount) {
endPage = (int) Math.ceil((double) totalCount / CONTENT_SIZE);
}
model.addAttribute("endPage", endPage);
boolean isLastPage = endPage == 1;
model.addAttribute("isLastPage", isLastPage);
return "member/myResult";
}
}
model을 활용하여 addAttribute 메소드로 프론트에 필요할 정보들을 주입시켜주었다.
또한 mapping도 mypage의 경로에서 뒤에 추가적으로 붙여주며 경로를 통일화시켜주었다.
☑️ myTest
내가 만든 테스트 조회 페이지의 html 코드이다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragment/base :: common_header (~{::title},~{::link})}">
<meta charset="UTF-8">
<title>짜툴 : 마이페이지</title>
<link rel="stylesheet" th:href="@{/css/member/mypage.css}"/>
<link rel="stylesheet" th:href="@{/css/member/myTest.css}"/>
</head>
<script th:src="@{/js/member/mypage.js}"></script>
<body>
<div th:insert="~{fragment/navigation :: copy}"></div>
<div class="mypage-bar">
내가 만든 테스트
</div>
<div class="mypage-content">
<div class="mypage-menu-bar">
<div class="mypage-menu" th:onclick="reload([[${link}]], [[${memberId}]])">
회원 정보 수정
</div>
<div class="mypage-menu" th:onclick="goResult([[${link}]], [[${memberId}]], '1')">
내가 푼 테스트
</div>
<div class="mypage-menu" th:onclick="goTest([[${link}]], [[${memberId}]], 'date', '1')">
내가 만든 테스트
</div>
<div class="mypage-menu">
회원 탈퇴
</div>
</div>
<div class="mypage-content-page">
<div class="sort-option">
<input autocomplete="off" class="btn-check" id="new" name="sort" th:checked="${order} == 'date'"
th:onclick="goTest([[${link}]], [[${memberId}]], 'date', '1')"
type="radio">
<label class="btn btn-outline-secondary" for="new">최신순</label>
<input autocomplete="off" class="btn-check" id="hot" name="sort" th:checked="${order} == 'count'"
th:onclick="goTest([[${link}]], [[${memberId}]], 'count', '1')" type="radio">
<label class="btn btn-outline-secondary" for="hot">인기순</label>
</div>
<span class="card" th:each="test : ${tests}">
<div class="card-body">
<h4 class="test-title" th:text="${test.testTitle}" th:title="${test.testTitle}"></h4>
<h5 class="test-count" th:text="'조회수 : '+${test.testCount}"></h5>
<span class="test-date"
th:text="${#dates.format(test.testDate, 'yyyy-MM-dd HH:mm')}"></span>
</div>
<div class="btn-area">
<button class="btn btn-primary" th:onclick="goTestInfo([[${test.testId}]])">테스트 보러 가기</button>
</div>
</span>
</div>
</div>
<nav aria-label="Page navigation example" id="page-button">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${isFirstPage}? 'disabled' : ''">
<a class="page-link"
th:onclick="goTest([[${link}]], [[${memberId}]], [[${order}]], [[${page} - 5 - ${page}%5]])">이전</a>
</li>
<li class="page-item" th:each="pageIndex: ${#numbers.sequence(startPage, endPage)}">
<a class="page-link" th:classappend="${page} == ${pageIndex}? 'active' : ''"
th:onclick="goTest([[${link}]], [[${memberId}]], [[${order}]], [[${pageIndex}]])"
th:text="${pageIndex}">0</a>
</li>
<li class="page-item" th:classappend="${isLastPage}? 'disabled' : ''">
<a class="page-link"
th:onclick="goTest([[${link}]], [[${memberId}]], [[${order}]], [[${page} + 5 - ${page}%5]])">다음</a>
</li>
</ul>
</nav>
<footer>푸터 가져오기</footer>
</body>
</html>
myResult 페이지와 비슷한 형식이다.
다른 점은 정렬 기능이 있다는 것인데, 최신순과 인기순으로 2개가 있다.
내가 만든 테스트를 최신순으로 혹은 인기순으로 확인할 수 있도록 구현했다.
css 파일은 myResult와 거의 같고, 명칭만 조금 다르다.
.sort-option {
margin-top: 10px;
margin-bottom: 10px;
}
.btn btn-secondary {
display: inline-block;
}
.card {
width: 20rem;
padding: 10px;
font-size: large;
display: inline-block;
margin-right: 30px;
margin-bottom: 20px;
}
.btn-area {
text-align: center;
}
.test-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#page-button {
margin-top: 30px;
}
추가적으로 정렬 옵션 부분의 margin을 조절해주었다.
마지막으로 controller 이다. TestController 중 내가 사용하는 코드만 따로 발췌해왔다.
package com.timekiller.zzatool.test.control;
import com.timekiller.zzatool.exception.RemoveException;
import com.timekiller.zzatool.test.dto.MyTestDTO;
import com.timekiller.zzatool.test.dto.TestCreateDTO;
import com.timekiller.zzatool.test.dto.TestDTO;
import com.timekiller.zzatool.test.service.TestService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class TestController {
private static final int MY_TEST_CONTENT_SIZE = 9;
private static final int PAGE_SIZE = 5;
private final TestService testService;
private int totalPage;
private long totalTestCount;
/* 내가 만든 테스트 조회 페이지 이동 */
@GetMapping("/mypage/{memberId}/test/{order}/{page}")
public String mytestList(
@PathVariable("memberId") Long memberId,
@PathVariable("order") String order,
@PathVariable("page") Integer page,
Model model)
throws Exception {
Pageable pageable = PageRequest.of(page - 1, 9);
try {
Page<MyTestDTO> testList =
testService.findTestListByMemberId(memberId, order, pageable);
this.totalPage = testList.getTotalPages();
this.totalTestCount = testList.getTotalElements();
model.addAttribute("tests", testList.getContent());
} catch (Exception e) {
throw new Exception(e.getMessage());
}
model.addAttribute("link", "/mypage");
model.addAttribute("memberId", memberId);
model.addAttribute("order", order);
model.addAttribute("page", page);
model.addAttribute("startPage", 1);
model.addAttribute("isFirstPage", true);
int endPage = 5;
if (PAGE_SIZE * MY_TEST_CONTENT_SIZE >= totalTestCount) {
endPage = (int) Math.ceil((double) totalTestCount / MY_TEST_CONTENT_SIZE);
}
model.addAttribute("endPage", endPage);
boolean isLastPage = endPage == 1;
model.addAttribute("isLastPage", isLastPage);
return "member/myTest";
}
}
myResult와 마찬가지로 model에 필요한 정보를 attribute 해준다. 경로도 마찬가지.
☑️ view
코드는 위와 같고 결과적으로 보여지는 화면 구성은 이렇다.
먼저 마이페이지.

설정한 title 대로 상단의 짜툴 : 마이페이지 를 확인할 수 있다.
아직 로그인 기능은 (다른 팀원이 맡은 기능) 구현되지 않은 상태였기 때문에 로그인 상태로 두었다.
아마 완성되면 저 부분에 로그인 버튼은 없을 것 같다.
또한 왼쪽에 탭을 두어서 마이페이지 내 다른 페이지로 이동할 수 있도록 했다.
여기서 내가 맡은 페이지는 내가 푼 테스트, 내가 만든 테스트 페이지이다.
다음은 내가 푼 테스트 페이지이다.

내가 푼 테스트를 클릭하면 위와 같이 페이지가 바뀐다.
경로 또한 mypage/1/result/1로 잘 설정되어 있는 것을 확인할 수 있다.
참고로 memberId=1, page=1인 경우이다.
아직 데이터가 부족해서 1페이지밖에 없다.
개발이 모두 끝나면 데이터를 여러개 추가하여 한 번 더 모두 테스트할 예정이다.
결과 다시 보기 버튼을 클릭하면 결과 페이지로 이동할 수 있는데 현재 구현되지 않은 상태라 콘솔 출력으로 확인만 할 수 있게 하였다.
마지막으로 내가 만든 테스트 페이지이다.

default는 최신순 정렬이다. 아직 데이터가 1개라 구분이 안 가지만 경로를 통해 확인할 수 있다.
현재 경로가 test/date/1이기 때문이다. 만약 여기서 인기순을 클릭한다면 아래 페이지로 이동한다.

같은 화면 같지만 경로를 확인하면 test/count/1이다. 즉, 정렬 기준이 달라졌다는 것을 확인할 수 있다.
테스트 보러 가기 버튼을 클릭하면 테스트 조회 페이지로 이동한다. 아직 구현이 되지 않은 상태이기 때문에 마찬가지로 콘솔 출력만 했다.
마치며 ...
html, javascript, css 등을 이미 여러 번 실습을 해 본 상태이고, 프로젝트 진행에 활용한 경험이 있었다.
그래서 thymeleaf를 처음 사용해보지만 쉽게 구현할 수 있었다. 기본적으로 th: 를 붙여 사용하면 대체로 비슷했기 때문이다.
다음은 테스트 조회 페이지를 만들 예정이다.
'🐾 개발' 카테고리의 다른 글
| 프로젝트 : 짜툴 (5) 테스트 조회 페이지 (0) | 2024.04.11 |
|---|---|
| 프로젝트 : 데메즈 (1) 240403 ~ 240405 (0) | 2024.04.07 |
| 프로젝트 : 짜툴 (3) 240330 ~ 240331 (2) | 2024.04.02 |
| 프로젝트 : 짜툴 (2) 240320 ~ 240325 (0) | 2024.03.31 |
| [MacOS M2] MySQL 설치하기 (feat. homebrew) (0) | 2024.03.20 |