본문 바로가기
스프링과 JPA 기반 웹 어플리케이션 개발/1부 (개발환경, 회원가입, 로그인, 계정설정)

25. 프로필 이미지 변경

by Backchus 2020. 4. 21.

아바타 이미지 잘라서 저장하기

 

프론트 라이브러리 설치

  • Cropper.js
  • npm install cropper
  • npm install jquery-cropper

Cropper.js 사용하기

 

Profile DTO에 이미지 경로를 위해 필드 추가

package me.weekbelt.studyolle.settings;

@NoArgsConstructor
@Data
public class Profile {

    // 기존코드 ........

    private String profileImage;

    public Profile(Account account) {
        // 기존코드 ............
        this.profileImage = account.getProfileImage();
    }
}

 

/settings/profile.html에 이미지 폼 추가

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<!--head-->
<head th:replace="fragments.html::head"></head>

<body class="bg-light">
<!--네비게이션 바-->
<div th:replace="fragments.html::main-nav"></div>

<div class="container">
    <div class="row mt-5 justify-content-center">
        <div class="col-2">
            <div th:replace="fragments.html :: settings-menu(currentMenu='profile')"></div>
        </div>
        <div class="col-8">
            <div th:if="${message}" class="alert alert-info alert-dismissible fade show mt-3" role="alert">
                <span th:text="${message}">메시지</span>
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">x</span>
                </button>
            </div>
            <div class="row">
                <h2 class="col-sm-12" th:text="${account.nickname}">weekbelt</h2>
            </div>
            <div class="row mt-3">
                <form action="#" class="col-sm-6"
                      th:action="@{/settings/profile}" th:object="${profile}" method="post" novalidate>
                    
                    // 기존 코드 .........

                    // 추가
                    <div class="form-group">
                        <input id="profileImage" type="hidden" th:field="*{profileImage}" class="form-control"/>
                    </div>

                    // 기존 코드 .........
                </form>
                // 추가
                <div class="col-sm-6">
                    <div class="card text-center">
                        <div class="card-header">
                            프로필 이미지
                        </div>
                        <div id="current-profile-image" class="mt-3">
                            <svg th:if="${#strings.isEmpty(profile.profileImage)}" class="rounded"
                                 th:data-jdenticon-value="${account.nickname}" width="125" height="125"></svg>
                            <img th:if="${!#strings.isEmpty(profile.profileImage)}" class="rounded"
                                 th:src="${profile.profileImage}" width="125" height="125" alt="name"
                                 th:alt="${account.nickname}"/>
                        </div>
                        <div id="new-profile-image" class="mt-3"></div>
                        <div class="card-body">
                            <div class="custom-file">
                                <input type="file" class="custom-file-input" id="profile-image-file">
                                <label class="custom-file-label" for="profile-image-file">프로필 이미지 변경</label>
                            </div>
                            <div id="new-profile-image-control" class="mt-3">
                                <button class="btn btn-outline-primary btn-block" id="cut-button">자르기</button>
                                <button class="btn btn-outline-success btn-block" id="confirm-button">확인</button>
                                <button class="btn btn-outline-warning btn-block" id="reset-button">취소</button>
                            </div>
                            <div id="cropped-new-profile-image" class="mt-3"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- footer -->
    <div th:replace="fragments.html::footer"></div>
</div>
// cropper 관련 링크 추가
<link href="/node_modules/cropper/dist/cropper.min.css" rel="stylesheet">
<script src="/node_modules/cropper/dist/cropper.min.js"></script>
<script src="/node_modules/jquery-cropper/dist/jquery-cropper.min.js"></script>

</body>
</html>

 

profile.html에 js로직 추가

