프로젝트 중간 점검, 서비스 View 에서 확인하기

JungWooLee·2022년 8월 28일
0

SpringBoot ToyProject

목록 보기
10/14

이어서 하기

  • 기존 인덱스에서 모든 것을 처리하던 것을 세가지 뷰로 나누어 정리
    • 인덱스 - 로그인 영역
    • 메인 - 유저 현재 위치 설정 영역
    • 레스토랑 - 유저 위치기반 레스토랑 설정 영역
  • OAuth 로그인 이후 사용자 위치 설정 값을 SessionUser 에 저장하여 세션으로 관리

1. 뷰 나누기

1. 인덱스

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <!--    <meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>-->
    <!--    <meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>-->
    <title>Title</title>
<!--    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>-->
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>

<h1>OAuth Test 중</h1>
<div class="col-md-12">
    <!--    로그인 기능 영역 -->
    <div class="row">
        <div class="col-md-6">
            <th:block th:if="${userName != null}">
                Logged in as: <span id="user" th:text="${userName}"></span>
                <a href="/logout" class="btn btn-info active" role="button">Logout</a>
            </th:block>
            <th:block th:unless="${userName != null}">
                <a href="/oauth2/authorization/google" class="btn btn-success active"
                   role="button">Google Login</a>
                <a href="/oauth2/authorization/naver" class="btn btn-secondary active"
                   role="button">Naver Login</a>
            </th:block>
        </div>
    </div>
</div>
<div id="myLocationInfo"></div>
<div id="fullAddress"></div>
</body>
</html>

인덱스는 로그인만 수행할 수 있도록 수정을 거쳤습니다

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration //시큐리티 활성화 -> 기본 스프링 필터 체인에 등록
public class SecurityConfig{
    @Autowired
    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .authorizeRequests()
                .antMatchers("/","/css/**","/images/**",
                        "/js/**","h2-console/**","/user/api/**").permitAll() // 해당 url을 가진 경우 모두 허용
                .antMatchers("/api/v1/**").hasRole(Role.USER.name())
                .anyRequest().authenticated()
                .and()
                .logout()
                .logoutSuccessUrl("/")
                .and()
                .oauth2Login()
                .defaultSuccessUrl("/main")  // 로그인 성공시 url
//                .failureUrl("/") // 로그인 실패시 url
                .userInfoEndpoint()
                .userService(customOAuth2UserService);
        return http.build();
    }
}
  • 기존 OAuth 로그인 설정에서 로그인 성공시 url 을 main으로 향하도록 합니다
  • 만약 사용자가 유저 권한이 없음에도 메인으로 접근한다면 /login 으로 향하도록 합니다

2. 메인

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script type="text/javascript"
            src="//dapi.kakao.com/v2/maps/sdk.js?appkey=f4c09d486f1f49e48a1490b5808b62b2"></script>
    <script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
    <title>Main</title>
</head>
<body>
<form th:action="@{/user/postXY}"  method="post">
    위도
    <input type="text"  id="x" name="x" readonly/>
    <br>
    경도
    <input type="text"  id="y" name="y" readonly/>
    <button type="submit">등록</button>
</form>
<div id="map" style="width:500px;height:400px;"></div>
<br>
<h2>나의 위치 정보</h2>
<div id="myLocationInfo">좌표값 오류 X</div>
<div>현재 주소 :<span id="fullAddress" ></span></div>
<div id="clickLatlng"></div>

<div>
    주소 입력하기 : <input type="text" id="findAddress">
    <button type="submit" onclick="toXY()">주소 반영하기</button>
</div>

