diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java index 9805709..a39b21d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java @@ -7,6 +7,7 @@ import org.withtime.be.withtimebe.domain.member.entity.enums.Gender; import org.withtime.be.withtimebe.domain.member.entity.enums.Role; import org.withtime.be.withtimebe.domain.member.entity.enums.UserRank; +import org.withtime.be.withtimebe.domain.weather.entity.Region; import org.withtime.be.withtimebe.global.common.BaseEntity; import java.time.LocalDate; @@ -75,6 +76,10 @@ public class Member extends BaseEntity { @Column(name = "role", nullable = false) private Role role; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "region_id") + private Region region; + public void changeUsername(String newUsername) { this.username= newUsername; } @@ -92,4 +97,8 @@ public void updateAlarmSetting(Boolean pushAlarm, Boolean emailAlarm, Boolean sm this.emailAlarm = emailAlarm; this.smsAlarm = smsAlarm; } + + public void updateRegion(Region region) { + this.region = region; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/RegionController.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/RegionController.java index 7346812..24ddfb0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/RegionController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/RegionController.java @@ -11,10 +11,12 @@ import org.namul.api.payload.response.DefaultResponse; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.weather.dto.request.RegionReqDTO; import org.withtime.be.withtimebe.domain.weather.dto.response.RegionResDTO; import org.withtime.be.withtimebe.domain.weather.service.command.RegionCommandService; import org.withtime.be.withtimebe.domain.weather.service.query.RegionQueryService; +import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember; @Slf4j @RestController @@ -190,6 +192,43 @@ public DefaultResponse searchRegions( return DefaultResponse.ok(response); } + @GetMapping("/users/current") + @Operation(summary = "현재 사용자 지역 조회 API by 지미", + description = "로그인한 사용자의 현재 지역 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "조회 성공"), + @ApiResponse(responseCode = "401", description = "인증 실패"), + @ApiResponse(responseCode = "404", description = "사용자 또는 지역을 찾을 수 없습니다.") + }) + public DefaultResponse getCurrentUserRegion(@AuthenticatedMember Member member) { + log.info("현재 사용자 지역 조회 API 호출"); + + RegionResDTO.UserRegion response = regionQueryService.getCurrentUserRegion(member); + return DefaultResponse.ok(response); + } + + @PatchMapping("/users") + @Operation(summary = "사용자 지역 변경 API by 지미", + description = "로그인한 사용자의 지역을 설정/변경합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "변경 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), + @ApiResponse(responseCode = "401", description = "인증 실패"), + @ApiResponse(responseCode = "404", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - MEMBER404_0: 사용자를 찾을 수 없습니다. + - WEATHER404_0: 지역을 찾을 수 없습니다. + """) + }) + public DefaultResponse updateUserRegion( + @Valid @RequestBody RegionReqDTO.UpdateUserRegion reqDTO, @AuthenticatedMember Member member) { + log.info("사용자 지역 변경 API 호출: regionId={}", reqDTO.regionId()); + + RegionResDTO.UserRegionWithMessage response = regionCommandService.updateUserRegion(reqDTO, member); + return DefaultResponse.ok(response); + } + @DeleteMapping("/codes/{regionCodeId}") @Operation(summary = "지역코드 삭제 API by 지미 [Only Admin]", description = "지역코드를 삭제합니다(해당 코드를 사용하는 지역이 없어야 함. 관리자용).") @ApiResponses(value = { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/WeatherController.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/WeatherController.java index 4a2a9e2..d893c65 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/WeatherController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/controller/WeatherController.java @@ -72,7 +72,7 @@ public DefaultResponse manualTrigger( @GetMapping("/{regionId}/weekly") @Operation( - summary = "지역별 주간 날씨 기반 추천 조회", + summary = "지역별 주간 날씨 기반 추천 조회 by 지미", description = """ 특정 지역의 7일치(오늘 기준) 날씨 데이터를 바탕으로 한 데이트 추천 정보를 제공합니다. @@ -100,7 +100,7 @@ public DefaultResponse getWeeklyRecommendati @GetMapping("/{regionId}/precipitation") @Operation( - summary = "지역별 7일간 강수확률 조회", + summary = "지역별 7일간 강수확률 조회 by 지미", description = """ 특정 지역의 7일간 강수확률 정보만 간단하게 조회합니다. diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/converter/RegionConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/converter/RegionConverter.java index 46773a6..83919ce 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/converter/RegionConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/converter/RegionConverter.java @@ -156,4 +156,25 @@ public static RegionResDTO.DeleteRegion toDeleteRegion(Region region) { .message("지역이 성공적으로 삭제되었습니다.") .build(); } + + public static RegionResDTO.UserRegion toUserRegion(Region region) { + return RegionResDTO.UserRegion.builder() + .regionId(region.getId()) + .name(region.getName()) + .latitude(region.getLatitude()) + .longitude(region.getLongitude()) + .gridX(region.getGridX()) + .gridY(region.getGridY()) + .regionCode(toRegionCodeInfo(region.getRegionCode())) + .build(); + } + + public static RegionResDTO.UserRegionWithMessage toUserRegionWithMessage(Region region, String message) { + return RegionResDTO.UserRegionWithMessage.builder() + .regionId(region.getId()) + .name(region.getName()) + .regionCode(toRegionCodeInfo(region.getRegionCode())) + .message(message) + .build(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/request/RegionReqDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/request/RegionReqDTO.java index b598b3c..3b1a283 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/request/RegionReqDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/request/RegionReqDTO.java @@ -56,4 +56,10 @@ public record CreateRegionWithNewCode( String regionCodeName ) { } + + public record UpdateUserRegion( + @NotNull(message = "지역 ID는 필수 입력값입니다.") + Long regionId + ) { + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/response/RegionResDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/response/RegionResDTO.java index 106b36f..99a6b6a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/response/RegionResDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/dto/response/RegionResDTO.java @@ -103,4 +103,25 @@ public record DeleteRegion( String message ) { } + + @Builder + public record UserRegion( + Long regionId, + String name, + BigDecimal latitude, + BigDecimal longitude, + BigDecimal gridX, + BigDecimal gridY, + RegionCodeInfo regionCode + ) { + } + + @Builder + public record UserRegionWithMessage( + Long regionId, + String name, + RegionCodeInfo regionCode, + String message + ) { + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandService.java index cebca59..0470db0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandService.java @@ -1,5 +1,6 @@ package org.withtime.be.withtimebe.domain.weather.service.command; +import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.weather.dto.request.RegionReqDTO; import org.withtime.be.withtimebe.domain.weather.dto.response.RegionResDTO; @@ -14,4 +15,6 @@ public interface RegionCommandService { RegionResDTO.DeleteRegionCode deleteRegionCode(Long regionCodeId); RegionResDTO.DeleteRegion deleteRegion(Long regionId); + + RegionResDTO.UserRegionWithMessage updateUserRegion(RegionReqDTO.UpdateUserRegion reqDTO, Member member); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandServiceImpl.java index 911ef17..9dac635 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/command/RegionCommandServiceImpl.java @@ -4,7 +4,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.reactive.function.client.WebClient; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; import org.withtime.be.withtimebe.domain.weather.converter.RegionConverter; import org.withtime.be.withtimebe.domain.weather.dto.request.RegionReqDTO; import org.withtime.be.withtimebe.domain.weather.dto.response.RegionResDTO; @@ -12,7 +15,11 @@ import org.withtime.be.withtimebe.domain.weather.entity.RegionCode; import org.withtime.be.withtimebe.domain.weather.repository.RegionCodeRepository; import org.withtime.be.withtimebe.domain.weather.repository.RegionRepository; +import org.withtime.be.withtimebe.global.error.code.MemberErrorCode; +import org.withtime.be.withtimebe.global.error.code.RegionErrorCode; import org.withtime.be.withtimebe.global.error.code.WeatherErrorCode; +import org.withtime.be.withtimebe.global.error.exception.MemberException; +import org.withtime.be.withtimebe.global.error.exception.RegionException; import org.withtime.be.withtimebe.global.error.exception.WeatherException; import java.math.BigDecimal; @@ -29,6 +36,7 @@ public class RegionCommandServiceImpl implements RegionCommandService { private final RegionRepository regionRepository; private final RegionCodeRepository regionCodeRepository; + private final MemberRepository memberRepository; @Value("${weather.api.key}") private String apiKey; @@ -128,6 +136,21 @@ public RegionResDTO.DeleteRegion deleteRegion(Long regionId) { return RegionConverter.toDeleteRegion(region); } + @Override + @Transactional + public RegionResDTO.UserRegionWithMessage updateUserRegion(RegionReqDTO.UpdateUserRegion reqDTO, Member member) { + Member currentMember = memberRepository.findByEmail(member.getEmail()) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + Region region = regionRepository.findByIdWithRegionCode(reqDTO.regionId()) + .orElseThrow(() -> new RegionException(RegionErrorCode.REGION_NOT_FOUND)); + + currentMember.updateRegion(region); + + String message = "지역이 성공적으로 변경되었습니다."; + return RegionConverter.toUserRegionWithMessage(region, message); + } + // ==== 내부 유틸리티 메서드들 ==== private void validateDuplicateRegionCode(String landRegCode, String tempRegCode) { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryService.java index 9dacbe3..d7356a4 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryService.java @@ -1,5 +1,6 @@ package org.withtime.be.withtimebe.domain.weather.service.query; +import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.weather.dto.response.RegionResDTO; public interface RegionQueryService { @@ -11,4 +12,6 @@ public interface RegionQueryService { RegionResDTO.RegionInfo getRegionById(Long regionId); RegionResDTO.RegionSearchResult searchRegions(String keyword); + + RegionResDTO.UserRegion getCurrentUserRegion(Member member); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryServiceImpl.java index 706e7e6..4266363 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/weather/service/query/RegionQueryServiceImpl.java @@ -3,12 +3,19 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; import org.withtime.be.withtimebe.domain.weather.converter.RegionConverter; import org.withtime.be.withtimebe.domain.weather.dto.response.RegionResDTO; import org.withtime.be.withtimebe.domain.weather.entity.Region; import org.withtime.be.withtimebe.domain.weather.repository.RegionCodeRepository; import org.withtime.be.withtimebe.domain.weather.repository.RegionRepository; +import org.withtime.be.withtimebe.global.error.code.MemberErrorCode; +import org.withtime.be.withtimebe.global.error.code.RegionErrorCode; import org.withtime.be.withtimebe.global.error.code.WeatherErrorCode; +import org.withtime.be.withtimebe.global.error.exception.MemberException; +import org.withtime.be.withtimebe.global.error.exception.RegionException; import org.withtime.be.withtimebe.global.error.exception.WeatherException; import java.util.List; @@ -20,6 +27,9 @@ public class RegionQueryServiceImpl implements RegionQueryService{ private final RegionCodeRepository regionCodeRepository; private final RegionRepository regionRepository; + private final MemberRepository memberRepository; + + private static final Long DEFAULT_REGION_ID = 1L; @Override public RegionResDTO.RegionCodeList getAllRegionCodes() { @@ -45,4 +55,28 @@ public RegionResDTO.RegionSearchResult searchRegions(String keyword) { List regions = regionRepository.searchByNameContaining(keyword); return RegionConverter.toSearchResult(regions, keyword); } + + @Override + @Transactional + public RegionResDTO.UserRegion getCurrentUserRegion(Member member) { + // 현재 로그인한 사용자 조회 + Member currentMember = memberRepository.findByEmail(member.getEmail()) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + Region userRegion = currentMember.getRegion(); + + // 사용자에게 지역이 설정되지 않은 경우 기본 지역 반환 + if (userRegion == null) { + userRegion = regionRepository.findByIdWithRegionCode(DEFAULT_REGION_ID) + .orElseThrow(() -> new RegionException(RegionErrorCode.REGION_NOT_FOUND)); + + currentMember.updateRegion(userRegion); + memberRepository.save(currentMember); + + log.info("사용자 {}에게 기본 지역({})이 자동 설정되었습니다.", + currentMember.getUsername(), userRegion.getName()); + } + + return RegionConverter.toUserRegion(userRegion); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/RegionErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/RegionErrorCode.java new file mode 100644 index 0000000..eacc3eb --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/RegionErrorCode.java @@ -0,0 +1,28 @@ +package org.withtime.be.withtimebe.global.error.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.code.dto.supports.DefaultResponseErrorReasonDTO; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum RegionErrorCode implements BaseErrorCode { + + // ==== 지역 관련 에러 (404) ==== + REGION_NOT_FOUND(HttpStatus.NOT_FOUND, "REGION404_0", "지역을 찾을 수 없습니다."), + ; + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public DefaultResponseErrorReasonDTO getReason() { + return DefaultResponseErrorReasonDTO.builder() + .httpStatus(this.httpStatus) + .code(this.code) + .message(this.message) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/exception/RegionException.java b/src/main/java/org/withtime/be/withtimebe/global/error/exception/RegionException.java new file mode 100644 index 0000000..76a0d6d --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/exception/RegionException.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.global.error.exception; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.error.exception.ServerApplicationException; + +public class RegionException extends ServerApplicationException { + public RegionException(BaseErrorCode baseErrorCode) { + super(baseErrorCode); + } +}