diff --git a/build.gradle b/build.gradle index ff85221a..c63d56a4 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,9 @@ dependencies { annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // 날짜/시간 타입(JSON 직렬화 지원) + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java b/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java index 93e06297..6fc22fd1 100644 --- a/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java +++ b/src/main/java/com/demo/pteam/global/config/ApplicationConfig.java @@ -1,6 +1,7 @@ package com.demo.pteam.global.config; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,6 +9,8 @@ public class ApplicationConfig { @Bean public ObjectMapper objectMapper() { - return new ObjectMapper(); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper; } } diff --git a/src/main/java/com/demo/pteam/security/config/SecurityConfig.java b/src/main/java/com/demo/pteam/security/config/SecurityConfig.java index bd0deff2..8831020e 100644 --- a/src/main/java/com/demo/pteam/security/config/SecurityConfig.java +++ b/src/main/java/com/demo/pteam/security/config/SecurityConfig.java @@ -46,6 +46,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auths/login").permitAll() + .requestMatchers("/api/trainers/**").permitAll() // 임시 .anyRequest().authenticated()); return http.build(); diff --git a/src/main/java/com/demo/pteam/trainer/address/domain/Coordinates.java b/src/main/java/com/demo/pteam/trainer/address/domain/Coordinates.java index 67cc679a..92495f5d 100644 --- a/src/main/java/com/demo/pteam/trainer/address/domain/Coordinates.java +++ b/src/main/java/com/demo/pteam/trainer/address/domain/Coordinates.java @@ -1,10 +1,9 @@ package com.demo.pteam.trainer.address.domain; -import com.demo.pteam.global.exception.ApiException; -import com.demo.pteam.trainer.address.exception.TrainerAddressErrorCode; import lombok.Getter; import java.math.BigDecimal; +import java.util.Objects; @Getter public class Coordinates { @@ -12,17 +11,34 @@ public class Coordinates { private final BigDecimal longitude; public Coordinates(BigDecimal latitude, BigDecimal longitude) { - if (latitude == null || longitude == null) { - throw new ApiException(TrainerAddressErrorCode.COORDINATES_NULL); - } - if (latitude.abs().compareTo(BigDecimal.valueOf(90)) > 0) { - throw new ApiException(TrainerAddressErrorCode.INVALID_LATITUDE); - } - if (longitude.abs().compareTo(BigDecimal.valueOf(180)) > 0) { - throw new ApiException(TrainerAddressErrorCode.INVALID_LONGITUDE); - } - this.latitude = latitude; this.longitude = longitude; } + + public boolean isNull() { + return latitude == null || longitude == null; + } + + public boolean isInvalidLatitude() { + return latitude != null && latitude.abs().compareTo(BigDecimal.valueOf(90)) > 0; + } + + public boolean isInvalidLongitude() { + return longitude != null && longitude.abs().compareTo(BigDecimal.valueOf(180)) > 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Coordinates that = (Coordinates) o; + + return latitude.compareTo(that.latitude) == 0 && + longitude.compareTo(that.longitude) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(latitude, longitude); + } } diff --git a/src/main/java/com/demo/pteam/trainer/address/domain/TrainerAddress.java b/src/main/java/com/demo/pteam/trainer/address/domain/TrainerAddress.java index 8e8e9a79..99bb62da 100644 --- a/src/main/java/com/demo/pteam/trainer/address/domain/TrainerAddress.java +++ b/src/main/java/com/demo/pteam/trainer/address/domain/TrainerAddress.java @@ -1,18 +1,18 @@ package com.demo.pteam.trainer.address.domain; -import com.demo.pteam.global.exception.ApiException; -import com.demo.pteam.trainer.address.exception.TrainerAddressErrorCode; import lombok.Getter; +import java.util.Objects; + @Getter public class TrainerAddress { private final Long id; - private String numberAddress; + private final String numberAddress; private final String roadAddress; private final String detailAddress; - private String postalCode; - private Coordinates coordinates; + private final String postalCode; + private final Coordinates coordinates; public TrainerAddress(Long id, String numberAddress, String roadAddress, String detailAddress, String postalCode, Coordinates coordinates) { @@ -24,17 +24,34 @@ public TrainerAddress(Long id, String numberAddress, String roadAddress, String this.coordinates = coordinates; } - public static TrainerAddress from(String roadAddress, String detailAddress, Coordinates coordinates) { - return new TrainerAddress(null, null, roadAddress, detailAddress, null, coordinates); - } - - public void completeAddress(String numberAddress, String postalCode) { - this.numberAddress = numberAddress; - this.postalCode = postalCode; + public TrainerAddress withCompletedAddress(String numberAddress, String postalCode) { + return new TrainerAddress( + this.id, + numberAddress, + this.roadAddress, + this.detailAddress, + postalCode, + this.coordinates + ); } public boolean matchesRoadAddress(String kakaoRoadAddress) { return this.roadAddress.equals(kakaoRoadAddress); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TrainerAddress that = (TrainerAddress) o; + + return Objects.equals(this.roadAddress, that.roadAddress) && + Objects.equals(this.detailAddress, that.detailAddress) && + this.coordinates.equals(that.coordinates); + } + + @Override + public int hashCode() { + return Objects.hash(roadAddress, detailAddress, coordinates); + } } diff --git a/src/main/java/com/demo/pteam/trainer/address/exception/TrainerAddressErrorCode.java b/src/main/java/com/demo/pteam/trainer/address/exception/TrainerAddressErrorCode.java index 416402e1..34abb49f 100644 --- a/src/main/java/com/demo/pteam/trainer/address/exception/TrainerAddressErrorCode.java +++ b/src/main/java/com/demo/pteam/trainer/address/exception/TrainerAddressErrorCode.java @@ -7,22 +7,24 @@ @Getter public enum TrainerAddressErrorCode implements ErrorCode { - // 주소 - COORDINATES_NULL(HttpStatus.BAD_REQUEST, "L_001", "위도, 경도 값은 null일 수 없습니다."), - INVALID_LATITUDE(HttpStatus.BAD_REQUEST, "L_002", "위도 값은 -90 ~ 90 사이여야 합니다."), - INVALID_LONGITUDE(HttpStatus.BAD_REQUEST, "L_003", "경도 값은 -180 ~ 180 사이여야 합니다."), - ADDRESS_COORDINATE_MISMATCH(HttpStatus.BAD_REQUEST, "L_004", "위도/경도와 도로명 주소가 일치하지 않습니다."), - KAKAO_API_EMPTY_RESPONSE(HttpStatus.BAD_GATEWAY, "L_005", "카카오 지도 API 응답이 비어있습니다."), - ROAD_ADDRESS_NOT_FOUND(HttpStatus.BAD_REQUEST, "L_006", "도로명 주소를 찾을 수 없습니다."); + // 주소 + COORDINATES_NULL(HttpStatus.BAD_REQUEST, "L_001", "위도, 경도 값은 null일 수 없습니다."), + INVALID_LATITUDE(HttpStatus.BAD_REQUEST, "L_002", "위도 값은 -90 ~ 90 사이여야 합니다."), + INVALID_LONGITUDE(HttpStatus.BAD_REQUEST, "L_003", "경도 값은 -180 ~ 180 사이여야 합니다."), + ADDRESS_COORDINATE_MISMATCH(HttpStatus.BAD_REQUEST, "L_004", "위도/경도와 도로명 주소가 일치하지 않습니다."), + KAKAO_API_EMPTY_RESPONSE(HttpStatus.BAD_GATEWAY, "L_005", "카카오 지도 API 응답이 비어있습니다."), + ROAD_ADDRESS_NOT_FOUND(HttpStatus.BAD_REQUEST, "L_006", "도로명 주소를 찾을 수 없습니다."); +// ADDRESS_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "L_007", "주소 저장에 실패했습니다."), +// ADDRESS_NOT_FOUND(HttpStatus.NOT_FOUND, "L_008", "등록되어 있는 트레이너 주소가 없습니다."); - private final HttpStatus status; - private final String code; - private final String message; + private final HttpStatus status; + private final String code; + private final String message; - TrainerAddressErrorCode(HttpStatus status, String code, String message) { - this.status = status; - this.code = code; - this.message = message; - } + TrainerAddressErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } } diff --git a/src/main/java/com/demo/pteam/trainer/address/mapper/TrainerAddressMapper.java b/src/main/java/com/demo/pteam/trainer/address/mapper/TrainerAddressMapper.java index 18978aec..cb71e0d0 100644 --- a/src/main/java/com/demo/pteam/trainer/address/mapper/TrainerAddressMapper.java +++ b/src/main/java/com/demo/pteam/trainer/address/mapper/TrainerAddressMapper.java @@ -2,6 +2,7 @@ import com.demo.pteam.trainer.address.domain.Coordinates; import com.demo.pteam.trainer.address.domain.TrainerAddress; +import com.demo.pteam.trainer.address.repository.entity.TrainerAddressEntity; import com.demo.pteam.trainer.profile.controller.dto.TrainerProfileRequest; public class TrainerAddressMapper { @@ -9,11 +10,41 @@ public class TrainerAddressMapper { // 요청 DTO -> 도메인 변환 public static TrainerAddress toDomain(TrainerProfileRequest.Address dto) { Coordinates coordinates = new Coordinates(dto.getLatitude(), dto.getLongitude()); - return TrainerAddress.from( + return new TrainerAddress( + null, + null, dto.getRoadAddress(), dto.getDetailAddress(), + null, coordinates ); } -} + // 엔티티 -> 도메인 변환 + public static TrainerAddress toDomain(TrainerAddressEntity entity) { + Coordinates coordinates = new Coordinates(entity.getLatitude(), entity.getLongitude()); + return new TrainerAddress( + entity.getId(), + entity.getNumberAddress(), + entity.getRoadAddress(), + entity.getDetailAddress(), + entity.getPostalCode(), + coordinates + ); + } + + // 도메인 -> 엔티티 변환 + public static TrainerAddressEntity toEntity(TrainerAddress address) { + Coordinates coordinates = address.getCoordinates(); + + return TrainerAddressEntity.builder() + .numberAddress(address.getNumberAddress()) + .roadAddress(address.getRoadAddress()) + .detailAddress(address.getDetailAddress()) + .postalCode(address.getPostalCode()) + .latitude(coordinates.getLatitude()) + .longitude(coordinates.getLongitude()) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepository.java b/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepository.java index e08013fd..b66e441a 100644 --- a/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepository.java +++ b/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepository.java @@ -1,8 +1,9 @@ package com.demo.pteam.trainer.address.repository; import com.demo.pteam.trainer.address.domain.TrainerAddress; -import java.util.Optional; +import com.demo.pteam.trainer.address.repository.entity.TrainerAddressEntity; public interface TrainerAddressRepository { TrainerAddress save(TrainerAddress address); + TrainerAddressEntity saveForEntityReference(TrainerAddress address); } \ No newline at end of file diff --git a/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepositoryImpl.java b/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepositoryImpl.java index 8cb3502a..e6431349 100644 --- a/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepositoryImpl.java +++ b/src/main/java/com/demo/pteam/trainer/address/repository/TrainerAddressRepositoryImpl.java @@ -2,10 +2,13 @@ import com.demo.pteam.trainer.address.domain.Coordinates; import com.demo.pteam.trainer.address.domain.TrainerAddress; +import com.demo.pteam.trainer.address.mapper.TrainerAddressMapper; import com.demo.pteam.trainer.address.repository.entity.TrainerAddressEntity; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository @RequiredArgsConstructor public class TrainerAddressRepositoryImpl implements TrainerAddressRepository { @@ -14,15 +17,7 @@ public class TrainerAddressRepositoryImpl implements TrainerAddressRepository { @Override public TrainerAddress save(TrainerAddress address) { - TrainerAddressEntity entity = TrainerAddressEntity.builder() - .numberAddress(address.getNumberAddress()) - .roadAddress(address.getRoadAddress()) - .detailAddress(address.getDetailAddress()) - .postalCode(address.getPostalCode()) - .latitude(address.getCoordinates().getLatitude()) - .longitude(address.getCoordinates().getLongitude()) - .build(); - + TrainerAddressEntity entity = TrainerAddressMapper.toEntity(address); TrainerAddressEntity saved = jpaRepository.save(entity); Coordinates coordinates = new Coordinates( @@ -40,4 +35,9 @@ public TrainerAddress save(TrainerAddress address) { ); } + @Override + public TrainerAddressEntity saveForEntityReference(TrainerAddress address) { + return jpaRepository.save(TrainerAddressMapper.toEntity(address)); + } + } \ No newline at end of file diff --git a/src/main/java/com/demo/pteam/trainer/profile/controller/TrainerProfileController.java b/src/main/java/com/demo/pteam/trainer/profile/controller/TrainerProfileController.java index 6e4335b3..350027c0 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/controller/TrainerProfileController.java +++ b/src/main/java/com/demo/pteam/trainer/profile/controller/TrainerProfileController.java @@ -6,25 +6,43 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -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; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/api/trainers/me/profile") public class TrainerProfileController { - private final TrainerProfileService trainerProfileService; + private final TrainerProfileService trainerProfileService; - @PostMapping - public ResponseEntity> createProfile( - @RequestBody @Valid TrainerProfileRequest request - ) { - Long userId = 4L; // TODO: 로그인 사용자 임시 + /** + * 트레이너 프로필 등록 API + * @param request 트레이너 프로필 요청 DTO + * @return 등록 성공 여부 + */ + @PostMapping + public ResponseEntity> createProfile( + @RequestBody @Valid TrainerProfileRequest request + ) { + Long userId = 4L; // TODO: 로그인 사용자 임시 + + trainerProfileService.createProfile(request, userId); + return ResponseEntity.status(201).body(ApiResponse.created("트레이너 프로필이 성공적으로 등록되었습니다.")); + } + + /** + * 트레이너 프로필 수정 API + * @param request 트레이너 프로필 요청 DTO + * @return 수정 성공 여부 + */ + @PutMapping + public ResponseEntity> updateProfile( + @RequestBody @Valid TrainerProfileRequest request + ) { + Long userId = 3L; // TODO: 로그인 사용자 임시 + + trainerProfileService.updateProfile(request, userId); + return ResponseEntity.ok(ApiResponse.success("트레이너 프로필이 성공적으로 수정되었습니다.")); + } - trainerProfileService.createProfile(request, userId); - return ResponseEntity.status(201).body(ApiResponse.created("트레이너 프로필이 성공적으로 등록되었습니다.")); - } } \ No newline at end of file diff --git a/src/main/java/com/demo/pteam/trainer/profile/controller/dto/TrainerProfileRequest.java b/src/main/java/com/demo/pteam/trainer/profile/controller/dto/TrainerProfileRequest.java index e4c0f696..f805183b 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/controller/dto/TrainerProfileRequest.java +++ b/src/main/java/com/demo/pteam/trainer/profile/controller/dto/TrainerProfileRequest.java @@ -1,5 +1,6 @@ package com.demo.pteam.trainer.profile.controller.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -23,7 +24,10 @@ public class TrainerProfileRequest { @PositiveOrZero(message = "크레딧은 0 이상이어야 합니다.") private Integer credit; + @JsonFormat(pattern = "HH:mm") private LocalTime contactStartTime; + + @JsonFormat(pattern = "HH:mm") private LocalTime contactEndTime; @NotNull(message = "이름 공개 선택 여부는 필수입니다.") diff --git a/src/main/java/com/demo/pteam/trainer/profile/domain/TrainerProfile.java b/src/main/java/com/demo/pteam/trainer/profile/domain/TrainerProfile.java index e93d89f3..761ebb75 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/domain/TrainerProfile.java +++ b/src/main/java/com/demo/pteam/trainer/profile/domain/TrainerProfile.java @@ -1,9 +1,6 @@ package com.demo.pteam.trainer.profile.domain; -import com.demo.pteam.global.exception.ApiException; -import com.demo.pteam.trainer.profile.exception.TrainerProfileErrorCode; import lombok.Getter; - import java.time.LocalTime; @Getter @@ -11,6 +8,8 @@ public class TrainerProfile { private final Long id; private final Long userId; + private final String name; + private final String nickname; private final Long addressId; private final String profileImg; private final String intro; @@ -19,10 +18,13 @@ public class TrainerProfile { private final LocalTime contactEndTime; private final Boolean isNamePublic; - public TrainerProfile(Long id, Long userId, Long addressId, String profileImg, String intro, Integer credit, + public TrainerProfile(Long id, Long userId, String name, String nickname, Long addressId, + String profileImg, String intro, Integer credit, LocalTime contactStartTime, LocalTime contactEndTime, Boolean isNamePublic) { this.id = id; this.userId = userId; + this.name = name; + this.nickname = nickname; this.addressId = addressId; this.profileImg = profileImg; this.intro = intro; @@ -32,31 +34,17 @@ public TrainerProfile(Long id, Long userId, Long addressId, String profileImg, S this.isNamePublic = isNamePublic; } - public static TrainerProfile of(Long userId, Long addressId, String profileImg, String intro, Integer credit, - LocalTime contactStartTime, LocalTime contactEndTime, Boolean isNamePublic) { - return new TrainerProfile(null, userId, addressId, profileImg, intro, credit, - contactStartTime, contactEndTime, isNamePublic); - } - - public boolean isNameVisible() { - return this.isNamePublic; - } - - public boolean isContactTimePairValid() { - return (contactStartTime == null && contactEndTime == null) || - (contactStartTime != null && contactEndTime != null); - } - - public boolean hasContactTime() { - return contactStartTime != null && contactEndTime != null; + public String getDisplayName() { + return isNamePublic ? name : nickname; } - public boolean isValidContatTimeRange() { - return hasContactTime() && !contactStartTime.isAfter(contactEndTime); + public boolean isInvalidContactTimePair() { + return !(contactStartTime == null && contactEndTime == null) && + !(contactStartTime != null && contactEndTime != null); } - public boolean isProfileComplete() { - return userId != null && isNamePublic != null; + public boolean isInvalidContactTimeRange() { + return contactStartTime != null && contactEndTime != null && contactStartTime.isAfter(contactEndTime); } } diff --git a/src/main/java/com/demo/pteam/trainer/profile/mapper/TrainerProfileMapper.java b/src/main/java/com/demo/pteam/trainer/profile/mapper/TrainerProfileMapper.java new file mode 100644 index 00000000..355ad2c6 --- /dev/null +++ b/src/main/java/com/demo/pteam/trainer/profile/mapper/TrainerProfileMapper.java @@ -0,0 +1,64 @@ +package com.demo.pteam.trainer.profile.mapper; + + +import com.demo.pteam.authentication.repository.entity.AccountEntity; +import com.demo.pteam.trainer.address.repository.entity.TrainerAddressEntity; +import com.demo.pteam.trainer.profile.controller.dto.TrainerProfileRequest; +import com.demo.pteam.trainer.profile.domain.TrainerProfile; +import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; + +public class TrainerProfileMapper { + + // 프로필 도메인 -> 프로필 엔티티 + public static TrainerProfileEntity toEntity( + TrainerProfile profile, + AccountEntity trainer, + TrainerAddressEntity address) { + + return TrainerProfileEntity.builder() + .trainer(trainer) + .address(address) + .profileImg(profile.getProfileImg()) + .intro(profile.getIntro()) + .credit(profile.getCredit()) + .contactStartTime(profile.getContactStartTime()) + .contactEndTime(profile.getContactEndTime()) + .isNamePublic(profile.getIsNamePublic()) + .build(); + } + + // 프로필 엔티티 -> 프로필 도메인 + public static TrainerProfile toDomain(TrainerProfileEntity entity) { + return new TrainerProfile( + entity.getId(), + entity.getTrainer().getId(), + entity.getTrainer().getName(), + entity.getTrainer().getNickname(), + entity.getAddress().getId(), + entity.getProfileImg(), + entity.getIntro(), + entity.getCredit(), + entity.getContactStartTime(), + entity.getContactEndTime(), + entity.getIsNamePublic() + ); + } + + // 프로필 요청 DTO -> 프로필 도메인 + public static TrainerProfile toDomain(TrainerProfileRequest dto, Long userId, Long addressId) { + return new TrainerProfile( + null, + userId, + null, + null, + addressId, + dto.getProfileImg(), + dto.getIntro(), + dto.getCredit(), + dto.getContactStartTime(), + dto.getContactEndTime(), + dto.getIsNamePublic() + ); + } + +} diff --git a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepository.java b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepository.java index 803f92b7..982db1b1 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepository.java +++ b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepository.java @@ -3,5 +3,5 @@ import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface TrainerProfileJPARepository extends JpaRepository { +public interface TrainerProfileJPARepository extends JpaRepository, TrainerProfileJPARepositoryCustom { } diff --git a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepositoryCustom.java b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepositoryCustom.java new file mode 100644 index 00000000..08506074 --- /dev/null +++ b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepositoryCustom.java @@ -0,0 +1,9 @@ +package com.demo.pteam.trainer.profile.repository; + +import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; + +import java.util.Optional; + +public interface TrainerProfileJPARepositoryCustom { + Optional findDetailedProfileEntityByUserId(Long userId); +} diff --git a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepositoryCustomImpl.java b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepositoryCustomImpl.java new file mode 100644 index 00000000..36ae8193 --- /dev/null +++ b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileJPARepositoryCustomImpl.java @@ -0,0 +1,34 @@ +package com.demo.pteam.trainer.profile.repository; + +import com.demo.pteam.authentication.repository.entity.QAccountEntity; +import com.demo.pteam.trainer.address.repository.entity.QTrainerAddressEntity; +import com.demo.pteam.trainer.profile.repository.entity.QTrainerProfileEntity; +import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class TrainerProfileJPARepositoryCustomImpl implements TrainerProfileJPARepositoryCustom { + private final JPAQueryFactory queryFactory; + + @Override + public Optional findDetailedProfileEntityByUserId(Long userId) { + QTrainerProfileEntity profile = QTrainerProfileEntity.trainerProfileEntity; + QTrainerAddressEntity address = QTrainerAddressEntity.trainerAddressEntity; + QAccountEntity account = QAccountEntity.accountEntity; + + return Optional.ofNullable( + queryFactory + .select(profile) + .from(profile) + .leftJoin(profile.address, address).fetchJoin() + .leftJoin(profile.trainer, account).fetchJoin() + .where(profile.trainer.id.eq(userId)) + .fetchOne() + ); + } +} diff --git a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepository.java b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepository.java index ed65e752..34931e63 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepository.java +++ b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepository.java @@ -1,7 +1,11 @@ package com.demo.pteam.trainer.profile.repository; import com.demo.pteam.trainer.profile.domain.TrainerProfile; +import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; + +import java.util.Optional; public interface TrainerProfileRepository { void save(TrainerProfile trainerProfile); + Optional findEntityByUserId(Long userId); } diff --git a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepositoryImpl.java b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepositoryImpl.java index b10e7d1d..4f7c214e 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepositoryImpl.java +++ b/src/main/java/com/demo/pteam/trainer/profile/repository/TrainerProfileRepositoryImpl.java @@ -3,35 +3,33 @@ import com.demo.pteam.authentication.repository.entity.AccountEntity; import com.demo.pteam.trainer.address.repository.entity.TrainerAddressEntity; import com.demo.pteam.trainer.profile.domain.TrainerProfile; +import com.demo.pteam.trainer.profile.mapper.TrainerProfileMapper; import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.Optional; + @RequiredArgsConstructor @Repository public class TrainerProfileRepositoryImpl implements TrainerProfileRepository { - private final TrainerProfileJPARepository trainerProfileJPARepository; - private final EntityManager em; + private final TrainerProfileJPARepository trainerProfileJPARepository; + private final EntityManager em; - @Override - public void save(TrainerProfile profile) { + @Override + public void save(TrainerProfile profile) { - AccountEntity trainer = em.getReference(AccountEntity.class, profile.getUserId()); - TrainerAddressEntity address = em.getReference(TrainerAddressEntity.class, profile.getAddressId()); + AccountEntity trainer = em.getReference(AccountEntity.class, profile.getUserId()); + TrainerAddressEntity address = em.getReference(TrainerAddressEntity.class, profile.getAddressId()); - TrainerProfileEntity entity = TrainerProfileEntity.builder() - .trainer(trainer) - .address(address) - .profileImg(profile.getProfileImg()) - .intro(profile.getIntro()) - .credit(profile.getCredit()) - .contactStartTime(profile.getContactStartTime()) - .contactEndTime(profile.getContactEndTime()) - .isNamePublic(profile.getIsNamePublic()) - .build(); + TrainerProfileEntity entity = TrainerProfileMapper.toEntity(profile, trainer, address); + trainerProfileJPARepository.save(entity); + } - trainerProfileJPARepository.save(entity); - } + @Override + public Optional findEntityByUserId(Long userId) { + return trainerProfileJPARepository.findDetailedProfileEntityByUserId(userId); + } } \ No newline at end of file diff --git a/src/main/java/com/demo/pteam/trainer/profile/repository/entity/TrainerProfileEntity.java b/src/main/java/com/demo/pteam/trainer/profile/repository/entity/TrainerProfileEntity.java index 1c38ec08..d7cd70d7 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/repository/entity/TrainerProfileEntity.java +++ b/src/main/java/com/demo/pteam/trainer/profile/repository/entity/TrainerProfileEntity.java @@ -17,45 +17,71 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TrainerProfileEntity extends SoftDeletableEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private AccountEntity trainer; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "address_id") - private TrainerAddressEntity address; - - @Lob - private String profileImg; - - @Lob - private String intro; - - @Column(columnDefinition = "INT DEFAULT 0") - private Integer credit; - - private LocalTime contactStartTime; - private LocalTime contactEndTime; - - @Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE") - private Boolean isNamePublic; - - @Builder - public TrainerProfileEntity(AccountEntity trainer, TrainerAddressEntity address, String profileImg, - String intro, Integer credit, LocalTime contactStartTime, - LocalTime contactEndTime, Boolean isNamePublic) { - this.trainer = trainer; - this.address = address; - this.profileImg = profileImg; - this.intro = intro; - this.credit = credit != null ? credit : 0; - this.contactStartTime = contactStartTime; - this.contactEndTime = contactEndTime; - this.isNamePublic = isNamePublic != null ? isNamePublic : true; - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, unique = true) + private AccountEntity trainer; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "address_id") + private TrainerAddressEntity address; + + @Lob + private String profileImg; + + @Lob + private String intro; + + @Column(columnDefinition = "INT DEFAULT 0") + private Integer credit; + + private LocalTime contactStartTime; + private LocalTime contactEndTime; + + @Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE") + private Boolean isNamePublic; + + @Builder + public TrainerProfileEntity(AccountEntity trainer, TrainerAddressEntity address, String profileImg, + String intro, Integer credit, LocalTime contactStartTime, + LocalTime contactEndTime, Boolean isNamePublic) { + this.trainer = trainer; + this.address = address; + this.profileImg = profileImg; + this.intro = intro; + this.credit = credit != null ? credit : 0; + this.contactStartTime = contactStartTime; + this.contactEndTime = contactEndTime; + this.isNamePublic = isNamePublic != null ? isNamePublic : true; + } + + // 수정 관련 메서드 + public void updateIntro(String intro) { + this.intro = intro; + } + + public void updateProfileImg(String profileImg) { + this.profileImg = profileImg; + } + + public void updateCredit(Integer credit) { + this.credit = credit; + } + + public void updateContactTime(LocalTime start, LocalTime end) { + this.contactStartTime = start; + this.contactEndTime = end; + } + + public void updateNamePublic(Boolean isPublic) { + this.isNamePublic = isPublic; + } + + public void updateAddress(TrainerAddressEntity address) { + this.address = address; + } } diff --git a/src/main/java/com/demo/pteam/trainer/profile/service/TrainerProfileService.java b/src/main/java/com/demo/pteam/trainer/profile/service/TrainerProfileService.java index a597d894..8760675b 100644 --- a/src/main/java/com/demo/pteam/trainer/profile/service/TrainerProfileService.java +++ b/src/main/java/com/demo/pteam/trainer/profile/service/TrainerProfileService.java @@ -3,31 +3,54 @@ import com.demo.pteam.external.kakao.dto.KakaoGeoResponse; import com.demo.pteam.external.kakao.service.KakaoMapService; import com.demo.pteam.global.exception.ApiException; +import com.demo.pteam.trainer.address.domain.Coordinates; import com.demo.pteam.trainer.address.domain.TrainerAddress; import com.demo.pteam.trainer.address.exception.TrainerAddressErrorCode; import com.demo.pteam.trainer.address.mapper.TrainerAddressMapper; import com.demo.pteam.trainer.address.repository.TrainerAddressRepository; +import com.demo.pteam.trainer.address.repository.entity.TrainerAddressEntity; import com.demo.pteam.trainer.profile.controller.dto.TrainerProfileRequest; import com.demo.pteam.trainer.profile.domain.TrainerProfile; import com.demo.pteam.trainer.profile.exception.TrainerProfileErrorCode; +import com.demo.pteam.trainer.profile.mapper.TrainerProfileMapper; import com.demo.pteam.trainer.profile.repository.TrainerProfileRepository; +import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional public class TrainerProfileService { private final TrainerProfileRepository trainerProfileRepository; private final TrainerAddressRepository trainerAddressRepository; private final KakaoMapService kakaoMapService; - @Transactional + /** + * 트레이너 프로필 등록 + * @param request 트레이너 프로필 요청 DTO + * @param userId 로그인 사용자 id + */ public void createProfile(TrainerProfileRequest request, Long userId) { + // TODO: '회원'이 아닌 '트레이너' 확인 여부 TrainerAddress newAddress = TrainerAddressMapper.toDomain(request.getAddress()); + Coordinates coordinates = newAddress.getCoordinates(); + + if (coordinates.isNull()) { + throw new ApiException(TrainerAddressErrorCode.COORDINATES_NULL); + } + if (coordinates.isInvalidLatitude()) { + throw new ApiException(TrainerAddressErrorCode.INVALID_LATITUDE); + } + if (coordinates.isInvalidLongitude()) { + throw new ApiException(TrainerAddressErrorCode.INVALID_LONGITUDE); + } + KakaoGeoResponse response = kakaoMapService.requestCoordToAddress( newAddress.getCoordinates().getLatitude(), newAddress.getCoordinates().getLongitude() @@ -43,37 +66,89 @@ public void createProfile(TrainerProfileRequest request, Long userId) { throw new ApiException(TrainerAddressErrorCode.ADDRESS_COORDINATE_MISMATCH); } - newAddress.completeAddress( + TrainerAddress completedAddress = newAddress.withCompletedAddress( document.getAddress().getAddressName(), document.getRoadAddress().getZoneNo() ); - TrainerAddress savedAddress = trainerAddressRepository.save(newAddress); - - TrainerProfile profile = TrainerProfile.of( - userId, - savedAddress.getId(), - request.getProfileImg(), - request.getIntro(), - request.getCredit(), - request.getContactStartTime(), - request.getContactEndTime(), - request.getIsNamePublic() - ); + TrainerAddress savedAddress = trainerAddressRepository.save(completedAddress); - if (!profile.isProfileComplete()) { - throw new ApiException(TrainerProfileErrorCode.PROFILE_INCOMPLETE); - } + // name, nickname 임시 + TrainerProfile profile = TrainerProfileMapper.toDomain(request, userId, savedAddress.getId()); - if (!profile.isContactTimePairValid()) { + if (profile.isInvalidContactTimePair()) { throw new ApiException(TrainerProfileErrorCode.INVALID_CONTACT_TIME_PAIR); } - if (!profile.isValidContatTimeRange()) { + if (profile.isInvalidContactTimeRange()) { throw new ApiException(TrainerProfileErrorCode.INVALID_CONTACT_TIME_RANGE); } trainerProfileRepository.save(profile); } + /** + * 트레이너 프로필 수정 + * @param request 트레이너 프로필 요청 DTO + * @param userId 로그인 사용자 id + */ + public void updateProfile(@Valid TrainerProfileRequest request, Long userId) { + TrainerProfileEntity entity = trainerProfileRepository.findEntityByUserId(userId) + .orElseThrow(() -> new ApiException(TrainerProfileErrorCode.PROFILE_NOT_FOUND)); + + // DB에 저장되어 있는 주소와 요청 주소가 같은지 확인 + TrainerAddress currentAddress = TrainerAddressMapper.toDomain(entity.getAddress()); // 기존 주소 + TrainerAddress newAddress = TrainerAddressMapper.toDomain(request.getAddress()); // 요청 주소 + + TrainerAddressEntity newAddressEntity = entity.getAddress(); + + boolean isAddressChanged = !newAddress.equals(currentAddress); + + if (isAddressChanged) { + KakaoGeoResponse response = kakaoMapService.requestCoordToAddress( + newAddress.getCoordinates().getLatitude(), + newAddress.getCoordinates().getLongitude() + ); + + KakaoGeoResponse.Document document = response.getDocuments().get(0); + + if (document.getRoadAddress() == null) { + throw new ApiException(TrainerAddressErrorCode.ROAD_ADDRESS_NOT_FOUND); + } + + if (!newAddress.matchesRoadAddress(document.getRoadAddress().getAddressName())) { + throw new ApiException(TrainerAddressErrorCode.ADDRESS_COORDINATE_MISMATCH); + } + + TrainerAddress completedAddress = newAddress.withCompletedAddress( + document.getAddress().getAddressName(), + document.getRoadAddress().getZoneNo() + ); + + newAddressEntity = trainerAddressRepository.saveForEntityReference(completedAddress); // 영속 상태 반환 + } + + // 나머지 트레이너 프로필 값 업데이트 + entity.updateIntro(request.getIntro()); + entity.updateProfileImg(request.getProfileImg()); + entity.updateCredit(request.getCredit()); + entity.updateContactTime(request.getContactStartTime(), request.getContactEndTime()); + entity.updateNamePublic(request.getIsNamePublic()); + + if (isAddressChanged) { + entity.updateAddress(newAddressEntity); + } + + TrainerProfile domain = TrainerProfileMapper.toDomain(entity); + + if (domain.isInvalidContactTimePair()) { + throw new ApiException(TrainerProfileErrorCode.INVALID_CONTACT_TIME_PAIR); + } + + if (domain.isInvalidContactTimeRange()) { + throw new ApiException(TrainerProfileErrorCode.INVALID_CONTACT_TIME_RANGE); + } + + } + }