</body>
<script>
    function toXY(){
        var address = $("#findAddress").val();
        console.log("current address : "+address);
        if(address.length > 0){
            // 만약 주소값이 입력되었다면 ajax호출

            $.ajax({
                url: "/user/api/getLonLat",
                data: {fullAddress : address},
                type: "POST",
            }).done(function (fragment) {

                console.log('x: '+fragment.x);
                console.log('y: '+fragment.y);

                // x,y, 현재 위치 바꿔주기
                x.value = fragment.x;
                y.value = fragment.y;
                $("#fullAddress").text(address);

                createKaKaoMap(fragment.y, fragment.x);
            });
        }
    }

    window.onload = function () {
        document.getElementById("findAddress").addEventListener("click", function () { //주소입력칸을 클릭하면
            //카카오 지도 발생
            new daum.Postcode({
                oncomplete: function (data) { //선택시 입력값 세팅
                    document.getElementById("findAddress").value = data.address; // 주소 넣기
                    document.getElementById("y").focus(); // 좌표값 변경 후 닫기 및 focus 이동
                }
            }).open();
        });
    }

    /* 비동기적으로 현재 위치를 알아내어 지정된 요소에 출력한다. */
    function whereami(elt) {
        // 이 객체를 getCurrentPosition() 메서드의 세번째 인자로 전달한다.
        var options = {
            // 가능한 경우, 높은 정확도의 위치(예를 들어, GPS 등) 를 읽어오려면 true로 설정
            // 그러나 이 기능은 배터리 지속 시간에 영향을 미친다.
            enableHighAccuracy: false, // 대략적인 값이라도 상관 없음: 기본값

            // 위치 정보가 충분히 캐시되었으면, 이 프로퍼티를 설정하자,
            // 위치 정보를 강제로 재확인하기 위해 사용하기도 하는 이 값의 기본 값은 0이다.
            maximumAge: 30000,     // 5분이 지나기 전까지는 수정되지 않아도 됨

            // 위치 정보를 받기 위해 얼마나 오랫동안 대기할 것인가?
            // 기본값은 Infinity이므로 getCurrentPosition()은 무한정 대기한다.
            timeout: 15000    // 15초 이상 기다리지 않는다.
        }

        if (navigator.geolocation) // geolocation 을 지원한다면 위치를 요청한다.
            navigator.geolocation.getCurrentPosition(success, error, options);
        else
            elt.innerHTML = "이 브라우저에서는 Geolocation이 지원되지 않습니다.";


        // geolocation 요청이 실패하면 이 함수를 호출한다.
        function error(e) {
            // 오류 객체에는 수치 코드와 텍스트 메시지가 존재한다.
            // 코드 값은 다음과 같다.
            // 1: 사용자가 위치 정보를 공유 권한을 제공하지 않음.
            // 2: 브라우저가 위치를 가져올 수 없음.
            // 3: 타임아웃이 발생됨.
            elt.innerHTML = "Geolocation 오류 " + e.code + ": " + e.message;
        }


        // geolocation 요청이 성공하면 이 함수가 호출된다.
        function success(pos) {

            console.log(pos); // [디버깅] Position 객체 내용 확인

            // 항상 가져올 수 있는 필드들이다. timestamp는 coords 객체 내부에 있지 않고,
            // 외부에서 가져오는 필드라는 점에 주의하다.
            x.value = pos.coords.longitude;
            y.value = pos.coords.latitude;

            createKaKaoMap(pos.coords.latitude,pos.coords.longitude);
            getFullAddress(pos.coords.longitude, pos.coords.latitude);
        }
    }

    function createKaKaoMap(lat, lon){
        var container = document.getElementById('map'); //지도를 담을 영역의 DOM 레퍼런스
        var options = { //지도를 생성할 때 필요한 기본 옵션
            center: new kakao.maps.LatLng(lat, lon), //지도의 중심좌표.
            level: 3 //지도의 레벨(확대, 축소 정도)
        };

        var map = new kakao.maps.Map(container, options); //지도 생성 및 객체 리턴

        // 지도를 클릭한 위치에 표출할 마커입니다
        var marker = new kakao.maps.Marker({
            // 지도 중심좌표에 마커를 생성합니다
            position: map.getCenter()
        });
        // 지도에 마커를 표시합니다
        marker.setMap(map);

        kakao.maps.event.addListener(map, 'click', function (mouseEvent) {

            // 클릭한 위도, 경도 정보를 가져옵니다
            var latlng = mouseEvent.latLng;

            // 마커 위치를 클릭한 위치로 옮깁니다
            marker.setPosition(latlng);

            // 마커 이동후 클릭한 위치에 맞게 위도, 경도 설정
            x.value = latlng.getLng();
            y.value = latlng.getLat();
            var message = '클릭한 위치의 위도는 ' + latlng.getLat() + ' 이고, ';
            message += '경도는 ' + latlng.getLng() + ' 입니다';

            var resultDiv = document.getElementById('clickLatlng');
            resultDiv.innerHTML = message;
            getFullAddress(latlng.getLng(), latlng.getLat());
        });
    }

    // 지도에 클릭 이벤트를 등록합니다
    // 지도를 클릭하면 마지막 파라미터로 넘어온 함수를 호출합니다
    function getFullAddress(longtitude, latitude) {
        var AddressRequest = {
            x: longtitude,
            y: latitude
        };

        $.ajax({
            url: "/user/api/getFullAddress",
            data: AddressRequest,
            type: "POST",
        }).done(function (fragment) {
            console.log(fragment);
            $("#fullAddress").text(fragment);
        });
    }

    // 나의 위치정보를 출력할 객체 구하기
    var elt = document.getElementById("myLocationInfo");
    var x = document.getElementById("x");
    var y = document.getElementById("y");

    // 나의 위치정보 출력하기
    whereami(elt);
