<!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();
}
}
<!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 사용
// 주소를 통해 위경도 반환 @PostMapping("/getLonLat") public AddressDTO getLonLat(@RequestParam(value ="fullAddress") String fullAddress){ log.info("full address : "+fullAddress); return userService.getXY(fullAddress); }
기존 세션 저장 형태
@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);
}
}
위치 등록 이후에는 ?
- 사용자의 위치 등록은 선택이 아닌 필수입니다. 도메인의 특성상 주변 맛집을 알 수 있도록 하는 것이 목적이기 때문에 위치 등록 이후에는 해당 좌표를 세션에 담고 하나의 유저 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";
}
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);
}
@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 는 팝업이라 화면 캡쳐가 불가하네요)