본문 바로가기

Server Programming/Spring Boot Full-Stack Programming

[스프링 풀스택 클론 코딩 - 계정 설정] (2-2) 프로필 수정 처리

반응형

SettingsController

package com.demo.settings;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.demo.account.AccountService;
import com.demo.account.CurrentUser;
import com.demo.domain.Account;

import lombok.RequiredArgsConstructor;

//프로필 수정 요청 처리
@Controller
@RequiredArgsConstructor
public class SettingsController {

	// 자주 틀리는 오타는 변수를 만들어서 사용
	private static final String SETTINGS_PROFILE_VIEW_NAME = "settings/profile";
	private static final String SETTINGS_PROFILE_URL = "/settings/profile";

	private final AccountService accountService;

	// Get으로 전달하면, 업데이트 폼 리턴
	@GetMapping(SETTINGS_PROFILE_URL)
	// 자신의 정보만 수정가능하므로, 현재 자기 자신 정보와 모델정보를 담을 인스턴스를 파라미터로
	public String profileUpdateForm(@CurrentUser Account account, Model model) {
		model.addAttribute(account);
		// 화면에 계정정보를 넣어주고
		model.addAttribute(new Profile(account));
		// 폼에 사용할 객체를 만들어서 정보를 넣어준다. -> 계정정보를 조회해 채운다.

		return SETTINGS_PROFILE_VIEW_NAME;
	}
	// return은 void로 메서드를 생성하면 생략가능 한데, 뷰네임트랜스레이터가 url네임과 같다고 추측

	// Post로 전달하면, 프로필을 업데이트 처리 -> @Valid (@ModelAttribute 생략) 이용해서 묶고, 검증을 하므로,
	// 검증시엔 바인딩 에러를 담는 변수도 필요하다.
	@PostMapping("/settings/profile")
	public String updatePorfile(@CurrentUser Account account, @Valid @ModelAttribute Profile profile, Errors errors,
			Model model) {
		if (errors.hasErrors()) {

			// 폼의 데이터는 모델에 자동으로 들어가고, 에러 정보도 모델에 들어간다.
			// 계정 정보는 직접 넣어야 한다.
			model.addAttribute(account);

			return SETTINGS_PROFILE_VIEW_NAME;

		}
		// 에러가 없는 경우 변경 진행 -> 서비스에서 처리 -> final만 의존성 주입해주는 @RequiredArgsConstructor 이용
		// 수정하고자 하는 계정 정보를 프로필로 전달해서 변경 진행
		accountService.updateProfile(account, profile);

		// 리프레시하더라도 폼 서브밋이 반복되지 않도록 리다이렉트
		return "redirect:" + SETTINGS_PROFILE_URL;
	}
	// 기본생성자가 없이 프로필 메서드가 생성되는데, Profile 인스턴스 생성할 때, account가 없기 때문에 NullPointer
	// Exception 발생

	//
	// 영속성 컨텍스트에서 관리하는 객체가 되면, DB에 적용이 된다.
	// 엔터티의 상태
	// 1. Transient: 객체를 생성하고, 값을 주어도 JPA나 hibernate가 그 객체에 관해 아무것도 모르는 상태. 즉,
	// 데이터베이스와 매핑된 것이 아무것도 없다.
	// 2. Persistent: 저장을 하고나서, JPA가 아는 상태(관리하는 상태)가 된다. 그러나 .save()를 했다고 해서, 이 순간
	// 바로 DB에 이 객체에 대한 데이터가 들어가는 것은 아니다. JPA가 persistent 상태로 관리하고 있다가, 후에 데이터를
	// 저장한다.(1차 캐시, Dirty Checking(변경사항 감지), Write Behind(최대한 늦게, 필요한 시점에 DB에 적용) 등의
	// 기능을 제공한다)
	// 3. Detached: JPA가 더이상 관리하지 않는 상태. JPA가 제공해주는 기능들을 사용하고 싶다면, 다시 persistent 상태로
	// 돌아가야한다.
	// 4. Removed: JPA가 관리하는 상태이긴 하지만, 실제 commit이 일어날 때, 삭제가 일어난다.

	// 하지만, 여기서 사용한 account 객체는 영속상태가 아니므로, 트랜잭션이 끝난 principal 객체 정보이기 때문에,
	// detached상태이므로 DB에 적용이 안된다.
	// 직접 저장을 하면 된다. -> 왜냐하면 id값을 가지고 있기 때문에

}

 

Profile

package com.demo.settings;

import com.demo.domain.Account;

import lombok.Data;
import lombok.NoArgsConstructor;

//폼을 채울 객체 생성
//롬복을 이용한 getter, setter 자동생성
@Data
//참조받을 account를 위해 기본 생성자를 만들어주는 어노테이션은 사용한다
@NoArgsConstructor
public class Profile {
	private String bio;
	private String url;
	private String occupation;
	private String location;
	
	//@NoArgsConstructor를 대신해 사용한다면
	//public Profile(){};
	
	//프로필 폼에 채울 객체에 정보 넣기 -> ModelMapper를 이용해 생성
	public Profile(Account account) {
		this.bio=account.getBio();
		this.url=account.getUrl();
		this.occupation=account.getOccupation();
		this.location=account.getLocation();
	}


}

 

AccountService의 updateProfile 메서드

	public void updateProfile(Account account, Profile profile) {
		// TODO Auto-generated method stub
		account.setUrl(profile.getUrl());
		account.setOccupation(profile.getOccupation());
		account.setLocation(profile.getLocation());
		account.setBio(profile.getBio());
		
		//트랜잭션이 끝난 detached상태이기 때문에 직접 DB에 반영을 해준다. 
		//즉 id값이 있으면 merge를 수행한다.
		accountRepository.save(account);
	}

 

 


 