</script>
</html>

메인의 경우 추가된 사항은 역지오코딩과 지오코딩이 될것입니다

KaKao 지도 API 사용

  • 주소를 좌표로 변환하는 지오코딩 navigator.geolocation 을 이용하여 디폴트 좌표값을 찾아옵니다
  • 좌표값에 따라 지도와 마커를 생성하여 마커를 이동시 이에따라 좌표값또한 수정 될 수 있도록 합니다
  • 좌표값에 따른 역지오코딩을 api를 사용하여 호출하고 이값을 화면에 표시해줍니다 - 역지오코딩
  • 다음 지도 api 를 사용하여 필요에 따라 주소값 검색을 하여 정확한 주소 변경을 지원합니다
 // 주소를 통해 위경도 반환
    @PostMapping("/getLonLat")
    public AddressDTO getLonLat(@RequestParam(value ="fullAddress") String fullAddress){
        log.info("full address : "+fullAddress);
        return userService.getXY(fullAddress);
    }

2. 세션 정보 바꾸기

기존 세션 저장 형태

@Getter
public class SessionUser implements Serializable {
    private String name;
    private String email;
    private String picture;

    // oauth 에서 지정해준 session user 에서 로그인 완료 후 x,y 값을 받아 올 수 있도록 함
    @Setter
    private Double x;
    @Setter
    private Double y;

    public SessionUser(User user){
        this.name = user.getName();
        this.email = user.getEmail();
        this.picture = user.getPicture();
        this.x = user.getX();
        this.y = user.getY();
    }
}
  • 세션에 "user" 를 통하여 로그인 시 유저 정보를 세션에 직렬화하여 저장하고 있었습니다
@RequiredArgsConstructor
@Service
@Slf4j
public class CustomOAuth2UserServiceImpl implements CustomOAuth2UserService {
    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {
        OAuth2UserService<OAuth2UserRequest,OAuth2User> delegate
                = new DefaultOAuth2UserService();
        log.info("userRequest: "+userRequest);
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId = userRequest.
                getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.
                getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint()
                .getUserNameAttributeName();

        OAuthAttributes attributes = OAuthAttributes.
                of(registrationId,userNameAttributeName,
                        oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes); // 만약 존재하는 id, 즉 email이라면 update, else save

        httpSession.setAttribute("user", new SessionUser(user));


        return new DefaultOAuth2User(
                Collections.singleton(new
                        SimpleGrantedAuthority(user.getRoleKey())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey());

    }

    @Override
    public User saveOrUpdate(OAuthAttributes attributes) {
        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(),
                        attributes.getPicture()))
                .orElse(attributes.toEntity());

        return userRepository.save(user);
    }
}
  • 만약 유저 정보가 없다면 신규 등록 / 있다면 이름과 사진을 업데이트 하는 형식으로 SessionUser 를 관리 하였습니다

