본문 바로가기

🐾 개발

프로젝트 : 짜툴 (4) 마이페이지


📌  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: 를 붙여 사용하면 대체로 비슷했기 때문이다.

다음은 테스트 조회 페이지를 만들 예정이다.