diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/application/ParentLocationService.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/application/ParentLocationService.java new file mode 100644 index 0000000..8510f12 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/application/ParentLocationService.java @@ -0,0 +1,69 @@ +package com.widyu.location.parentlocation.application; + +import com.widyu.global.error.BusinessException; +import com.widyu.global.error.ErrorCode; +import com.widyu.location.parentlocation.dto.request.ParentLocationCreateRequest; +import com.widyu.location.parentlocation.dto.response.LocationInfo; +import com.widyu.location.parentlocation.dto.response.ParentLocationResponse; +import com.widyu.location.parentlocation.dto.response.SeniorWithLocationsResponse; +import com.widyu.location.parentlocation.repository.ParentLocationRepository; +import com.widyu.member.FamilyConnection; +import com.widyu.member.Member; +import com.widyu.member.repository.FamilyConnectionRepository; +import com.widyu.member.repository.MemberRepository; +import com.widyu.parentlocation.ParentLocation; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ParentLocationService { + + private final ParentLocationRepository parentLocationRepository; + private final MemberRepository memberRepository; + private final FamilyConnectionRepository familyConnectionRepository; + + public List findAllByGuardianId(Long guardianId) { + + List familyConnections = familyConnectionRepository + .findAllByGuardianIdWithSeniorAndMember(guardianId); + + return familyConnections.stream() + .map(fc -> { + Member senior = fc.getSenior().getMember(); + List locations = parentLocationRepository.findAllByMember(senior) + .stream() + .map(LocationInfo::of) + .collect(Collectors.toList()); + return SeniorWithLocationsResponse.of(senior, locations); + }) + .collect(Collectors.toList()); + } + + @Transactional + public void create(ParentLocationCreateRequest request) { + Member seniorMember = memberRepository.findById(request.memberId()) + .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST, "존재하지 않는 회원입니다.")); + + if (parentLocationRepository.existsByMemberAndPlaceAddress(seniorMember, request.placeAddress())) { + throw new BusinessException(ErrorCode.BAD_REQUEST, "이미 등록된 주소입니다."); + } + + parentLocationRepository.save(request.toEntity(seniorMember)); + } + + @Transactional + public void delete(Long memberId, Long parentLocationId) { + Member seniorMember = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST, "존재하지 않는 회원입니다.")); + + ParentLocation location = parentLocationRepository.findByIdAndMember(parentLocationId, seniorMember) + .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST, "이미 삭제된 장소입니다.")); + + parentLocationRepository.delete(location); + } +} diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/controller/ParentLocationController.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/controller/ParentLocationController.java new file mode 100644 index 0000000..9253970 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/controller/ParentLocationController.java @@ -0,0 +1,60 @@ +package com.widyu.location.parentlocation.controller; + +import com.widyu.global.response.ApiResponseTemplate; +import com.widyu.location.parentlocation.application.ParentLocationService; +import com.widyu.location.parentlocation.controller.docs.ParentLocationDocs; +import com.widyu.location.parentlocation.dto.request.ParentLocationCreateRequest; +import com.widyu.location.parentlocation.dto.response.ParentLocationResponse; +import com.widyu.location.parentlocation.dto.response.SeniorWithLocationsResponse; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/goals/parent-locations") +public class ParentLocationController implements ParentLocationDocs { + + private final ParentLocationService parentLocationService; + + @PostMapping + public ApiResponseTemplate createParentLocation( + @Valid @RequestBody ParentLocationCreateRequest request + ) { + parentLocationService.create(request); + return ApiResponseTemplate.ok() + .code("PLO_2001") + .message("부모님 장소가 등록되었습니다.") + .build(); + } + + @DeleteMapping("/{memberId}/{parentLocationId}") + public ApiResponseTemplate deleteParentLocation( + @PathVariable Long memberId, + @PathVariable Long parentLocationId + ) { + parentLocationService.delete(memberId, parentLocationId); + return ApiResponseTemplate.ok() + .code("PLO_2002") + .message("부모님 장소가 삭제되었습니다.") + .build(); + } + + @GetMapping("/{guardianId}") + public ApiResponseTemplate> getParentLocations( + @PathVariable Long guardianId + ) { + List data = parentLocationService.findAllByGuardianId(guardianId); + return ApiResponseTemplate.ok() + .code("PLO_2000") + .message("부모님 장소 목록이 조회되었습니다.") + .body(data); + } +} diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/controller/docs/ParentLocationDocs.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/controller/docs/ParentLocationDocs.java new file mode 100644 index 0000000..8215df3 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/controller/docs/ParentLocationDocs.java @@ -0,0 +1,145 @@ +package com.widyu.location.parentlocation.controller.docs; + +import com.widyu.global.response.ApiResponseTemplate; +import com.widyu.location.parentlocation.dto.request.ParentLocationCreateRequest; +import com.widyu.location.parentlocation.dto.response.ParentLocationResponse; +import com.widyu.location.parentlocation.dto.response.SeniorWithLocationsResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; + +@Tag(name = "Parent-Location", description = "부모님 장소 관리 API") +public interface ParentLocationDocs { + + @Operation( + summary = "부모님 장소 등록", + description = "시니어 회원의 자주 가는 장소를 등록합니다. 장소 타입은 HOME(집) 또는 OTHER(기타)로 구분됩니다." + ) + @RequestBody( + description = "부모님 장소 등록 정보", + required = true, + content = @Content( + schema = @Schema(implementation = ParentLocationCreateRequest.class), + examples = @ExampleObject( + value = """ + { + "memberId": 1, + "locationType": "HOME", + "placeAddress": "서울특별시 강남구 테헤란로 123", + "latitude": "37.5012", + "longitude": "127.0396" + } + """ + ) + ) + ) + @ApiResponse( + responseCode = "200", + description = "부모님 장소 등록 성공", + content = @Content( + schema = @Schema(implementation = ApiResponseTemplate.class), + examples = @ExampleObject( + value = """ + { + "code": "PLO_2001", + "message": "부모님 장소가 등록되었습니다.", + "data": null + } + """ + ) + ) + ) + ApiResponseTemplate createParentLocation(ParentLocationCreateRequest request); + + @Operation( + summary = "부모님 장소 삭제", + description = "등록된 부모님 장소를 삭제합니다. 소프트 삭제 방식으로 상태만 변경됩니다." + ) + @ApiResponse( + responseCode = "200", + description = "부모님 장소 삭제 성공", + content = @Content( + schema = @Schema(implementation = ApiResponseTemplate.class), + examples = @ExampleObject( + value = """ + { + "code": "PLO_2002", + "message": "부모님 장소가 삭제되었습니다.", + "data": null + } + """ + ) + ) + ) + ApiResponseTemplate deleteParentLocation( + @Parameter(description = "시니어 회원 ID", required = true, example = "1") + Long memberId, + @Parameter(description = "삭제할 부모님 장소 ID", required = true, example = "1") + Long parentLocationId + ); + + @Operation( + summary = "부모님 장소 목록 조회", + description = "보호자가 관리하는 모든 부모님들의 등록된 장소 목록을 부모님별로 그룹핑하여 조회합니다." + ) + @ApiResponse( + responseCode = "200", + description = "부모님 장소 목록 조회 성공", + content = @Content( + schema = @Schema(implementation = ApiResponseTemplate.class), + examples = @ExampleObject( + value = """ + { + "code": "PLO_2000", + "message": "부모님 장소 목록이 조회되었습니다.", + "data": [ + { + "memberId": 1, + "memberName": "홍길동", + "locations": [ + { + "parentLocationId": 1, + "locationType": "HOME", + "placeAddress": "서울특별시 강남구 테헤란로 123", + "latitude": "37.5012", + "longitude": "127.0396" + }, + { + "parentLocationId": 2, + "locationType": "OTHER", + "placeAddress": "서울특별시 마포구 월드컵북로 123", + "latitude": "37.5665", + "longitude": "126.9780" + } + ] + }, + { + "memberId": 2, + "memberName": "김영희", + "locations": [ + { + "parentLocationId": 3, + "locationType": "HOME", + "placeAddress": "서울특별시 송파구 올림픽로 456", + "latitude": "37.5145", + "longitude": "127.1059" + } + ] + } + ] + } + """ + ) + ) + ) + ApiResponseTemplate> getParentLocations( + @Parameter(description = "보호자 회원 ID", required = true, example = "1") + Long guardianId + ); +} diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/request/ParentLocationCreateRequest.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/request/ParentLocationCreateRequest.java new file mode 100644 index 0000000..466c5e7 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/request/ParentLocationCreateRequest.java @@ -0,0 +1,32 @@ +package com.widyu.location.parentlocation.dto.request; + +import com.widyu.global.entity.Status; +import com.widyu.member.Member; +import com.widyu.parentlocation.LocationType; +import com.widyu.parentlocation.ParentLocation; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ParentLocationCreateRequest( + @NotNull(message = "시니어 회원 ID는 필수입니다.") + Long memberId, + @NotNull(message = "장소 타입은 필수입니다.") + LocationType locationType, + @NotBlank(message = "장소 주소는 필수입니다.") + String placeAddress, + @NotBlank(message = "위도는 필수입니다.") + String latitude, + @NotBlank(message = "경도는 필수입니다.") + String longitude +) { + public ParentLocation toEntity(Member seniorMember) { + return ParentLocation.builder() + .member(seniorMember) + .locationType(locationType) + .placeAddress(placeAddress) + .latitude(latitude) + .longitude(longitude) + .status(Status.ACTIVE) + .build(); + } +} diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/LocationInfo.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/LocationInfo.java new file mode 100644 index 0000000..9b4a4f1 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/LocationInfo.java @@ -0,0 +1,28 @@ +package com.widyu.location.parentlocation.dto.response; + +import com.widyu.parentlocation.LocationType; +import com.widyu.parentlocation.ParentLocation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class LocationInfo { + private Long parentLocationId; + private LocationType locationType; + private String placeAddress; + private String latitude; + private String longitude; + + public static LocationInfo of(ParentLocation parentLocation) { + return LocationInfo.builder() + .parentLocationId(parentLocation.getId()) + .locationType(parentLocation.getLocationType()) + .placeAddress(parentLocation.getPlaceAddress()) + .latitude(parentLocation.getLatitude()) + .longitude(parentLocation.getLongitude()) + .build(); + } +} diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/ParentLocationResponse.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/ParentLocationResponse.java new file mode 100644 index 0000000..e977b75 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/ParentLocationResponse.java @@ -0,0 +1,32 @@ +package com.widyu.location.parentlocation.dto.response; + +import com.widyu.parentlocation.LocationType; +import com.widyu.parentlocation.ParentLocation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class ParentLocationResponse { + private Long parentLocationId; + private Long memberId; + private String memberName; + private LocationType locationType; + private String placeAddress; + private String latitude; + private String longitude; + + public static ParentLocationResponse of(ParentLocation parentLocation) { + return ParentLocationResponse.builder() + .parentLocationId(parentLocation.getId()) + .memberId(parentLocation.getMember().getId()) + .memberName(parentLocation.getMember().getName()) + .locationType(parentLocation.getLocationType()) + .placeAddress(parentLocation.getPlaceAddress()) + .latitude(parentLocation.getLatitude()) + .longitude(parentLocation.getLongitude()) + .build(); + } +} diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/SeniorWithLocationsResponse.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/SeniorWithLocationsResponse.java new file mode 100644 index 0000000..3f4d3f5 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/dto/response/SeniorWithLocationsResponse.java @@ -0,0 +1,24 @@ +package com.widyu.location.parentlocation.dto.response; + +import com.widyu.member.Member; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class SeniorWithLocationsResponse { + private Long memberId; + private String memberName; + private List locations; + + public static SeniorWithLocationsResponse of(Member senior, List locations) { + return SeniorWithLocationsResponse.builder() + .memberId(senior.getId()) + .memberName(senior.getName()) + .locations(locations) + .build(); + } +} diff --git a/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/repository/ParentLocationRepository.java b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/repository/ParentLocationRepository.java new file mode 100644 index 0000000..eebe866 --- /dev/null +++ b/backend/widyu-api/src/main/java/com/widyu/location/parentlocation/repository/ParentLocationRepository.java @@ -0,0 +1,17 @@ +package com.widyu.location.parentlocation.repository; + +import com.widyu.member.Member; +import com.widyu.parentlocation.LocationType; +import com.widyu.parentlocation.ParentLocation; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ParentLocationRepository extends JpaRepository { + + boolean existsByMemberAndPlaceAddress(Member member, String placeAddress); + + Optional findByIdAndMember(Long id, Member member); + + List findAllByMember(Member member); +} diff --git a/backend/widyu-domain/src/main/java/com/widyu/parentlocation/LocationType.java b/backend/widyu-domain/src/main/java/com/widyu/parentlocation/LocationType.java new file mode 100644 index 0000000..4291b8b --- /dev/null +++ b/backend/widyu-domain/src/main/java/com/widyu/parentlocation/LocationType.java @@ -0,0 +1,13 @@ +package com.widyu.parentlocation; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum LocationType { + HOME("집"), + OTHER("기타"); + + private final String description; +} diff --git a/backend/widyu-domain/src/main/java/com/widyu/parentlocation/ParentLocation.java b/backend/widyu-domain/src/main/java/com/widyu/parentlocation/ParentLocation.java new file mode 100644 index 0000000..3758048 --- /dev/null +++ b/backend/widyu-domain/src/main/java/com/widyu/parentlocation/ParentLocation.java @@ -0,0 +1,57 @@ +package com.widyu.parentlocation; + +import com.widyu.global.entity.BaseTimeEntity; +import com.widyu.global.entity.Status; +import com.widyu.member.Member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE parent_location SET status = 'DELETED' WHERE parent_location_id = ?") +@Where(clause = "status = 'ACTIVE'") +public class ParentLocation extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "parent_location_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private LocationType locationType; + + @Column(nullable = false) + private String placeAddress; + + @Column(nullable = false) + private String latitude; + + @Column(nullable = false) + private String longitude; + + @Enumerated(EnumType.STRING) + private Status status; +}