위치 등록 이후에는 ?

  • 사용자의 위치 등록은 선택이 아닌 필수입니다. 도메인의 특성상 주변 맛집을 알 수 있도록 하는 것이 목적이기 때문에 위치 등록 이후에는 해당 좌표를 세션에 담고 하나의 유저 info 로서 관리하는 것이 올바르다 판단하였습니다
  • 단순히 직렬화 되어있는 객체를 deserialize 하여 set 하는 것은 의미가 없습니다. User 엔티티에 Default로 사용자의 위,경도 값이 들어가져 있지 않은데 세팅 이후 Repository에도 반영하여야 하기 때문에 이에 관련된 서비스를 생성합니다

Controller

	@PostMapping("/postXY")
    public String postXY(AddressDTO dto
            , @LoginUser SessionUser user){
        log.info("inside postXY");
        log.info(user.getEmail());
        log.info(user.getName());
        user.setX(dto.getX());
        user.setY(dto.getY());
        User newUser = userService.saveOrUpdateXY(user);
        httpSession.setAttribute("user", new SessionUser(newUser));

        return "restaurant";
    }
  • 주소값 x,y 를 세트로 많이 사용하게 되면서 아에 DTO로 따로 작성하였습니다
  • 로깅을 통해 컨트롤러안에 잘 들어왔는지 확인

Service

	@Override
    public User saveOrUpdateXY(SessionUser sessionUser) {
        log.info("save xy email : "+sessionUser.getEmail());
        log.info(sessionUser.getX()+"");
        log.info(sessionUser.getY()+"");

        User user = userRepository.findByEmail(sessionUser.getEmail())
                .map(entity -> entity.updateXY(sessionUser.getX(),
                        sessionUser.getY()))
                .orElse(User.userXY()
                        .sessionUser(sessionUser)
                        .build());
        log.info("new User : "+user);
        return userRepository.save(user);
    }
  • SessionUser를 매개변수로 받아와 해당 아이디를 찾고 x,y 값을 업데이트 합니다
  • 기존 Optional 로 User를 받아왔오는 JPA 를 사용하였기 때문에 이를 재사용하여 해당 유저가 없을시 (물론 그런 경우는 없겠지만), SessionUser를 통해 User 객체를 생성해주었습니다
  • 유저에서 따로 SessionUser 를 통한 빌더를 생성해주었습니다
 	@Builder(builderMethodName = "userXY")
    public User(SessionUser sessionUser){
        this.name = sessionUser.getName();
        this.email = sessionUser.getEmail();
        this.picture = sessionUser.getPicture();
        this.role = Role.USER;
        this.x = sessionUser.getX();
        this.y = sessionUser.getY();
    }

🙋‍♂️왜 USER Role? : 로그인이 완료된 상태이기 때문에 Role은 유저로 통일합니다

여기까지 프로세스를 로깅을 통해 확인합니다

[2022-08-28 23:06:08:19141] INFO  27832 --- [nio-8080-exec-9] c.a.l.controller.ApiController           : AddressDTO(x=127.02713245190886, y=37.56867906617772)
[2022-08-28 23:06:10:20933] INFO  27832 --- [nio-8080-exec-5] c.a.l.controller.UserController          : inside postXY
[2022-08-28 23:06:10:20933] INFO  27832 --- [nio-8080-exec-5] c.a.l.controller.UserController          : wjddn3711@gmail.com
[2022-08-28 23:06:10:20933] INFO  27832 --- [nio-8080-exec-5] c.a.l.controller.UserController          : 딩스터
[2022-08-28 23:06:10:20934] INFO  27832 --- [nio-8080-exec-5] c.a.l.service.UserServiceImpl            : save xy email : wjddn3711@gmail.com
[2022-08-28 23:06:10:20936] INFO  27832 --- [nio-8080-exec-5] c.a.l.service.UserServiceImpl            : 127.02713245190886
[2022-08-28 23:06:10:20936] INFO  27832 --- [nio-8080-exec-5] c.a.l.service.UserServiceImpl            : 37.56867906617772

DB 업데이트 여부 확인

실행 화면 (😰 다음 주소 api 는 팝업이라 화면 캡쳐가 불가하네요)

  • 레스토랑의 경우 아직 업데이트가 되지 않아 수정중에 있습니다
    • 다음 포스팅때 레스토랑 View 이동전 주변 음식점 정보가 없다면 추가하는 것으로 변경예정!

0개의 댓글