fragments의 메뉴 부분

<!-- 메뉴 -->
<div th:fragment="study-settings-menu (currentMenu)" class="list-group">
<!-- currentMenu에 따라 활성화할 메뉴 보여줌 -->
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'description'}? active"
       href="#" th:href="@{'/study/' + ${study.path} + '/settings/description'}">소개</a>
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'image'}? active"
       href="#" th:href="@{'/study/' + ${study.path} + '/settings/banner'}">배너 이미지</a>
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'tags'}? active"
       href="#" th:href="@{'/study/' + ${study.path} + '/settings/tags'}">스터디 주제</a>
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'zones'}? active"
       href="#" th:href="@{'/study/' + ${study.path} + '/settings/zones'}">활동 지역</a>
    <a class="list-group-item list-group-item-action list-group-item-danger" th:classappend="${currentMenu == 'study'}? active"
       href="#" th:href="@{'/study/' + ${study.path} + '/settings/study'}">스터디</a>
</div>
<div th:fragment="settings-menu (currentMenu)" class="list-group">
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'profile'}? active" href="#" th:href="@{/settings/profile}">프로필</a>
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'password'}? active" href="#" th:href="@{/settings/password}">패스워드</a>
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'notifications'}? active" href="#" th:href="@{/settings/notifications}">알림</a>
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'tags'}? active" href="#" th:href="@{/settings/tags}">관심 주제</a>
    <a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'zones'}? active" href="#" th:href="@{/settings/zones}">활동 지역</a>
    <a class="list-group-item list-group-item-action list-group-item-danger" th:classappend="${currentMenu == 'account'}? active" href="#" th:href="@{/settings/account}">계정</a>
</div>

 

profile

<!DOCTYPE html>
<!-- 프로필 수정 뷰 -> profile form을 만들어야 한다. 닉네임 이메일 비밀번호-->
<!-- 타임리프 네임스페이스 설정-->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<!-- replace를 이용해 교체-->

<head th:replace="fragments.html :: head">
</head>

<body class="bg-light">
	<div th:replace="fragments.html :: main-nav"></div>

	<!-- profile html -> 그리드 시스템 이용 row를 column 12개로 나눈 상태-->
	<div class="container">
		<!-- top 마진 5만큼-->
		<div class="row mt-5 justify-content-center">
			<!-- 아바타-->
			<div class="col-2">
				<!--프로필, 패스워드, 알림, 관심주제, 활동지역 계정 메뉴 -> fragment 이용-->
				<!-- profile이 선택된 상태의 뷰 -->

				<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">&times;</span>
					</button>
				</div>
				<!-- row마다 설정 -->
				<div class="row">
					<h2 class="col-sm-12" th:text="${account.nickname}">whiteship</h2>
				</div>

				<!-- 12중에 6/6으로 나눠서 프로필 이미지 수정 폼 만들기-->
				<!--  <div class="row  mt-3" th:fragment="profile-form">-->

				<div class="row  mt-3">
					<form class="col-sm-6" action="#"
                          th:action="@{/settings/profile}" th:object="${profile}" method="post" novalidate>
                        <div class="form-group">
                            <label for="bio">한 줄 소개</label>
                            <input id="bio" type="text" th:field="*{bio}" class="form-control"
                                   placeholder="간략한 소개를 부탁합니다." aria-describedby="bioHelp" required>
                            <small id="bioHelp" class="form-text text-muted">
                                길지 않게 35자 이내로 입력하세요.
                            </small>
                            <small class="form-text text-danger" th:if="${#fields.hasErrors('bio')}" th:errors="*{bio}">
                                조금 길어요.
                            </small>
                        </div>

                        <div class="form-group">
                            <label for="url">링크</label>
                            <input id="url" type="url" th:field="*{url}" class="form-control"
                                   placeholder="http://studyolle.com" aria-describedby="urlHelp" required>
                            <small id="urlHelp" class="form-text text-muted">
                                블로그, 유튜브 또는 포트폴리오나 좋아하는 웹 사이트 등 본인을 표현할 수 있는 링크를 추가하세요.
                            </small>
                            <small class="form-text text-danger" th:if="${#fields.hasErrors('url')}" th:errors="*{url}">
                                옳바른 URL이 아닙니다. 예시처럼 입력해 주세요.
                            </small>
                        </div>

                        <div class="form-group">
                            <label for="company">직업</label>
                            <input id="company" type="text" th:field="*{occupation}" class="form-control"
                                   placeholder="어떤 일을 하고 계신가요?" aria-describedby="occupationHelp" required>
                            <small id="occupationHelp" class="form-text text-muted">
                                개발자? 매니저? 취준생? 대표님?
                            </small>
                        </div>

                        <div class="form-group">
                            <label for="location">활동 지역</label>
                            <input id="location" type="text" th:field="*{location}" class="form-control"
                                   placeholder="Redmond, WA, USA"
                                   aria-describedby="locationdHelp" required>
                            <small id="locationdHelp" class="form-text text-muted">
                                주요 활동(사는 곳이나 직장을 다니는 곳 또는 놀러 다니는 곳) 지역의 도시 이름을 알려주세요.
                            </small>
                        </div>

                        <div class="form-group">
                            <button class="btn btn-primary btn-block" type="submit"
                                    aria-describedby="submitHelp">수정하기</button>
                        </div>
                    </form>

				</div>
			</div>
		</div>
	</div>
	    <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>
반응형