Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment on lines +66 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spring-boot-starter-web를 추가하면 jsr310이 자동으로 추가되기 때문에 의존성을 추가하지 않아도 괜찮습니다!


// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand All @@ -76,5 +79,4 @@ dependencies {

tasks.named('test') {
useJUnitPlatform()
jvmArgs "-javaagent:${configurations.testRuntimeClasspath.find { it.name.contains('byte-buddy-agent') }.absolutePath}"
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.demo.pteam.global.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper;
}
}
8 changes: 4 additions & 4 deletions src/main/java/com/demo/pteam/global/config/JpaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

@Configuration
public class JpaConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
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;

@Getter
Expand All @@ -12,17 +9,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;
}

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
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;

@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) {
Expand All @@ -24,13 +21,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public enum TrainerAddressErrorCode implements ErrorCode {
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", "도로명 주소를 찾을 수 없습니다.");
ROAD_ADDRESS_NOT_FOUND(HttpStatus.BAD_REQUEST, "L_006", "도로명 주소를 찾을 수 없습니다."),
ADDRESS_NOT_FOUND(HttpStatus.NOT_FOUND, "L_007", "등록되어 있는 트레이너 주소가 없습니다.");;

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,37 @@

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 {

// 요청 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
);
Comment on lines +14 to 21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null 주입을 TrainerAddress 생성자 내부에서 처리해도 좋을 것 같아요

}

// 도메인 -> 엔티티 변환
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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

public interface TrainerAddressRepository {
TrainerAddress save(TrainerAddress address);
Optional<TrainerAddress> findById(Long addressId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand All @@ -40,4 +35,17 @@ public TrainerAddress save(TrainerAddress address) {
);
}

@Override
public Optional<TrainerAddress> findById(Long addressId) {
return jpaRepository.findById(addressId)
.map(entity -> new TrainerAddress(
entity.getId(),
entity.getNumberAddress(),
entity.getRoadAddress(),
entity.getDetailAddress(),
entity.getPostalCode(),
new Coordinates(entity.getLatitude(), entity.getLongitude())
));
}
Comment on lines +38 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 매핑을 레포지토리보단 서비스에서 하는게 좋을 것 같다고 생각해요
레포지토리 내부에서 매핑을 사용한다면 재사용성이 떨어집니다.
그리고 jpa가 아닌 다른 서비스로 변경할 경우 레포지토리를 새로 추가해야 하는데 매핑 로직까지 새로 작성해야하는 문제도 발생합니다.
마지막으로 레포지토리의 책임이 많다는 생각이 듭니다. 레포지토리는 데이터 조회랑 저장까지만 담당하고 그걸 변형하거나 검증하는 등의 비즈니스 로직은 서비스에서 담당하는게 좋을 것 같아요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 매핑을 레포지토리보단 서비스에서 하는게 좋을 것 같다고 생각해요 레포지토리 내부에서 매핑을 사용한다면 재사용성이 떨어집니다. 그리고 jpa가 아닌 다른 서비스로 변경할 경우 레포지토리를 새로 추가해야 하는데 매핑 로직까지 새로 작성해야하는 문제도 발생합니다. 마지막으로 레포지토리의 책임이 많다는 생각이 듭니다. 레포지토리는 데이터 조회랑 저장까지만 담당하고 그걸 변형하거나 검증하는 등의 비즈니스 로직은 서비스에서 담당하는게 좋을 것 같아요

생각해보니 그렇네요! 수정해보도록 하겠습니다!!


}
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,45 @@

import com.demo.pteam.global.response.ApiResponse;
import com.demo.pteam.trainer.profile.controller.dto.TrainerProfileRequest;
import com.demo.pteam.trainer.profile.controller.dto.TrainerProfileResponse;
import com.demo.pteam.trainer.profile.service.TrainerProfileService;
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<ApiResponse<Void>> createProfile(
@RequestBody @Valid TrainerProfileRequest request
) {
Long userId = 4L; // TODO: 로그인 사용자 임시
/**
* 트레이너 프로필 등록 API
* @param request 트레이너 프로필 요청 DTO
* @return 등록 성공 여부
*/
@PostMapping
public ResponseEntity<ApiResponse<Void>> createProfile(
@RequestBody @Valid TrainerProfileRequest request
) {
Long userId = 4L; // TODO: 로그인 사용자 임시

trainerProfileService.createProfile(request, userId);
return ResponseEntity.status(201).body(ApiResponse.created("트레이너 프로필이 성공적으로 등록되었습니다."));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response에 Location 헤더의 추가가 필요할 것 같습니다!

}

/**
* 트레이너 프로필 조회 API (사용자 본인)
* @return 트레이너 프로필 응답 DTO
*/
@GetMapping
public ResponseEntity<ApiResponse<TrainerProfileResponse>> getProfile() {
Long userId = 4L; // TODO: 로그인 사용자 임시

TrainerProfileResponse response = trainerProfileService.getProfile(userId);
return ResponseEntity.ok(ApiResponse.success(response));
}

trainerProfileService.createProfile(request, userId);
return ResponseEntity.status(201).body(ApiResponse.created("트레이너 프로필이 성공적으로 등록되었습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 = "이름 공개 선택 여부는 필수입니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
package com.demo.pteam.trainer.profile.controller.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TrainerProfileResponse {
private Long profileId;
private String displayName;
private String intro;
private Integer credit;

@JsonFormat(pattern = "HH:mm")
private LocalTime contactStartTime;

@JsonFormat(pattern = "HH:mm")
private LocalTime contactEndTime;
private String profileImg;

@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;

@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime updatedAt;

private Address address;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Address {
private String roadAddress;
private String detailAddress;
private String postalCode;
private BigDecimal latitude;
private BigDecimal longitude;
}

}

Loading