diff --git a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java index ef47c35..efe5357 100644 --- a/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/RecommendActivity/service/RecommendActivityService.java @@ -12,6 +12,8 @@ import com.example.silverbridgeX_user.global.api_payload.ErrorCode; import com.example.silverbridgeX_user.global.exception.GeneralException; import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.domain.UserRole; +import com.example.silverbridgeX_user.user.repository.UserRepository; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.LinkedHashSet; @@ -27,6 +29,7 @@ @Service @RequiredArgsConstructor public class RecommendActivityService { + private final UserRepository userRepository; private final ActivityLogRepository activityLogRepository; private final ActivityRepository activityRepository; private final RecommendActivityRepository recommendActivityRepository; @@ -155,7 +158,7 @@ public void insertSimilarEdges(List> rows) { public void generateAllUsersRecommendation() { // 0) 모든 사용자 조회 (PostgreSQL) - List users = activityRepository.findAllUsers(); + List users = userRepository.findAllUsersByRole(UserRole.OLDER); try (Session session = neo4jDriver.session()) { diff --git a/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java index ff97c2e..e9fc6e8 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/repository/ActivityRepository.java @@ -1,7 +1,6 @@ package com.example.silverbridgeX_user.activity.repository; import com.example.silverbridgeX_user.activity.domain.Activity; -import com.example.silverbridgeX_user.user.domain.User; import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -18,7 +17,4 @@ ORDER BY random() """, nativeQuery = true) List pickRandom(int limit, Collection excluded); - @Query("select u from User u") - List findAllUsers(); - } diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java index aa407e7..6708ff4 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java +++ b/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityService.java @@ -6,16 +6,14 @@ import com.example.silverbridgeX_user.activity.dto.ActivityResponseDto.TourSpotItemDto; import com.example.silverbridgeX_user.activity.repository.ActivityNativeRepository; import com.example.silverbridgeX_user.activity.repository.ActivityRepository; -import com.example.silverbridgeX_user.global.api_payload.ErrorCode; -import com.example.silverbridgeX_user.global.exception.GeneralException; +import com.example.silverbridgeX_user.global.service.ApiService; +import com.example.silverbridgeX_user.global.service.CoordinateService; import com.example.silverbridgeX_user.global.util.EmbeddingClient; import com.example.silverbridgeX_user.user.repository.UserRepository; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.transaction.Transactional; import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.time.LocalDate; import java.util.ArrayList; @@ -27,7 +25,6 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.Values; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Slf4j @@ -37,7 +34,8 @@ public class ActivityService { private final ActivityRepository activityRepository; private final EmbeddingClient embeddingClient; private final ActivityNativeRepository activityNativeRepository; - private final ActivityApiService activityApiService; + private final CoordinateService coordinateService; + private final ApiService apiService; private final UserRepository userRepository; private final Driver driver; @@ -45,7 +43,6 @@ public class ActivityService { String festivalApiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_cltur_fstvl_api"; String tourSpotApiUrl = "http://api.data.go.kr/openapi/tn_pubr_public_trrsrt_api"; - String addressToCoordinateApiurl = "https://api.vworld.kr/req/address?service=address&format=json&request=getCoord&refine=false"; public void fetchAndSaveFestivalData() { ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); @@ -53,9 +50,9 @@ public void fetchAndSaveFestivalData() { int page = 1; while (true) { try { - String urlStr = activityApiService.buildUrl(festivalApiUrl, page); - String json = activityApiService.getJsonFromUrl(urlStr); - JsonNode items = activityApiService.parseItems(json); + String urlStr = apiService.buildUrl(festivalApiUrl, page); + String json = apiService.getJsonFromUrl(urlStr); + JsonNode items = apiService.parseItems(json); if (items == null || !items.isArray() || items.isEmpty()) { log.info("page {} 응답 없음. 중단.", page); @@ -111,15 +108,61 @@ public void fetchAndSaveFestivalData() { } } + public void fetchAndSaveCoordinate() throws Exception { + List activities = activityRepository.findAll(); + System.out.println(activities.size()); + + for (Activity activity : activities) { + + try { + if (activity.getLatitude().startsWith("\"")) { + coordinateService.change(activity); + } + + if (!activity.getLongitude().isEmpty() || !activity.getLatitude().isEmpty()) { + continue; + } + + String urlStr = null; + + if (activity.getStreetAddress() != null) { + urlStr = coordinateService.buildCoordinateUrl("ROAD", activity.getStreetAddress()); + } else if (activity.getLotNumberAddress() != null) { + urlStr = coordinateService.buildCoordinateUrl("PARCEL", activity.getLotNumberAddress()); + } + + if (urlStr == null) { + continue; + } + + String json = apiService.getJsonFromUrl(urlStr); + log.info(json); + JsonNode point = apiService.parsePoint(json); + + if (!point.isMissingNode()) { + String x = String.valueOf(point.get("x")); + String y = String.valueOf(point.get("y")); + + activity.updateCoordinate(x, y); + activityRepository.save(activity); + } + } catch (IOException | ParseException e) { + throw new RuntimeException(e); + } + + } + + } + public void fetchAndSaveTourSpotData() { ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); int page = 1; while (true) { try { - String urlStr = activityApiService.buildUrl(tourSpotApiUrl, page); - String json = activityApiService.getJsonFromUrl(urlStr); - JsonNode items = activityApiService.parseItems(json); + String urlStr = apiService.buildUrl(tourSpotApiUrl, page); + String json = apiService.getJsonFromUrl(urlStr); + JsonNode items = apiService.parseItems(json); if (items == null || !items.isArray() || items.isEmpty()) { log.info("page {} 응답 없음. 중단.", page); @@ -169,97 +212,6 @@ public void fetchAndSaveTourSpotData() { } } - public void fetchAndSaveCoordinate() throws Exception { - List activities = activityRepository.findAll(); - System.out.println(activities.size()); - - for (Activity activity : activities) { - - try { - if (activity.getLatitude().startsWith("\"")) { - change(activity); - } - - if (!activity.getLongitude().isEmpty() || !activity.getLatitude().isEmpty()) { - continue; - } - - String urlStr = null; - - if (activity.getStreetAddress() != null) { - urlStr = buildCoordinateUrl("ROAD", activity.getStreetAddress()); - } else if (activity.getLotNumberAddress() != null) { - urlStr = buildCoordinateUrl("PARCEL", activity.getLotNumberAddress()); - } - - if (urlStr == null) { - continue; - } - - String json = activityApiService.getJsonFromUrl(urlStr); - log.info(json); - JsonNode point = activityApiService.parsePoint(json); - - if (!point.isMissingNode()) { - String x = String.valueOf(point.get("x")); - String y = String.valueOf(point.get("y")); - - activity.updateCoordinate(x, y); - activityRepository.save(activity); - } - } catch (IOException | ParseException e) { - throw new RuntimeException(e); - } - - } - - } - - @Value("${geocoder.api.key}") - private String key; - - private String buildCoordinateUrl(String type, String address) throws Exception { - StringBuilder urlBuilder = new StringBuilder(addressToCoordinateApiurl); - urlBuilder.append("&key=" + key); - urlBuilder.append("&type=" + type); // PARCEL : 지번주소, ROAD : 도로명주소 - urlBuilder.append("&address=" + URLEncoder.encode(address, StandardCharsets.UTF_8)); - return urlBuilder.toString(); - } - - private void change(Activity activity) { - String latitude = activity.getLatitude(); - String longitude = activity.getLongitude(); - - boolean updated = false; - - if (latitude != null && latitude.contains("\"")) { - latitude = latitude.replace("\"", ""); // 모든 " 제거 - activity.updateLatitude(latitude); - updated = true; - } - - if (longitude != null && longitude.contains("\"")) { - longitude = longitude.replace("\"", ""); - activity.updateLongitude(longitude); - updated = true; - } - - if (updated) { - activityRepository.save(activity); - System.out.println("좌표 문자열 정제 완료: " + activity.getId()); - } - } - - // 해당 API에서 제공하지 않는 주소들에 대한 2차 검증 로직 - public void test() throws Exception { - Activity activity = activityRepository.findById(Long.valueOf(102)) - .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); - String urlStr = buildCoordinateUrl("PARCEL", activity.getLotNumberAddress()); - String json = activityApiService.getJsonFromUrl(urlStr); - System.out.println(json); - - } - public void insertActivitiesNeo4j() { List activities = activityRepository.findAll(); diff --git a/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java b/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java index 757ac71..349d5df 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java +++ b/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java @@ -11,16 +11,13 @@ public enum ErrorCode implements BaseCode { // Common BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON_400", "잘못된 요청입니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_500", "서버 에러, 서버 개발자에게 문의하세요."), - INVALID_PARAMETER(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_5002", "잘못된 DAY Parameter 가 들어왔습니다. "), // User USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_4041", "존재하지 않는 회원입니다."), USER_NOT_FOUND_BY_EMAIL(HttpStatus.NOT_FOUND, "USER_4042", "존재하지 않는 회원입니다.-EMAIL"), - USER_NOT_FOUND_BY_USERNAME(HttpStatus.NOT_FOUND, "USER_4043", "존재하지 않는 회원입니다.-USERNAME"), - ALREADY_USED_NICKNAME(HttpStatus.FORBIDDEN, "USER_4031", "이미 사용중인 닉네임입니다."), - USER_ADDRESS_NULL(HttpStatus.BAD_REQUEST, "USER_4001", "주소값이 비었거나 NULL입니다."), - INVALID_PHONE_NUMBER_FORMAT(HttpStatus.BAD_REQUEST, "USER_4002", "전화번호 형식은 연속된 숫자 11개여야 합니다."), - SMS_CODE_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "USER_4003", "전화번호 인증 코드가 틀렸습니다"), + USER_NOT_FOUND_BY_USERNAME(HttpStatus.NOT_FOUND, "USER_4043", "존재하지 않는 회원입니다.-USERNAME & KEY"), + USER_NOT_OLDER(HttpStatus.FORBIDDEN, "USER_4031", "해당 유저가 노인이 아니랍니다."), + USER_NOT_PROTECTOR(HttpStatus.FORBIDDEN, "USER_4032", "해당 유저가 보호자가 아닙니다."), // Jwt WRONG_REFRESH_TOKEN(HttpStatus.NOT_FOUND, "JWT_4041", "일치하는 리프레시 토큰이 없습니다."), diff --git a/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java b/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java index c5e7828..784f7fa 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java +++ b/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java @@ -13,15 +13,16 @@ public enum SuccessCode implements BaseCode { CREATED(HttpStatus.CREATED, "COMMON_201", "Created"), // User - USER_LOGIN_SUCCESS(HttpStatus.CREATED, "USER_2011", "회원가입& 로그인이 완료되었습니다."), + USER_SOCIAL_LOGIN_SUCCESS(HttpStatus.CREATED, "USER_2011", "소셜 회원가입 혹은 로그인이 완료되었습니다."), + USER_KEY_LOGIN_SUCCESS(HttpStatus.CREATED, "USER_2012", "KEY 로그인이 완료되었습니다."), USER_LOGOUT_SUCCESS(HttpStatus.OK, "USER_2001", "로그아웃 되었습니다."), USER_REISSUE_SUCCESS(HttpStatus.OK, "USER_2002", "토큰 재발급이 완료되었습니다."), USER_DELETE_SUCCESS(HttpStatus.OK, "USER_2003", "회원탈퇴가 완료되었습니다."), - USER_NICKNAME_SUCCESS(HttpStatus.OK, "USER_2004", "닉네임 생성/수정이 완료되었습니다."), - USER_INFO_UPDATE_SUCCESS(HttpStatus.OK, "USER_2005", "회원 정보 수정이 완료 되었습니다."), - USER_INFO_VIEW_SUCCESS(HttpStatus.OK, "USER_2006", "회원 정보 조회가 완료 되었습니다."), - USER_PROFILE_IMAGE_UPDATE_SUCCESS(HttpStatus.OK, "USER_2007", "프로필 사진 이미지 업로드가 완료 되었습니다."), - USER_MYPAGE_VIEW_SUCCESS(HttpStatus.OK, "USER_2010", "마이페이지 정보 조회가 완료되었습니다."), + USER_NICKNAME_UPDATE_SUCCESS(HttpStatus.OK, "USER_2004", "닉네임 수정이 완료되었습니다."), + USER_ADDRESS_UPDATE_SUCCESS(HttpStatus.OK, "USER_2005", "주소지 수정이 완료되었습니다."), + USER_MYPAGE_VIEW_SUCCESS(HttpStatus.OK, "USER_2006", "마이페이지 정보 조회가 완료되었습니다."), + USER_PROTECTOR_CONNECT_OLDER_SUCCESS(HttpStatus.OK, "USER_2007", "보호자의 노인 연결이 완료되었습니다."), + USER_PROTECTOR_REGISTER_OLDER_SUCCESS(HttpStatus.OK, "USER_2008", "보호자의 노인 등록이 완료되었습니다."), // Recommend Activity RECOMMEND_ACTIVITY_VIEW_LIST_SUCCESS(HttpStatus.OK, "ACTIVITY_2001", "추천하는 활동 리스를 반환 완료했습니다."), diff --git a/src/main/java/com/example/silverbridgeX_user/global/converter/CoordinateConverter.java b/src/main/java/com/example/silverbridgeX_user/global/converter/CoordinateConverter.java new file mode 100644 index 0000000..f11848b --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/global/converter/CoordinateConverter.java @@ -0,0 +1,14 @@ +package com.example.silverbridgeX_user.global.converter; + +import com.example.silverbridgeX_user.global.dto.CoordinateDto.simpleCoordinateDto; + +public class CoordinateConverter { + + public static simpleCoordinateDto simpleCoordinateDto(String x, String y) { + + return simpleCoordinateDto.builder() + .x(x) + .y(y) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/global/dto/CoordinateDto.java b/src/main/java/com/example/silverbridgeX_user/global/dto/CoordinateDto.java new file mode 100644 index 0000000..0e68319 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/global/dto/CoordinateDto.java @@ -0,0 +1,20 @@ +package com.example.silverbridgeX_user.global.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +public class CoordinateDto { + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class simpleCoordinateDto { + private String x; + private String y; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java b/src/main/java/com/example/silverbridgeX_user/global/service/ApiService.java similarity index 95% rename from src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java rename to src/main/java/com/example/silverbridgeX_user/global/service/ApiService.java index 8e81a5b..08da73f 100644 --- a/src/main/java/com/example/silverbridgeX_user/activity/service/ActivityApiService.java +++ b/src/main/java/com/example/silverbridgeX_user/global/service/ApiService.java @@ -1,4 +1,4 @@ -package com.example.silverbridgeX_user.activity.service; +package com.example.silverbridgeX_user.global.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,7 +12,7 @@ import org.springframework.stereotype.Service; @Service -public class ActivityApiService { +public class ApiService { private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/src/main/java/com/example/silverbridgeX_user/global/service/CoordinateService.java b/src/main/java/com/example/silverbridgeX_user/global/service/CoordinateService.java new file mode 100644 index 0000000..f3e3600 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/global/service/CoordinateService.java @@ -0,0 +1,84 @@ +package com.example.silverbridgeX_user.global.service; + +import com.example.silverbridgeX_user.activity.domain.Activity; +import com.example.silverbridgeX_user.activity.repository.ActivityRepository; +import com.example.silverbridgeX_user.global.api_payload.ErrorCode; +import com.example.silverbridgeX_user.global.converter.CoordinateConverter; +import com.example.silverbridgeX_user.global.dto.CoordinateDto; +import com.example.silverbridgeX_user.global.exception.GeneralException; +import com.fasterxml.jackson.databind.JsonNode; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CoordinateService { + private final ActivityRepository activityRepository; + private final ApiService apiService; + + public String addressToCoordinateApiurl = "https://api.vworld.kr/req/address?service=address&format=json&request=getCoord&refine=false"; + + @Value("${geocoder.api.key}") + private String key; + + public CoordinateDto.simpleCoordinateDto getCoordinateByAddress(String type, String address) throws Exception { + String urlStr = buildCoordinateUrl(type, address); + assert apiService != null; + String json = apiService.getJsonFromUrl(urlStr); + JsonNode point = apiService.parsePoint(json); + + String x = String.valueOf(point.get("x")); + String y = String.valueOf(point.get("y")); + x = x.replace("\"", ""); + y = y.replace("\"", ""); + + return CoordinateConverter.simpleCoordinateDto(x, y); + } + + public String buildCoordinateUrl(String type, String address) { + StringBuilder urlBuilder = new StringBuilder(addressToCoordinateApiurl); + urlBuilder.append("&key=" + key); + urlBuilder.append("&type=" + type); // PARCEL : 지번주소, ROAD : 도로명주소 + urlBuilder.append("&address=" + URLEncoder.encode(address, StandardCharsets.UTF_8)); + return urlBuilder.toString(); + } + + public void change(Activity activity) { + String latitude = activity.getLatitude(); + String longitude = activity.getLongitude(); + + boolean updated = false; + + if (latitude != null && latitude.contains("\"")) { + latitude = latitude.replace("\"", ""); // 모든 " 제거 + activity.updateLatitude(latitude); + updated = true; + } + + if (longitude != null && longitude.contains("\"")) { + longitude = longitude.replace("\"", ""); + activity.updateLongitude(longitude); + updated = true; + } + + if (updated) { + activityRepository.save(activity); + System.out.println("좌표 문자열 정제 완료: " + activity.getId()); + } + } + + // 해당 API에서 제공하지 않는 주소들에 대한 2차 검증 로직 + public void test() throws Exception { + Activity activity = activityRepository.findById(Long.valueOf(102)) + .orElseThrow(() -> GeneralException.of(ErrorCode.ACTIVITY_NOT_FOUND)); + String urlStr = buildCoordinateUrl("PARCEL", activity.getLotNumberAddress()); + String json = apiService.getJsonFromUrl(urlStr); + System.out.println(json); + + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/global/util/UUID.java b/src/main/java/com/example/silverbridgeX_user/global/util/UUID.java new file mode 100644 index 0000000..a745c99 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/global/util/UUID.java @@ -0,0 +1,18 @@ +package com.example.silverbridgeX_user.global.util; + +import java.security.SecureRandom; + +public class UUID { + + private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final SecureRandom random = new SecureRandom(); + + public static String generate() { + StringBuilder sb = new StringBuilder(6); + for (int i = 0; i < 6; i++) { + sb.append(BASE62.charAt(random.nextInt(BASE62.length()))); + } + return sb.toString(); + } + +} diff --git a/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java b/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java index 4247f16..b0da1e3 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java +++ b/src/main/java/com/example/silverbridgeX_user/user/controller/TokenController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -25,14 +26,14 @@ public class TokenController { private final UserService userService; - @Operation(summary = "토큰 반환", description = "프론트에게 유저 정보 받아 토큰 반환하는 메서드입니다.") + @Operation(summary = "토큰 반환", description = "노인/보호자 소셜 로그인 후, 프론트에게 유저 정보 받아 토큰 반환하는 메서드입니다.") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2011", description = "회원가입 & 로그인 성공"), }) - @PostMapping("/generate") + @PostMapping("/generate/social") public ApiResponse tokenToFront( - @RequestBody UserRequestDto.UserReqDto userReqDto // email, username, nickname - ) { + @RequestBody UserRequestDto.UserReqDto userReqDto + ) throws Exception { Boolean isMember = userService.checkMemberByEmail(userReqDto.getEmail()); String accessToken = ""; @@ -41,23 +42,41 @@ public ApiResponse tokenToFront( String signIn = "wasUser"; if (isMember) { - User user = userService.findByUserName(userReqDto.getEmail()); + User user = userService.findByEmail(userReqDto.getEmail()); - JwtDto jwt = userService.jwtMakeSave(userReqDto.getEmail()); + JwtDto jwt = userService.jwtMakeSave(user.getUsername()); accessToken = jwt.getAccessToken(); refreshToken = jwt.getRefreshToken(); } else { - User newUser = userService.createUser(userReqDto); + User user = userService.createUser(userReqDto); - JwtDto jwt = userService.jwtMakeSave(userReqDto.getEmail()); + JwtDto jwt = userService.jwtMakeSave(user.getUsername()); accessToken = jwt.getAccessToken(); refreshToken = jwt.getRefreshToken(); signIn = "newUser"; } - return ApiResponse.onSuccess(SuccessCode.USER_LOGIN_SUCCESS, + return ApiResponse.onSuccess(SuccessCode.USER_SOCIAL_LOGIN_SUCCESS, UserConverter.jwtDto(accessToken, refreshToken, signIn)); } + + @Operation(summary = "토큰 반환", description = "노인의 key 로그인 후, 프론트에게 유저 정보 받아 토큰 반환하는 메서드입니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2012", description = "회원가입 & 로그인 성공"), + }) + @PostMapping("/generate/key") + public ApiResponse tokenToFront( + @RequestParam String key + ) { + User user = userService.findByUserName(key); + + JwtDto jwt = userService.jwtMakeSave(user.getUsername()); + String accessToken = jwt.getAccessToken(); + String refreshToken = jwt.getRefreshToken(); + + return ApiResponse.onSuccess(SuccessCode.USER_KEY_LOGIN_SUCCESS, + UserConverter.jwtDto(accessToken, refreshToken, "wasUser")); + } } diff --git a/src/main/java/com/example/silverbridgeX_user/user/controller/UserController.java b/src/main/java/com/example/silverbridgeX_user/user/controller/UserController.java index 1f71ecd..1ac0b2a 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/controller/UserController.java +++ b/src/main/java/com/example/silverbridgeX_user/user/controller/UserController.java @@ -2,9 +2,14 @@ import com.example.silverbridgeX_user.global.api_payload.ApiResponse; import com.example.silverbridgeX_user.global.api_payload.SuccessCode; +import com.example.silverbridgeX_user.user.converter.UserConverter; import com.example.silverbridgeX_user.user.domain.User; import com.example.silverbridgeX_user.user.dto.JwtDto; +import com.example.silverbridgeX_user.user.dto.UserRequestDto; +import com.example.silverbridgeX_user.user.dto.UserRequestDto.UserAddressReqDto; import com.example.silverbridgeX_user.user.dto.UserRequestDto.UserNicknameReqDto; +import com.example.silverbridgeX_user.user.dto.UserResponseDto.OlderMyPageResDto; +import com.example.silverbridgeX_user.user.dto.UserResponseDto.ProtectorMyPageResDto; import com.example.silverbridgeX_user.user.jwt.CustomUserDetails; import com.example.silverbridgeX_user.user.service.UserService; import io.swagger.v3.oas.annotations.Operation; @@ -15,9 +20,11 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "회원", description = "회원 관련 api 입니다.") @@ -53,23 +60,97 @@ public ApiResponse reissue( @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2003", description = "회원탈퇴가 완료되었습니다."), }) - @DeleteMapping("/delete") + @DeleteMapping("/me") public ApiResponse deleteUser(Authentication auth) { userService.deleteUser(auth.getName()); return ApiResponse.onSuccess(SuccessCode.USER_DELETE_SUCCESS, "user entity 삭제 완료"); } - @Operation(summary = "닉네임 입력, 수정", description = "중복 안되는 닉네임을 입력받는 메서드입니다.") + @Operation(summary = "닉네임 수정", description = "닉네임을 변경하는 메서드입니다.") @ApiResponses(value = { - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2004", description = "닉네임 생성이 완료되었습니다.") + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2004", description = "닉네임 변경이 완료되었습니다.") }) - @PostMapping(value = "/nickname/update") + @PostMapping(value = "/nickname") public ApiResponse nickname( @RequestBody UserNicknameReqDto nicknameReqDto, @AuthenticationPrincipal CustomUserDetails customUserDetails ) { User user = userService.findByUserName(customUserDetails.getUsername()); userService.saveNickname(nicknameReqDto, user); - return ApiResponse.onSuccess(SuccessCode.USER_NICKNAME_SUCCESS, true); + return ApiResponse.onSuccess(SuccessCode.USER_NICKNAME_UPDATE_SUCCESS, true); } + + @Operation(summary = "주소 수정", description = "주소를 변경하는 메서드입니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2005", description = "주소지 변경이 완료되었습니다.") + }) + @PostMapping(value = "/address") + public ApiResponse address( + @RequestBody UserAddressReqDto addressReqDto, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) throws Exception { + User user = userService.findByUserName(customUserDetails.getUsername()); + userService.saveAddress(addressReqDto, user); + return ApiResponse.onSuccess(SuccessCode.USER_ADDRESS_UPDATE_SUCCESS, true); + } + + @Operation(summary = "노인 마이페이지 조회", description = "노인 마이페이지를 조회하는 메서드입니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2006", description = "노인 마이페이지 정보 조회가 완료되었습니다.") + }) + @GetMapping(value = "/mypage/older") + public ApiResponse mypageOlder( + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + User user = userService.findByUserName(customUserDetails.getUsername()); + userService.validateOlder(user); + return ApiResponse.onSuccess(SuccessCode.USER_MYPAGE_VIEW_SUCCESS, UserConverter.olderMyPageResDto(user)); + } + + @Operation(summary = "보호자 마이페이지 조회", description = "보호자 마이페이지를 조회하는 메서드입니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2006", description = "노인 마이페이지 정보 조회가 완료되었습니다.") + }) + @GetMapping(value = "/mypage/protector") + public ApiResponse mypageProtector( + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + User user = userService.findByUserName(customUserDetails.getUsername()); + userService.validateProtector(user); + + return ApiResponse.onSuccess(SuccessCode.USER_MYPAGE_VIEW_SUCCESS, UserConverter.protectorMyPageResDto(user)); + } + + @Operation(summary = "보호자의 노인 연결", description = "보호자가 관리할 노인을 연결하는 메서드입니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2007", description = "보호자의 노인 연결이 완료되었습니다.") + }) + @PostMapping(value = "/protectors/older-links") + public ApiResponse assignOlder( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @RequestParam("olderKey") String olderKey + ) { + User protector = userService.findByUserName(customUserDetails.getUsername()); + userService.assignOlder(protector, olderKey); + + return ApiResponse.onSuccess(SuccessCode.USER_PROTECTOR_CONNECT_OLDER_SUCCESS, true); + } + + @Operation(summary = "보호자의 노인 등록", description = "보호자가 관리할 노인을 등록하는 메서드입니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER_2008", description = "보호자의 노인 등록이 완료되었습니다.") + }) + @PostMapping(value = "/protectors/olders") + public ApiResponse registerOlder( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @RequestBody UserRequestDto.UserReqDto userReqDto + ) throws Exception { + User protector = userService.findByUserName(customUserDetails.getUsername()); + + User older = userService.createUser(userReqDto); + String key = userService.registerOlder(protector, older); + + return ApiResponse.onSuccess(SuccessCode.USER_PROTECTOR_REGISTER_OLDER_SUCCESS, key); + } + } diff --git a/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java b/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java index 9a050b9..d4755aa 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java +++ b/src/main/java/com/example/silverbridgeX_user/user/converter/UserConverter.java @@ -3,18 +3,21 @@ import com.example.silverbridgeX_user.user.domain.User; import com.example.silverbridgeX_user.user.dto.JwtDto; import com.example.silverbridgeX_user.user.dto.UserRequestDto; +import com.example.silverbridgeX_user.user.dto.UserResponseDto.OlderMyPageResDto; +import com.example.silverbridgeX_user.user.dto.UserResponseDto.ProtectorMyPageResDto; public class UserConverter { - public static User saveUser(UserRequestDto.UserReqDto userReqDto) { + public static User saveUser(UserRequestDto.UserReqDto userReqDto, String key) { return User.builder() + .role(userReqDto.getRole()) .email(userReqDto.getEmail()) - .username(userReqDto.getEmail()) + .username(key) .nickname(userReqDto.getNickname()) + .streetAddress(userReqDto.getStreetAddress()) .build(); } - public static JwtDto jwtDto(String access, String refresh, String signIn) { return JwtDto.builder() .accessToken(access) @@ -23,4 +26,23 @@ public static JwtDto jwtDto(String access, String refresh, String signIn) { .build(); } + public static OlderMyPageResDto olderMyPageResDto(User user) { + return OlderMyPageResDto.builder() + .key(user.getUsername()) + .nickname(user.getNickname()) + .address(user.getStreetAddress()) + .email(user.getEmail()) + .build(); + } + + public static ProtectorMyPageResDto protectorMyPageResDto(User user) { + return ProtectorMyPageResDto.builder() + .key(user.getUsername()) + .nickname(user.getNickname()) + .address(user.getStreetAddress()) + .email(user.getEmail()) + .build(); + } + + } diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java index 2bdea90..82d98a2 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java @@ -6,11 +6,17 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; +import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -42,6 +48,17 @@ public class User extends BaseEntity { @Column(nullable = false) private String nickname; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private UserRole role; + + @ManyToOne + @JoinColumn(name = "protector_id") + private User protector; + + @OneToMany(mappedBy = "protector", cascade = CascadeType.ALL) + private List olders = new ArrayList<>(); + private String sex; private LocalDate birth; @@ -85,9 +102,22 @@ public void updateNickname(String nickname) { this.nickname = nickname; } + public void updateAddress(String address) { + this.streetAddress = address; + } + public void updatePreferredKeywords(List keywords) { this.preferredKeywords = keywords; } + + public void updateCoordinate(String longitude, String latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + public void updateProtector(User protector) { + this.protector = protector; // null 가능 + } } diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/UserRole.java b/src/main/java/com/example/silverbridgeX_user/user/domain/UserRole.java new file mode 100644 index 0000000..79cfec2 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/UserRole.java @@ -0,0 +1,6 @@ +package com.example.silverbridgeX_user.user.domain; + +public enum UserRole { + OLDER, + PROTECTOR +} diff --git a/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java b/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java index f5224a4..f544b1a 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java +++ b/src/main/java/com/example/silverbridgeX_user/user/dto/UserRequestDto.java @@ -1,5 +1,6 @@ package com.example.silverbridgeX_user.user.dto; +import com.example.silverbridgeX_user.user.domain.UserRole; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import lombok.AllArgsConstructor; @@ -16,6 +17,8 @@ public class UserRequestDto { @AllArgsConstructor @NoArgsConstructor public static class UserReqDto { + @Schema(description = "역할(OLDER/PROTECTOR)") + private UserRole role; @Schema(description = "이메일") private String email; @@ -23,6 +26,9 @@ public static class UserReqDto { @Schema(description = "닉네임") private String nickname; + @Schema(description = "도로명 주소") + private String streetAddress; + } @Schema(description = "UserNicknameReqDto") @@ -44,17 +50,10 @@ public static class UserNicknameReqDto { @AllArgsConstructor @NoArgsConstructor public static class UserAddressReqDto { - @Schema(description = "수령자 이름") - private String recipientName; - - @Schema(description = "전화번호") - private String phoneNumber; @Schema(description = "도로명 주소") private String streetAddress; - @Schema(description = "상세 주소") - private String detailedAddress; } @Schema(description = "UserPreferenceDto") diff --git a/src/main/java/com/example/silverbridgeX_user/user/dto/UserResponseDto.java b/src/main/java/com/example/silverbridgeX_user/user/dto/UserResponseDto.java index c11f4c8..1fadca5 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/dto/UserResponseDto.java +++ b/src/main/java/com/example/silverbridgeX_user/user/dto/UserResponseDto.java @@ -10,18 +10,44 @@ @NoArgsConstructor public class UserResponseDto { - @Schema(description = "UserMyPageResDto") + @Schema(description = "OlderMyPageResDto") @Getter @Setter @Builder @AllArgsConstructor @NoArgsConstructor - public static class UserMyPageResDto { + public static class OlderMyPageResDto { @Schema(description = "닉네임") private String nickname; - @Schema(description = "프로필 이미지") - private String profileImage; + @Schema(description = "key") + private String key; + + @Schema(description = "address") + private String address; + + @Schema(description = "email") + private String email; + } + + @Schema(description = "ProtectorMyPageResDto") + @Getter + @Setter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ProtectorMyPageResDto { + @Schema(description = "닉네임") + private String nickname; + + @Schema(description = "key") + private String key; + + @Schema(description = "address") + private String address; + + @Schema(description = "email") + private String email; } } diff --git a/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java b/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java index aec249c..79bb94d 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/user/repository/UserRepository.java @@ -1,6 +1,8 @@ package com.example.silverbridgeX_user.user.repository; import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.domain.UserRole; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -26,6 +28,8 @@ public interface UserRepository extends JpaRepository { // 5. 닉네임이 사용중인지 판단하는 기능 boolean existsByNickname(String nickname); + List findAllUsersByRole(UserRole role); + @Modifying @Transactional @Query(value = "UPDATE member SET preferred_embedding = CAST(:embedding AS vector) WHERE id = :userId", nativeQuery = true) diff --git a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java index ada5a3c..8c750b1 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java +++ b/src/main/java/com/example/silverbridgeX_user/user/service/UserService.java @@ -1,13 +1,18 @@ package com.example.silverbridgeX_user.user.service; import com.example.silverbridgeX_user.global.api_payload.ErrorCode; +import com.example.silverbridgeX_user.global.dto.CoordinateDto; import com.example.silverbridgeX_user.global.exception.GeneralException; +import com.example.silverbridgeX_user.global.service.CoordinateService; +import com.example.silverbridgeX_user.global.util.UUID; import com.example.silverbridgeX_user.user.converter.UserConverter; import com.example.silverbridgeX_user.user.domain.RefreshToken; import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.domain.UserRole; import com.example.silverbridgeX_user.user.dto.JwtDto; import com.example.silverbridgeX_user.user.dto.UserRequestDto; import com.example.silverbridgeX_user.user.dto.UserRequestDto.UserPreferenceDto; +import com.example.silverbridgeX_user.user.dto.UserRequestDto.UserReqDto; import com.example.silverbridgeX_user.user.jwt.JwtTokenUtils; import com.example.silverbridgeX_user.user.repository.RefreshTokenRepository; import com.example.silverbridgeX_user.user.repository.UserRepository; @@ -34,12 +39,14 @@ public class UserService { private final UserRepository userRepository; private final RefreshTokenRepository refreshTokenRepository; private final JpaUserDetailsManager manager; + private final CoordinateService coordinateService; private final JwtTokenUtils jwtTokenUtils; private final RestTemplate restTemplate; private final Driver driver; @Value("${chat.server.url}") private String chatServerUrl; + private String PREF_URL; @PostConstruct @@ -53,21 +60,40 @@ public User findByUserName(String userName) { .orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND_BY_USERNAME)); } + @Transactional + public User findByEmail(String email) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND_BY_EMAIL)); + } + @Transactional public Boolean checkMemberByEmail(String email) { return userRepository.existsByEmail(email); } @Transactional - public User createUser(UserRequestDto.UserReqDto userReqDto) { - // User 엔티티 만들기 - User newUser = UserConverter.saveUser(userReqDto); + public User createUser(UserReqDto userReqDto) throws Exception { + + String uniqueKey; + do { + uniqueKey = UUID.generate(); + } while (userRepository.existsByUsername(uniqueKey)); + + User newUser = UserConverter.saveUser(userReqDto, uniqueKey); userRepository.save(newUser); - // Neo4j 사용자 노드 만들기 - insertUserNodeIfNotExists(newUser.getId()); + if (userReqDto.getRole().equals(UserRole.OLDER)) { + + insertUserNodeIfNotExists(newUser.getId()); + + CoordinateDto.simpleCoordinateDto coordinateDto = + coordinateService.getCoordinateByAddress("ROAD", userReqDto.getStreetAddress()); + + newUser.updateCoordinate(coordinateDto.getX(), coordinateDto.getY()); + userRepository.save(newUser); + } - manager.loadUserByUsername(userReqDto.getEmail()); // 저장된 사용자 정보를 다시 로드하여 동기화 시도 + manager.loadUserByUsername(newUser.getUsername()); // 저장된 사용자 정보를 다시 로드하여 동기화 시도 return newUser; } @@ -148,6 +174,13 @@ public void deleteUser(String username) { refreshTokenRepository.deleteByUsername(username); log.info("DB에서 리프레시 토큰 삭제 완료"); } + + if (user.getRole().equals(UserRole.PROTECTOR)) { + for (User older : user.getOlders()) { + older.updateProtector(null); // FK 끊기 + } + } + userRepository.delete(user); log.info("{} 회원 탈퇴 완료", username); } @@ -182,13 +215,21 @@ public void insertUserNodeIfNotExists(Long userId) { @Transactional public void saveNickname(UserRequestDto.UserNicknameReqDto nicknameReqDto, User user) { String nickname = nicknameReqDto.getNickname(); - - if (userRepository.existsByNickname(nickname)) { - throw GeneralException.of(ErrorCode.ALREADY_USED_NICKNAME); - } user.updateNickname(nickname); + userRepository.save(user); + } + + @Transactional + public void saveAddress(UserRequestDto.UserAddressReqDto addressReqDto, User user) throws Exception { + String address = addressReqDto.getStreetAddress(); + user.updateAddress(address); + + CoordinateDto.simpleCoordinateDto coordinateDto = coordinateService.getCoordinateByAddress("ROAD", address); + user.updateCoordinate(coordinateDto.getX(), coordinateDto.getY()); + userRepository.save(user); } + @Transactional public void updatePreferredKeywords() { ResponseEntity response = restTemplate.getForEntity(PREF_URL, UserPreferenceDto[].class); @@ -210,4 +251,41 @@ public void updatePreferredKeywords() { }); } } + + public void validateOlder(User user) { + if (!user.getRole().equals(UserRole.OLDER)) { + log.info(String.valueOf(user.getRole())); + throw new GeneralException(ErrorCode.USER_NOT_OLDER); + } + } + + public void validateProtector(User user) { + if (!user.getRole().equals(UserRole.PROTECTOR)) { + log.info(String.valueOf(user.getRole())); + throw new GeneralException(ErrorCode.USER_NOT_PROTECTOR); + } + } + + @Transactional + public void assignOlder(User protector, String key) { + User older = userRepository.findByUsername(key) + .orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND_BY_USERNAME)); + + validateProtector(protector); + validateOlder(older); + + older.updateProtector(protector); + userRepository.save(older); + } + + @Transactional + public String registerOlder(User protector, User older) { + validateProtector(protector); + validateOlder(older); + + older.updateProtector(protector); + userRepository.save(older); + return older.getUsername(); + } + }