From 4d7dca9839d2b987e3a94834e38bd4f282ac9a5a Mon Sep 17 00:00:00 2001 From: Jimyeong Kim Date: Thu, 7 Aug 2025 19:20:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=ED=95=9C=20=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/entity/Member.java | 9 +++++ .../weather/controller/RegionController.java | 17 ++++++++++ .../weather/converter/RegionConverter.java | 12 +++++++ .../weather/dto/response/RegionResDTO.java | 12 +++++++ .../service/query/RegionQueryService.java | 3 ++ .../service/query/RegionQueryServiceImpl.java | 34 +++++++++++++++++++ .../global/error/code/RegionErrorCode.java | 28 +++++++++++++++ .../error/exception/RegionException.java | 10 ++++++ 8 files changed, 125 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/code/RegionErrorCode.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/exception/RegionException.java 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..ad19673 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,21 @@ public DefaultResponse searchRegions( return DefaultResponse.ok(response); } + @GetMapping("/user/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); + } + @DeleteMapping("/codes/{regionCodeId}") @Operation(summary = "지역코드 삭제 API by 지미 [Only Admin]", description = "지역코드를 삭제합니다(해당 코드를 사용하는 지역이 없어야 함. 관리자용).") @ApiResponses(value = { 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..23d1ecf 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,16 @@ 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(); + } } 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..682fef1 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,16 @@ public record DeleteRegion( String message ) { } + + @Builder + public record UserRegion( + Long regionId, + String name, + BigDecimal latitude, + BigDecimal longitude, + BigDecimal gridX, + BigDecimal gridY, + RegionCodeInfo regionCode + ) { + } } 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); + } +} From e25c6f98cce6254fb35e1b86fcc81a63aafaf403 Mon Sep 17 00:00:00 2001 From: Jimyeong Kim Date: Thu, 7 Aug 2025 19:54:46 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8feat:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A7=80=EC=97=AD=20=EB=B3=80=EA=B2=BD=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weather/controller/RegionController.java | 24 ++++++++++++++++++- .../weather/controller/WeatherController.java | 4 ++-- .../weather/converter/RegionConverter.java | 9 +++++++ .../weather/dto/request/RegionReqDTO.java | 6 +++++ .../weather/dto/response/RegionResDTO.java | 9 +++++++ .../service/command/RegionCommandService.java | 3 +++ .../command/RegionCommandServiceImpl.java | 23 ++++++++++++++++++ 7 files changed, 75 insertions(+), 3 deletions(-) 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 ad19673..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 @@ -192,7 +192,7 @@ public DefaultResponse searchRegions( return DefaultResponse.ok(response); } - @GetMapping("/user/current") + @GetMapping("/users/current") @Operation(summary = "현재 사용자 지역 조회 API by 지미", description = "로그인한 사용자의 현재 지역 정보를 조회합니다.") @ApiResponses(value = { @@ -207,6 +207,28 @@ public DefaultResponse getCurrentUserRegion(@Authentica 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 23d1ecf..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 @@ -168,4 +168,13 @@ public static RegionResDTO.UserRegion toUserRegion(Region region) { .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 682fef1..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 @@ -115,4 +115,13 @@ public record UserRegion( 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) {