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..1806247c 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,7 +1,5 @@ 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; @@ -12,17 +10,20 @@ 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; + } + } 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..737db38e 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 @@ -8,11 +8,11 @@ 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,13 +24,15 @@ 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) { 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..3b303b6a 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 @@ -9,9 +9,13 @@ 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 ); } 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..1ef526ad 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,39 @@ 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("트레이너 프로필이 성공적으로 등록되었습니다.")); - } + trainerProfileService.createProfile(request, userId); + return ResponseEntity.status(201).body(ApiResponse.created("트레이너 프로필이 성공적으로 등록되었습니다.")); + } + + /** + * 트레이너 프로필 삭제 API + * @return 삭제 성공 여부 + */ + @DeleteMapping + public ResponseEntity> deleteProfile() { + Long userId = 3L; // TODO: 로그인 사용자 임시 + + trainerProfileService.deleteProfile(userId); + return ResponseEntity.ok(ApiResponse.success("트레이너 프로필이 성공적으로 삭제되었습니다.")); + } } \ No newline at end of file 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..190c814e 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,7 +1,5 @@ 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; @@ -11,6 +9,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 +19,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 +35,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 String getDisplayName() { + return isNamePublic ? name : nickname; } - public boolean isNameVisible() { - return this.isNamePublic; + public boolean isInvalidContactTimePair() { + return !(contactStartTime == null && contactEndTime == null) && + !(contactStartTime != null && contactEndTime != null); } - public boolean isContactTimePairValid() { - return (contactStartTime == null && contactEndTime == null) || - (contactStartTime != null && contactEndTime != null); - } - - public boolean hasContactTime() { - return contactStartTime != null && contactEndTime != null; - } - - public boolean isValidContatTimeRange() { - return hasContactTime() && !contactStartTime.isAfter(contactEndTime); - } - - 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..bc9f6fbd --- /dev/null +++ b/src/main/java/com/demo/pteam/trainer/profile/mapper/TrainerProfileMapper.java @@ -0,0 +1,63 @@ +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..5d251403 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,8 @@ import com.demo.pteam.trainer.profile.repository.entity.TrainerProfileEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface TrainerProfileJPARepository extends JpaRepository { + Optional findByTrainerId(Long userId); } 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..ca494bf5 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 @@ -2,6 +2,10 @@ import com.demo.pteam.trainer.profile.domain.TrainerProfile; +import java.util.Optional; + public interface TrainerProfileRepository { void save(TrainerProfile trainerProfile); + Optional findByUserId(Long userId); + void delete(Long profileId); } 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..77f9ddb4 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 @@ -2,36 +2,42 @@ import com.demo.pteam.authentication.repository.entity.AccountEntity; import com.demo.pteam.trainer.address.repository.entity.TrainerAddressEntity; +import com.demo.pteam.trainer.profile.mapper.TrainerProfileMapper; import com.demo.pteam.trainer.profile.domain.TrainerProfile; 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) { + + AccountEntity trainer = em.getReference(AccountEntity.class, profile.getUserId()); + TrainerAddressEntity address = em.getReference(TrainerAddressEntity.class, profile.getAddressId()); + + TrainerProfileEntity entity = TrainerProfileMapper.toEntity(profile, trainer, address); - @Override - public void save(TrainerProfile profile) { + trainerProfileJPARepository.save(entity); + } - AccountEntity trainer = em.getReference(AccountEntity.class, profile.getUserId()); - TrainerAddressEntity address = em.getReference(TrainerAddressEntity.class, profile.getAddressId()); + @Override + public Optional findByUserId(Long userId) { + return trainerProfileJPARepository.findByTrainerId(userId) + .map(entity -> TrainerProfileMapper.toDomain(entity)); + } - 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(); + @Override + public void delete(Long profileId) { + trainerProfileJPARepository.deleteById(profileId); + } - trainerProfileJPARepository.save(entity); - } } \ 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..580b1e42 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 @@ -8,6 +8,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; import java.time.LocalTime; @@ -15,6 +17,8 @@ @Table(name = "trainer_profile") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE trainer_profile SET deleted_at = NOW() WHERE id = ?") +@Where(clause = "deleted_at IS NULL") public class TrainerProfileEntity extends SoftDeletableEntity { @Id @@ -22,7 +26,7 @@ public class TrainerProfileEntity extends SoftDeletableEntity { private Long id; @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "user_id", nullable = false, unique = true) private AccountEntity trainer; @OneToOne(fetch = FetchType.LAZY) 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..bacbf188 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,6 +3,7 @@ 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; @@ -10,6 +11,7 @@ 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 lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,63 +19,78 @@ @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() ); - 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); } - 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 userId 로그인 사용자 id + */ + public void deleteProfile(Long userId) { + TrainerProfile profile = trainerProfileRepository.findByUserId(userId) + .orElseThrow(() -> new ApiException(TrainerProfileErrorCode.PROFILE_NOT_FOUND)); + + trainerProfileRepository.delete(profile.getId()); + } + }