diff --git a/build.gradle b/build.gradle index 67bb7c9..4a79f03 100644 --- a/build.gradle +++ b/build.gradle @@ -117,6 +117,7 @@ jacocoTestReport { includes: [ '**/service/**', '**/controller/**', + '**/common/entity/FullAddress*', '**/repository/**' ]) })) @@ -147,6 +148,7 @@ jacocoTestCoverageVerification { includes = [ '*service.*Service*', '*controller.*Controller*', + '*common.entity.FullAddress*', '*repository.*Repository*' ] } diff --git a/src/main/java/com/ajou/hertz/common/entity/Coordinate.java b/src/main/java/com/ajou/hertz/common/entity/Coordinate.java new file mode 100644 index 0000000..bcc5153 --- /dev/null +++ b/src/main/java/com/ajou/hertz/common/entity/Coordinate.java @@ -0,0 +1,22 @@ +package com.ajou.hertz.common.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Embeddable +public class Coordinate { + + @Column(nullable = false) + private String lat; + + @Column(nullable = false) + private String lng; + +} diff --git a/src/main/java/com/ajou/hertz/common/entity/FullAddress.java b/src/main/java/com/ajou/hertz/common/entity/FullAddress.java new file mode 100644 index 0000000..9421c3d --- /dev/null +++ b/src/main/java/com/ajou/hertz/common/entity/FullAddress.java @@ -0,0 +1,68 @@ +package com.ajou.hertz.common.entity; + +import java.util.Arrays; + +import com.ajou.hertz.common.exception.InvalidAddressFormatException; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Embeddable +public class FullAddress { + + @Column(nullable = false) + private String sido; + + @Column(nullable = false) + private String sgg; + + private String lotNumberAddress; + + private String roadAddress; + + @Column(nullable = false) + private String detailAddress; + + public static FullAddress of(String fullAddress, String detailAddress) { + + String[] parsedAddress = fullAddress.split(" "); + + if (parsedAddress.length < 2) { + throw new InvalidAddressFormatException(fullAddress); + + } + + String sido = parsedAddress[0]; + StringBuilder sggBuilder = new StringBuilder(); + String lotNumberAddress = null; + String roadAddress = null; + + int num; + for (num = 1; num < parsedAddress.length; num++) { + if (parsedAddress[num].matches(".*[동면읍소로길]$")) { + break; + } + sggBuilder.append(parsedAddress[num]).append(" "); + } + + String sgg = sggBuilder.toString().trim(); + + if (parsedAddress.length > num) { + if (fullAddress.matches(".+[로길].+")) { + roadAddress = String.join(" ", Arrays.copyOfRange(parsedAddress, num, parsedAddress.length)); + } else { + lotNumberAddress = String.join(" ", Arrays.copyOfRange(parsedAddress, num, parsedAddress.length)); + } + } + + return new FullAddress(sido, sgg, lotNumberAddress, roadAddress, detailAddress); + } + +} \ No newline at end of file diff --git a/src/main/java/com/ajou/hertz/common/exception/InvalidAddressFormatException.java b/src/main/java/com/ajou/hertz/common/exception/InvalidAddressFormatException.java new file mode 100644 index 0000000..66f8d95 --- /dev/null +++ b/src/main/java/com/ajou/hertz/common/exception/InvalidAddressFormatException.java @@ -0,0 +1,9 @@ +package com.ajou.hertz.common.exception; + +import com.ajou.hertz.common.exception.constant.CustomExceptionType; + +public class InvalidAddressFormatException extends BadRequestException { + public InvalidAddressFormatException(String address) { + super(CustomExceptionType.INVALID_ADDRESS_FORMAT, "address=" + address); + } +} diff --git a/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java b/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java index 657e68b..cc64662 100644 --- a/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java +++ b/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java @@ -11,6 +11,7 @@ *
  • 1001 ~ 1999: 일반 예외. 아래 항목에 해당하지 않는 대부분의 예외가 여기에 해당한다.
  • *
  • 2000 ~ 2199: 인증 관련 예외
  • *
  • 2200 ~ 2399: 유저 관련 예외
  • + *
  • 2400 ~ 2599: 주소 관련 예외
  • * */ @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -40,6 +41,11 @@ public enum CustomExceptionType { USER_NOT_FOUND_BY_PHONE(2206, "일치하는 회원을 찾을 수 없습니다."), KAKAO_CLIENT(10000, "카카오 서버와의 통신 중 오류가 발생했습니다."), + + /** + * 주소 관련 예외 + */ + INVALID_ADDRESS_FORMAT(2400, "주소 형식이 올바르지 않습니다."), ; private final Integer code; diff --git a/src/main/java/com/ajou/hertz/domain/concert_hall/constant/ConcertHallProgressStatus.java b/src/main/java/com/ajou/hertz/domain/concert_hall/constant/ConcertHallProgressStatus.java new file mode 100644 index 0000000..eac7d15 --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/concert_hall/constant/ConcertHallProgressStatus.java @@ -0,0 +1,15 @@ +package com.ajou.hertz.domain.concert_hall.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum ConcertHallProgressStatus { + + SELLING("판매중"), + RESERVED("예약중"), + SOLD_OUT("판매 완료"); + + private final String description; +} diff --git a/src/main/java/com/ajou/hertz/domain/concert_hall/dto/ConcertHallImageDto.java b/src/main/java/com/ajou/hertz/domain/concert_hall/dto/ConcertHallImageDto.java new file mode 100644 index 0000000..96deb8d --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/concert_hall/dto/ConcertHallImageDto.java @@ -0,0 +1,25 @@ +package com.ajou.hertz.domain.concert_hall.dto; + +import com.ajou.hertz.domain.concert_hall.entity.ConcertHallImage; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class ConcertHallImageDto { + private Long id; + private String name; + private String url; + + public static ConcertHallImageDto from(ConcertHallImage concertHallImage) { + return new ConcertHallImageDto( + concertHallImage.getId(), + concertHallImage.getOriginalName(), + concertHallImage.getUrl() + ); + } +} diff --git a/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHall.java b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHall.java new file mode 100644 index 0000000..990640d --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHall.java @@ -0,0 +1,87 @@ +package com.ajou.hertz.domain.concert_hall.entity; + +import com.ajou.hertz.common.entity.Coordinate; +import com.ajou.hertz.common.entity.FullAddress; +import com.ajou.hertz.common.entity.TimeTrackedBaseEntity; +import com.ajou.hertz.domain.concert_hall.constant.ConcertHallProgressStatus; +import com.ajou.hertz.domain.user.entity.User; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +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.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class ConcertHall extends TimeTrackedBaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "concert_hall_id", nullable = false) + private Long id; + + @JoinColumn(name = "seller_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private User seller; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ConcertHallProgressStatus progressStatus; + + @Column(nullable = false) + @Embedded + private FullAddress fullAddress; + + @Column(nullable = false) + private Boolean hasSoundEquipment; + + @Column(nullable = false) + private Boolean hasStaff; + + @Column(nullable = false) + private Boolean hasAdditionalSpace; + + @Column(nullable = false) + private Integer pricePerDay; + + @Column(nullable = false) + private Integer pricePerHour; + + @Column(nullable = false) + private Short capacity; + + @Column(nullable = false) + private String size; + + @Column(nullable = false) + private Boolean hasParkingLot; + + @Column(length = 1000, nullable = false) + private String description; + + @Embedded + private Coordinate coordinate; + + @Embedded + private ConcertHallImages images = new ConcertHallImages(); + + @Embedded + private ConcertHallHashtags hashtags = new ConcertHallHashtags(); + +} diff --git a/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallHashtag.java b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallHashtag.java new file mode 100644 index 0000000..bd609dc --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallHashtag.java @@ -0,0 +1,39 @@ +package com.ajou.hertz.domain.concert_hall.entity; + +import com.ajou.hertz.common.entity.BaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +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.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class ConcertHallHashtag extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "concert_hall_hashtag_id", nullable = false) + private Long id; + + @JoinColumn(name = "concert_hall_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private ConcertHall concertHall; + + @Column(length = 10, nullable = false) + private String content; + + public static ConcertHallHashtag create(ConcertHall concertHall, String content) { + return new ConcertHallHashtag(null, concertHall, content); + } +} diff --git a/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallHashtags.java b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallHashtags.java new file mode 100644 index 0000000..2bb34b3 --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallHashtags.java @@ -0,0 +1,20 @@ +package com.ajou.hertz.domain.concert_hall.entity; + +import java.util.LinkedList; +import java.util.List; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Embeddable +public class ConcertHallHashtags { + + @OneToMany(mappedBy = "concertHall") + private List content = new LinkedList<>(); + +} diff --git a/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallImage.java b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallImage.java new file mode 100644 index 0000000..cfabc59 --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallImage.java @@ -0,0 +1,46 @@ +package com.ajou.hertz.domain.concert_hall.entity; + +import com.ajou.hertz.common.entity.FileEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +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.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class ConcertHallImage extends FileEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "concert_hall_image_id", nullable = false) + private Long id; + + @JoinColumn(name = "concert_hall", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private ConcertHall concertHall; + + private ConcertHallImage(Long id, ConcertHall concertHall, String originalName, String storedName, String url) { + super(originalName, storedName, url); + this.id = id; + this.concertHall = concertHall; + } + + public static ConcertHallImage create(ConcertHall concertHall, String originalName, String storedName, String url) { + return new ConcertHallImage( + null, + concertHall, + originalName, + storedName, + url + ); + } +} diff --git a/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallImages.java b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallImages.java new file mode 100644 index 0000000..c8b2ba5 --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/concert_hall/entity/ConcertHallImages.java @@ -0,0 +1,19 @@ +package com.ajou.hertz.domain.concert_hall.entity; + +import java.util.LinkedList; +import java.util.List; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Embeddable +public class ConcertHallImages { + @OneToMany(mappedBy = "concertHall") + private List content = new LinkedList<>(); + +} diff --git a/src/test/java/com/ajou/hertz/unit/common/entity/full_address/FullAddressParsingTest.java b/src/test/java/com/ajou/hertz/unit/common/entity/full_address/FullAddressParsingTest.java new file mode 100644 index 0000000..228b9e9 --- /dev/null +++ b/src/test/java/com/ajou/hertz/unit/common/entity/full_address/FullAddressParsingTest.java @@ -0,0 +1,66 @@ +package com.ajou.hertz.unit.common.entity.full_address; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Constructor; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.ajou.hertz.common.entity.FullAddress; +import com.ajou.hertz.common.exception.InvalidAddressFormatException; + +@DisplayName("[Unit] Entity - FullAddress") +public class FullAddressParsingTest { + + @ParameterizedTest + @MethodSource("fullAddressesParsingTest") + void 전체_주소를_파싱하고_결과와_일치하는지_확인한다( + String fullAddress, + String detailAddress, + String expectedSido, + String expectedSgg, + String expectedLotNumberAddress, + String expectedRoadAddress, + String expectedDetailAddress + ) { + FullAddress parsedAddress = FullAddress.of(fullAddress, detailAddress); + assertThat(parsedAddress.getSido()).isEqualTo(expectedSido); + assertThat(parsedAddress.getSgg()).isEqualTo(expectedSgg); + assertThat(parsedAddress.getLotNumberAddress()).isEqualTo(expectedLotNumberAddress); + assertThat(parsedAddress.getRoadAddress()).isEqualTo(expectedRoadAddress); + assertThat(parsedAddress.getDetailAddress()).isEqualTo(expectedDetailAddress); + } + + private static Stream fullAddressesParsingTest() { + return Stream.of( + Arguments.of("경기 성남시 분당구 판교역로 152", "12층", "경기", "성남시 분당구", null, "판교역로 152", "12층"), + Arguments.of("경북 안동시 일직면 감시골길 63", "빌라 202호", "경북", "안동시", null, "일직면 감시골길 63", "빌라 202호"), + Arguments.of("경기 여주시 가남읍 가남로 15", "아파트 101호", "경기", "여주시", null, "가남읍 가남로 15", "아파트 101호"), + Arguments.of("제주특별자치도 제주시 가령골길 1", "빌라 202호", "제주특별자치도", "제주시", null, "가령골길 1", "빌라 202호"), + Arguments.of("인천 서구 가남로 51", "아파트 101호", "인천", "서구", null, "가남로 51", "아파트 101호"), + Arguments.of("강원특별자치도 원주시 지정면 신평리 469", "빌라 202호", "강원특별자치도", "원주시", "지정면 신평리 469", null, "빌라 202호"), + Arguments.of("전남 고흥군 고흥읍 등암리 1679", "아파트 101호", "전남", "고흥군", "고흥읍 등암리 1679", null, "아파트 101호"), + Arguments.of("충북 청주시 서원구 모충동 372", "빌라 202호", "충북", "청주시 서원구", "모충동 372", null, "빌라 202호"), + Arguments.of("인천 서구", "빌라 202호", "인천", "서구", null, null, "빌라 202호") + ); + } + + @Test + void 기본_생성자의_작동을_확인한다() throws Exception { + Constructor constructor = FullAddress.class.getDeclaredConstructor(); + constructor.setAccessible(true); + FullAddress fullAddress = constructor.newInstance(); + assertNotNull(fullAddress); + } + + @Test + void 잘못된_형식의_주소이면_예외가_발생하는지_확인한다() { + assertThrows(InvalidAddressFormatException.class, () -> FullAddress.of("전라북도", "상세주소")); + } +}