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("전라북도", "상세주소"));
+ }
+}