diff --git a/.gitignore b/.gitignore index 2460512d..c53d3163 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ out/ ### Mac OS ### .DS_Store -application-secret.yml \ No newline at end of file +## applicaion-secret +application-secret.yml + diff --git a/build.gradle b/build.gradle index 5944692a..abb27a08 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' + // mysql + implementation 'mysql:mysql-connector-java:8.0.33' + // Spring Security implementation 'org.springframework.security:spring-security-crypto:5.7.1' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' diff --git a/src/main/java/poomasi/domain/farm/controller/FarmController.java b/src/main/java/poomasi/domain/farm/controller/FarmController.java new file mode 100644 index 00000000..9f073ce0 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/controller/FarmController.java @@ -0,0 +1,25 @@ +package poomasi.domain.farm.controller; + +import lombok.AllArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.farm.service.FarmService; + + +@RestController +@AllArgsConstructor +@RequestMapping("/api/farm") +public class FarmController { + private final FarmService farmService; + + @GetMapping("/{farmId}") + public ResponseEntity getFarm(@RequestParam Long farmId) { + return ResponseEntity.ok(farmService.getFarmByFarmId(farmId)); + } + + @GetMapping("") + public ResponseEntity getFarmList(Pageable pageable) { + return ResponseEntity.ok(farmService.getFarmList(pageable)); + } +} diff --git a/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java new file mode 100644 index 00000000..71a3aa33 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/controller/FarmFarmerController.java @@ -0,0 +1,32 @@ +package poomasi.domain.farm.controller; + +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +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 poomasi.domain.farm.dto.FarmRegisterRequest; +import poomasi.domain.farm.dto.FarmUpdateRequest; +import poomasi.domain.farm.service.FarmFarmerService; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/farm") +public class FarmFarmerController { + private final FarmFarmerService farmFarmerService; + + // TODO: 판매자만 접근가능하도록 인증/인가 annotation 추가 + @PostMapping("") + public ResponseEntity registerFarm(@RequestBody FarmRegisterRequest request) { + return ResponseEntity.ok(farmFarmerService.registerFarm(request)); + } + + @PostMapping("/update") + public ResponseEntity updateFarm(@Valid @RequestBody FarmUpdateRequest request) { + // TODO: 판매자 ID(Spring Security Context)로 대체 + Long farmerId = 1L; + return ResponseEntity.ok(farmFarmerService.updateFarm(farmerId, request)); + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java new file mode 100644 index 00000000..2f60d40a --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/FarmRegisterRequest.java @@ -0,0 +1,26 @@ +package poomasi.domain.farm.dto; + +import poomasi.domain.farm.entity.Farm; + +public record FarmRegisterRequest( + String name, + Long ownerId, + String address, + String addressDetail, + Double latitude, + Double longitude, + String phoneNumber, + String description +) { + public Farm toEntity() { + return Farm.builder() + .name(name) + .ownerId(ownerId) + .address(address) + .addressDetail(addressDetail) + .latitude(latitude) + .longitude(longitude) + .description(description) + .build(); + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/FarmResponse.java b/src/main/java/poomasi/domain/farm/dto/FarmResponse.java new file mode 100644 index 00000000..74021c34 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/FarmResponse.java @@ -0,0 +1,26 @@ +package poomasi.domain.farm.dto; + +import poomasi.domain.farm.entity.Farm; + + +public record FarmResponse( // FIXME: 사용자 정보 추가 및 설명/전화번호 추가 + Long id, + String name, + String address, + String addressDetail, + Double latitude, + Double longitude, + String description + ) { + public static FarmResponse fromEntity(Farm farm) { + return new FarmResponse( + farm.getId(), + farm.getName(), + farm.getAddress(), + farm.getAddressDetail(), + farm.getLatitude(), + farm.getLongitude(), + farm.getDescription() + ); + } +} diff --git a/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java b/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java new file mode 100644 index 00000000..23e5ebbe --- /dev/null +++ b/src/main/java/poomasi/domain/farm/dto/FarmUpdateRequest.java @@ -0,0 +1,18 @@ +package poomasi.domain.farm.dto; + +import jakarta.validation.constraints.NotNull; +import poomasi.domain.farm.entity.Farm; + +public record FarmUpdateRequest( + @NotNull(message = "Farm ID는 필수 값입니다.") Long farmId, + String name, + String description, + String address, + String addressDetail, + Double latitude, + Double longitude +) { + public Farm toEntity(Farm farm) { + return farm.updateFarm(this); + } +} diff --git a/src/main/java/poomasi/domain/farm/entity/Farm.java b/src/main/java/poomasi/domain/farm/entity/Farm.java new file mode 100644 index 00000000..e89db2bf --- /dev/null +++ b/src/main/java/poomasi/domain/farm/entity/Farm.java @@ -0,0 +1,73 @@ +package poomasi.domain.farm.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLSelect; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.farm.dto.FarmUpdateRequest; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Table(name = "farm") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE farm SET deleted = true WHERE id = ?") +@SQLSelect(sql = "SELECT * FROM farm WHERE deleted = false") +public class Farm { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + // FIXME: owner_id는 Member의 id를 참조해야 합니다. + @Column(name = "owner_id") + private Long ownerId; + + private String description; + + private String address; // 도로명 주소 + private String addressDetail; // 상세 주소 + + private Double latitude; + private Double longitude; + + private FarmStatus status = FarmStatus.WAITING; + + private boolean deleted = false; + + @CreationTimestamp + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + @UpdateTimestamp + private LocalDateTime updatedAt = LocalDateTime.now(); + + @Builder + public Farm(String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description) { + this.name = name; + this.ownerId = ownerId; + this.address = address; + this.addressDetail = addressDetail; + this.latitude = latitude; + this.longitude = longitude; + this.description = description; + } + + public Farm updateFarm(FarmUpdateRequest farmUpdateRequest) { + this.name = farmUpdateRequest.name(); + this.address = farmUpdateRequest.address(); + this.addressDetail = farmUpdateRequest.addressDetail(); + this.latitude = farmUpdateRequest.latitude(); + this.longitude = farmUpdateRequest.longitude(); + this.description = farmUpdateRequest.description(); + return this; + } +} diff --git a/src/main/java/poomasi/domain/farm/entity/FarmStatus.java b/src/main/java/poomasi/domain/farm/entity/FarmStatus.java new file mode 100644 index 00000000..f1723a19 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/entity/FarmStatus.java @@ -0,0 +1,5 @@ +package poomasi.domain.farm.entity; + +public enum FarmStatus { + OPEN, CLOSE, WAITING +} diff --git a/src/main/java/poomasi/domain/farm/repository/FarmRepository.java b/src/main/java/poomasi/domain/farm/repository/FarmRepository.java new file mode 100644 index 00000000..7c4c6e9b --- /dev/null +++ b/src/main/java/poomasi/domain/farm/repository/FarmRepository.java @@ -0,0 +1,12 @@ +package poomasi.domain.farm.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.farm.entity.Farm; + +@Repository +public interface FarmRepository extends JpaRepository { + Page findAll(Pageable pageable); +} diff --git a/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java new file mode 100644 index 00000000..d5f144a5 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/service/FarmFarmerService.java @@ -0,0 +1,43 @@ +package poomasi.domain.farm.service; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import poomasi.domain.farm.dto.FarmRegisterRequest; +import poomasi.domain.farm.dto.FarmUpdateRequest; +import poomasi.domain.farm.entity.Farm; +import poomasi.domain.farm.repository.FarmRepository; +import poomasi.global.error.BusinessException; + +import static poomasi.global.error.BusinessError.FARM_NOT_FOUND; +import static poomasi.global.error.BusinessError.FARM_OWNER_MISMATCH; + +@Service +@AllArgsConstructor +public class FarmFarmerService { + private final FarmRepository farmRepository; + + public Long registerFarm(FarmRegisterRequest request) { + // TODO: 판매자 인가? + + // TODO: 이미 등록된 농장 인가? + + return farmRepository.save(request.toEntity()).getId(); + + } + + public Long updateFarm(Long farmerId, FarmUpdateRequest request) { + Farm farm = this.getFarmByFarmId(request.farmId()); + if (!farm.getOwnerId().equals(farmerId)) { + throw new BusinessException(FARM_OWNER_MISMATCH); + } + + // TODO: 변경 가능한 상태인가? + + return farmRepository.save(request.toEntity(farm)).getId(); + } + + public Farm getFarmByFarmId(Long farmId) { + return farmRepository.findById(farmId).orElseThrow(() -> new BusinessException(FARM_NOT_FOUND)); + } + +} diff --git a/src/main/java/poomasi/domain/farm/service/FarmService.java b/src/main/java/poomasi/domain/farm/service/FarmService.java new file mode 100644 index 00000000..753a2c87 --- /dev/null +++ b/src/main/java/poomasi/domain/farm/service/FarmService.java @@ -0,0 +1,31 @@ +package poomasi.domain.farm.service; + +import lombok.AllArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import poomasi.domain.farm.dto.FarmResponse; +import poomasi.domain.farm.repository.FarmRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class FarmService { + private final FarmRepository farmRepository; + + public FarmResponse getFarmByFarmId(Long farmId) { + return farmRepository.findById(farmId) + .map(FarmResponse::fromEntity) + .orElseThrow(() -> new BusinessException(BusinessError.FARM_NOT_FOUND)); + } + + + public List getFarmList(Pageable pageable) { + return farmRepository.findAll(pageable).stream() + .map(FarmResponse::fromEntity) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index 6e704243..435056f5 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -8,15 +8,20 @@ @Getter @AllArgsConstructor public enum BusinessError { - //member - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."), - DUPLICATE_MEMBER_EMAIL(HttpStatus.CONFLICT, "중복된 이메일입니다."), - INVALID_FARMER_QUALIFICATION(HttpStatus.BAD_REQUEST, "농부 자격 증명이 필요합니다."), - //auth - INVALID_CREDENTIAL(HttpStatus.UNAUTHORIZED, "잘못된 비밀번호 입니다."), - REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 유효하지 않습니다."); + //member + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."), + DUPLICATE_MEMBER_EMAIL(HttpStatus.CONFLICT, "중복된 이메일입니다."), + INVALID_FARMER_QUALIFICATION(HttpStatus.BAD_REQUEST, "농부 자격 증명이 필요합니다."), + //auth + INVALID_CREDENTIAL(HttpStatus.UNAUTHORIZED, "잘못된 비밀번호 입니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 유효하지 않습니다."), + // Farm + FARM_NOT_FOUND(HttpStatus.NOT_FOUND, "농장을 찾을 수 없습니다."), + FARM_OWNER_MISMATCH(HttpStatus.FORBIDDEN, "해당 농장의 소유자가 아닙니다."); + - private final HttpStatus httpStatus; - private final String message; + private final HttpStatus httpStatus; + + private final String message; }