<script type="application/javascript">
    $(function() {
        cropper = '';
        let $confirmBtn = $('#confirm-button');
        let $resetBtn = $("#reset-button");
        let $cutBtn = $('#cut-button');
        let $newProfileImage = $('#new-profile-image');
        let $currentProfileImage = $("#current-profile-image");
        let $resultImage = $('#cropped-new-profile-image');
        let $profileImage = $('#profileImage');

        // 초기 버튼과 새로운 프로필 이미지 숨기기
        $newProfileImage.hide();
        $cutBtn.hide();
        $resetBtn.hide();
        $confirmBtn.hide();

        $("#profile-image-file").change(function (e) {      // 프로필 사진이 바뀔때
            if (e.target.files.length === 1) {
                const reader = new FileReader();
                reader.onload = e => {
                    if(e.target.result) {                           // 파일을 가져왔을 경우
                        // 이미지태그 만들기
                        let img = document.createElement("img");
                        img.id = 'new-profile';
                        img.src = e.target.result;
                        img.width = 250;

                        $newProfileImage.html(img);     // newProfileImage영역에 위에서 만든 이미지 태그 삽입
                        $newProfileImage.show();
                        $currentProfileImage.hide();

                        // Cropper 적용
                        let $newImage = $(img);
                        $newImage.cropper({aspectRatio: 1});
                        cropper = $newImage.data('cropper');

                        $cutBtn.show();
                        $confirmBtn.hide();
                        $resetBtn.show();
                    }
                };

                reader.readAsDataURL(e.target.files[0]);
            }
        });

        $resetBtn.click(function () {
            $currentProfileImage.show();
            $newProfileImage.hide();
            $resultImage.hide();
            $resetBtn.hide();
            $cutBtn.hide();
            $confirmBtn.hide();
            $profileImage.val('');
        });

        $cutBtn.click(function () {
            let dataUrl = cropper.getCroppedCanvas().toDataURL();
            let newImage = document.createElement("img");
            newImage.id = "cropped-new-profile-image";
            newImage.src = dataUrl;
            newImage.width = 125;
            $resultImage.html(newImage);
            $resultImage.show();
            $confirmBtn.show();

            $confirmBtn.click(function() {
               $newProfileImage.html(newImage);
               $cutBtn.hide();
               $confirmBtn.hide();
               $profileImage.val(dataUrl);
            });
        });
    });
</script>

 

AccountService의 updateProfile메소드에서 이미지 업데이트 처리

package me.weekbelt.studyolle.account;

@Transactional
@RequiredArgsConstructor
@Service
public class AccountService implements UserDetailsService {

    // 기존 코드 ......

    public void updateProfile(Account account, Profile profile) {
        
        // 기존 코드 ....
        
        account.setProfileImage(profile.getProfileImage());    // 추가
        accountRepository.save(account);
    }
}

 

네비게이션 바에서 보이는 이미지도 바꿔주기 위해 fragment.html에서 nav태그 수정

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

		// 기존 코드 .......

        <ul class="navbar-nav justify-content-end">
            <li class="nav-item" sec:authorize="!isAuthenticated()">
                <a class="nav-link" th:href="@{/login}">로그인</a>
            </li>
            <li class="nav-item" sec:authorize="!isAuthenticated()">
                <a class="nav-link" th:href="@{/sign-up}">가입</a>
            </li>
            <li class="nav-item" sec:authorize="isAuthenticated()">
                <a class="nav-link" th:href="@{/notifications}">
                    <i class="fa fa-bell-o" aria-hidden="true"></i>
                </a>
            </li>
            <li class="nav-item" sec:authorize="isAuthenticated()">
                <a class="nav-link btn btn-outline-primary" th:href="@{/notifications}">
                    <i class="fa fa-plus" aria-hidden="true"></i>스터디 개설
                </a>
            </li>
            <li class="nav-item dropdown" sec:authorize="isAuthenticated()">
                <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown"
                   aria-haspopup="true" aria-expanded="false">
                   // 추가
                    <svg th:if="${#strings.isEmpty(account?.profileImage)}"
                         th:data-jdenticon-value="${#authentication.name}"
                         width="24" height="24" class="rounded border bg-light"></svg>
                         // 추가
                    <img th:if="${!#strings.isEmpty(account?.profileImage)}" th:src="${account.profileImage}"
                         width="24" height="24" class="rounded border">
                </a>
                <div class="dropdown-menu dropdown-menu-sm-right" aria-labelledby="userDropdown">
                    <h6 class="dropdown-header">
                        <span sec:authentication="name">Username</span>
                    </h6>
                    <a class="dropdown-item" th:href="@{'/profile/' + ${#authentication.name}}">프로필</a>
                    <a class="dropdown-item">스터디</a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" th:href="@{'/settings/profile'}">설정</a>
                    <form class="form-inline my-2 my-lg-0" th:action="@{/logout}" method="post">
                        <button class="dropdown-item" type="submit">로그아웃</button>
                    </form>
                </div>
            </li>
        </ul>
    </div>
</nav>

// 기존 코드 .......

 

DataURL이란?

  • data: 라는 접두어를 가진 URL로 파일을 문서에 내장 시킬때 사용할 수 있다.
  • 이미지를 DataURL로 저장할 수 있다.

 

 

참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1#

 

스프링과 JPA 기반 웹 애플리케이션 개발 - 인프런

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습니다. 이 강좌를 충분히 학습한다면 여러분 만의 웹 서비스를 만들거나 취직에 도움이 될만한 포트폴리오를 만들 수 있을 겁니다. 활용 웹 개발 프레임워크 및 라이브러리 Java Spring Spring Boot Spring Data JPA Thymeleaf 온라인 강의 스

www.inflearn.com