From fb16ee56e3b534127f2f557e141fd253b197e7cc Mon Sep 17 00:00:00 2001
From: tinon1004 <tinon1004@ajou.ac.kr>
Date: Fri, 26 Apr 2024 19:19:17 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20#107=20=ED=95=A9=EC=A3=BC=EC=8B=A4=20?=
 =?UTF-8?q?=EB=A7=A4=EB=AC=BC=20=EC=83=9D=EC=84=B1=20api=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ajou/hertz/common/dto/CoordinateDto.java  |  20 ++
 .../ajou/hertz/common/dto/FullAddressDto.java |  29 +++
 .../common/dto/request/CoordinateRequest.java |  26 +++
 .../dto/request/FullAddressRequest.java       |  25 +++
 .../dto/response/CoordinateResponse.java      |  24 +++
 .../dto/response/FullAddressResponse.java     |  40 ++++
 .../ajou/hertz/common/entity/Coordinate.java  |  11 +-
 .../ajou/hertz/common/entity/FullAddress.java |  74 ++++---
 .../constant/CustomExceptionType.java         |  78 ++++----
 .../controller/PracticeRoomController.java    |  67 +++++++
 .../practice_room/dto/PracticeRoomDto.java    |  61 ++++++
 .../request/CreateNewPracticeRoomRequest.java | 102 ++++++++++
 .../response/PracticeRoomImageResponse.java   |  35 ++++
 .../dto/response/PracticeRoomResponse.java    |  91 +++++++++
 .../practice_room/entity/PracticeRoom.java    | 181 ++++++++++++------
 .../entity/PracticeRoomDescription.java       |  49 +++++
 .../entity/PracticeRoomHashtags.java          |  20 +-
 .../entity/PracticeRoomImages.java            |  21 +-
 ...inkInPracticeRoomDescriptionException.java |  10 +
 .../service/PracticeRoomCommandService.java   |  54 ++++++
 .../PracticeRoomHashtagCommandService.java    |  32 ++++
 .../PracticeRoomImageCommandService.java      |  43 +++++
 22 files changed, 940 insertions(+), 153 deletions(-)
 create mode 100644 src/main/java/com/ajou/hertz/common/dto/CoordinateDto.java
 create mode 100644 src/main/java/com/ajou/hertz/common/dto/FullAddressDto.java
 create mode 100644 src/main/java/com/ajou/hertz/common/dto/request/CoordinateRequest.java
 create mode 100644 src/main/java/com/ajou/hertz/common/dto/request/FullAddressRequest.java
 create mode 100644 src/main/java/com/ajou/hertz/common/dto/response/CoordinateResponse.java
 create mode 100644 src/main/java/com/ajou/hertz/common/dto/response/FullAddressResponse.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/controller/PracticeRoomController.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/dto/PracticeRoomDto.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/dto/request/CreateNewPracticeRoomRequest.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomImageResponse.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomResponse.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomDescription.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/exception/UnexpectedLinkInPracticeRoomDescriptionException.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomCommandService.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomHashtagCommandService.java
 create mode 100644 src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomImageCommandService.java

diff --git a/src/main/java/com/ajou/hertz/common/dto/CoordinateDto.java b/src/main/java/com/ajou/hertz/common/dto/CoordinateDto.java
new file mode 100644
index 0000000..6e8f7f6
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/dto/CoordinateDto.java
@@ -0,0 +1,20 @@
+package com.ajou.hertz.common.dto;
+
+import com.ajou.hertz.common.entity.Coordinate;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class CoordinateDto {
+
+    private String lat;
+    private String lng;
+
+    public static CoordinateDto from(Coordinate coordinate) {
+        return new CoordinateDto(coordinate.getLat(), coordinate.getLng());
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/common/dto/FullAddressDto.java b/src/main/java/com/ajou/hertz/common/dto/FullAddressDto.java
new file mode 100644
index 0000000..ca0807f
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/dto/FullAddressDto.java
@@ -0,0 +1,29 @@
+package com.ajou.hertz.common.dto;
+
+import com.ajou.hertz.common.entity.FullAddress;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class FullAddressDto {
+
+    private String sido;
+    private String sgg;
+    private String lotNumberAddress;
+    private String roadAddress;
+    private String detailAddress;
+
+    public static FullAddressDto from(FullAddress fullAddress) {
+        return new FullAddressDto(
+                fullAddress.getSido(),
+                fullAddress.getSgg(),
+                fullAddress.getLotNumberAddress(),
+                fullAddress.getRoadAddress(),
+                fullAddress.getDetailAddress()
+        );
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/common/dto/request/CoordinateRequest.java b/src/main/java/com/ajou/hertz/common/dto/request/CoordinateRequest.java
new file mode 100644
index 0000000..6b9d38e
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/dto/request/CoordinateRequest.java
@@ -0,0 +1,26 @@
+package com.ajou.hertz.common.dto.request;
+
+import com.ajou.hertz.common.entity.Coordinate;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.*;
+
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Setter
+@Getter
+public class CoordinateRequest {
+
+    @Schema(description = "위도", example = "37.123456")
+    @NotBlank
+    private String lat;
+
+    @Schema(description = "경도", example = "127.123456")
+    @NotBlank
+    private String lng;
+
+
+    public Coordinate toEntity() {
+        return new Coordinate(lat, lng);
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/common/dto/request/FullAddressRequest.java b/src/main/java/com/ajou/hertz/common/dto/request/FullAddressRequest.java
new file mode 100644
index 0000000..9c7e276
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/dto/request/FullAddressRequest.java
@@ -0,0 +1,25 @@
+package com.ajou.hertz.common.dto.request;
+
+import com.ajou.hertz.common.entity.FullAddress;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.*;
+
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Setter
+@Getter
+public class FullAddressRequest {
+
+    @Schema(description = "전체 주소", example = "서울특별시 강남구 역삼동 123-45")
+    @NotBlank
+    private String fullAddress;
+
+    @Schema(description = "상세주소", example = "아주 빌딩 2층")
+    @NotBlank
+    private String detailAddress;
+
+    public FullAddress toEntity() {
+        return FullAddress.of(fullAddress, detailAddress);
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/common/dto/response/CoordinateResponse.java b/src/main/java/com/ajou/hertz/common/dto/response/CoordinateResponse.java
new file mode 100644
index 0000000..78b0a5d
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/dto/response/CoordinateResponse.java
@@ -0,0 +1,24 @@
+package com.ajou.hertz.common.dto.response;
+
+import com.ajou.hertz.common.dto.CoordinateDto;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class CoordinateResponse {
+
+    @Schema(description = "위도", example = "37.123456")
+    private String lat;
+
+    @Schema(description = "경도", example = "127.123456")
+    private String lng;
+
+    public static CoordinateResponse from(CoordinateDto coordinateDto) {
+        return new CoordinateResponse(coordinateDto.getLat(), coordinateDto.getLng());
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/common/dto/response/FullAddressResponse.java b/src/main/java/com/ajou/hertz/common/dto/response/FullAddressResponse.java
new file mode 100644
index 0000000..6f1dc0e
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/common/dto/response/FullAddressResponse.java
@@ -0,0 +1,40 @@
+package com.ajou.hertz.common.dto.response;
+
+import com.ajou.hertz.common.dto.FullAddressDto;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class FullAddressResponse {
+
+
+    @Schema(description = "시/도", example = "서울특별시")
+    private String sido;
+
+    @Schema(description = "시/군/구", example = "강남구")
+    private String sgg;
+
+    @Schema(description = "지번주소", example = "수원시 팔달구 권선로11")
+    private String lotNumberAddress;
+
+    @Schema(description = "도로명 주소", example = "서울특별시 강남구 역삼동 123-45번지")
+    private String roadAddress;
+
+    @Schema(description = "상세주소", example = "아주 빌딩 2층")
+    private String detailAddress;
+
+    public static FullAddressResponse from(FullAddressDto fullAddressDto) {
+        return new FullAddressResponse(
+                fullAddressDto.getSido(),
+                fullAddressDto.getSgg(),
+                fullAddressDto.getLotNumberAddress(),
+                fullAddressDto.getRoadAddress(),
+                fullAddressDto.getDetailAddress()
+        );
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/common/entity/Coordinate.java b/src/main/java/com/ajou/hertz/common/entity/Coordinate.java
index bcc5153..e0c683c 100644
--- a/src/main/java/com/ajou/hertz/common/entity/Coordinate.java
+++ b/src/main/java/com/ajou/hertz/common/entity/Coordinate.java
@@ -8,15 +8,14 @@
 import lombok.NoArgsConstructor;
 
 @AllArgsConstructor
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
 @Getter
 @Embeddable
 public class Coordinate {
 
-	@Column(nullable = false)
-	private String lat;
-
-	@Column(nullable = false)
-	private String lng;
+    @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
index 9421c3d..91a50a9 100644
--- a/src/main/java/com/ajou/hertz/common/entity/FullAddress.java
+++ b/src/main/java/com/ajou/hertz/common/entity/FullAddress.java
@@ -1,9 +1,6 @@
 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;
@@ -11,58 +8,59 @@
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
+import java.util.Arrays;
+
 @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;
+    @Column(nullable = false)
+    private String sido;
 
-	private String lotNumberAddress;
+    @Column(nullable = false)
+    private String sgg;
 
-	private String roadAddress;
+    private String lotNumberAddress;
 
-	@Column(nullable = false)
-	private String detailAddress;
+    private String roadAddress;
 
-	public static FullAddress of(String fullAddress, String detailAddress) {
+    @Column(nullable = false)
+    private String detailAddress;
 
-		String[] parsedAddress = fullAddress.split(" ");
+    public static FullAddress of(String fullAddress, String detailAddress) {
 
-		if (parsedAddress.length < 2) {
-			throw new InvalidAddressFormatException(fullAddress);
+        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 sido = parsedAddress[0];
+        StringBuilder sggBuilder = new StringBuilder();
+        String lotNumberAddress = null;
+        String roadAddress = null;
 
-		String sgg = sggBuilder.toString().trim();
+        int num;
+        for (num = 1; num < parsedAddress.length; num++) {
+            if (parsedAddress[num].matches(".*[동면읍소로길]$")) {
+                break;
+            }
+            sggBuilder.append(parsedAddress[num]).append(" ");
+        }
 
-		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));
-			}
-		}
+        String sgg = sggBuilder.toString().trim();
 
-		return new FullAddress(sido, sgg, lotNumberAddress, roadAddress, detailAddress);
-	}
+        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/constant/CustomExceptionType.java b/src/main/java/com/ajou/hertz/common/exception/constant/CustomExceptionType.java
index 70664b3..7fe906b 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
@@ -13,52 +13,58 @@
  *     <li>2200 ~ 2399: 유저 관련 예외</li>
  *     <li>2400 ~ 2599: 주소 관련 예외</li>
  *     <li>2600 ~ 2799: 악기 관련 예외</li>
+ *     <li>2800 ~ 2999: 악기 관련 예외</li>
  * </ul>
  */
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
 @Getter
 public enum CustomExceptionType {
 
-	MULTIPART_FILE_NOT_READABLE(1000, "파일을 읽을 수 없습니다. 올바른 파일인지 다시 확인 후 요청해주세요."),
-	IO_PROCESSING(1001, "입출력 처리 중 오류가 발생했습니다."),
-	SEND_MESSAGE(1002, "문자 발송 과정에서 알 수 없는 에러가 발생했습니다."),
+    MULTIPART_FILE_NOT_READABLE(1000, "파일을 읽을 수 없습니다. 올바른 파일인지 다시 확인 후 요청해주세요."),
+    IO_PROCESSING(1001, "입출력 처리 중 오류가 발생했습니다."),
+    SEND_MESSAGE(1002, "문자 발송 과정에서 알 수 없는 에러가 발생했습니다."),
 
-	/**
-	 * 로그인, 인증 관련 예외
-	 */
-	ACCESS_DENIED(2000, "접근이 거부되었습니다."),
-	UNAUTHORIZED(2001, "유효하지 않은 인증 정보로 인해 인증 과정에서 문제가 발생하였습니다."),
-	TOKEN_VALIDATE(2002, "유효하지 않은 token입니다. Token 값이 잘못되었거나 만료되어 유효하지 않은 경우로 token 갱신이 필요합니다."),
-	PASSWORD_MISMATCH(2003, "비밀번호가 일치하지 않습니다."),
-	USER_AUTH_CODE_NOT_FOUND(2004, "본인 인증 코드 발행 이력을 찾을 수 없습니다. 인증 코드가 만료되었거나 잘못된 코드가 전송되었을 수 있습니다. 다시 시도해주세요."),
-	INVALID_AUTH_CODE(2005, "유효하지 않은 인증 코드 또는 전화번호입니다. 인증 코드가 만료되었거나 잘못된 정보가 전송되었을 수 있습니다. 다시 시도해주세요."),
+    /**
+     * 로그인, 인증 관련 예외
+     */
+    ACCESS_DENIED(2000, "접근이 거부되었습니다."),
+    UNAUTHORIZED(2001, "유효하지 않은 인증 정보로 인해 인증 과정에서 문제가 발생하였습니다."),
+    TOKEN_VALIDATE(2002, "유효하지 않은 token입니다. Token 값이 잘못되었거나 만료되어 유효하지 않은 경우로 token 갱신이 필요합니다."),
+    PASSWORD_MISMATCH(2003, "비밀번호가 일치하지 않습니다."),
+    USER_AUTH_CODE_NOT_FOUND(2004, "본인 인증 코드 발행 이력을 찾을 수 없습니다. 인증 코드가 만료되었거나 잘못된 코드가 전송되었을 수 있습니다. 다시 시도해주세요."),
+    INVALID_AUTH_CODE(2005, "유효하지 않은 인증 코드 또는 전화번호입니다. 인증 코드가 만료되었거나 잘못된 정보가 전송되었을 수 있습니다. 다시 시도해주세요."),
 
-	/**
-	 * 유저 관련 예외
-	 */
-	USER_EMAIL_DUPLICATION(2200, "이미 사용 중인 이메일입니다."),
-	USER_NOT_FOUND_BY_ID(2201, "일치하는 회원을 찾을 수 없습니다."),
-	USER_NOT_FOUND_BY_EMAIL(2202, "일치하는 회원을 찾을 수 없습니다."),
-	USER_PHONE_DUPLICATION(2203, "이미 사용 중인 전화번호입니다."),
-	USER_KAKAO_UID_DUPLICATION(2204, "이미 가입한 계정입니다."),
-	USER_NOT_FOUND_BY_KAKAO_UID(2205, "일치하는 회원을 찾을 수 없습니다."),
-	USER_NOT_FOUND_BY_PHONE(2206, "일치하는 회원을 찾을 수 없습니다."),
+    /**
+     * 유저 관련 예외
+     */
+    USER_EMAIL_DUPLICATION(2200, "이미 사용 중인 이메일입니다."),
+    USER_NOT_FOUND_BY_ID(2201, "일치하는 회원을 찾을 수 없습니다."),
+    USER_NOT_FOUND_BY_EMAIL(2202, "일치하는 회원을 찾을 수 없습니다."),
+    USER_PHONE_DUPLICATION(2203, "이미 사용 중인 전화번호입니다."),
+    USER_KAKAO_UID_DUPLICATION(2204, "이미 가입한 계정입니다."),
+    USER_NOT_FOUND_BY_KAKAO_UID(2205, "일치하는 회원을 찾을 수 없습니다."),
+    USER_NOT_FOUND_BY_PHONE(2206, "일치하는 회원을 찾을 수 없습니다."),
 
-	KAKAO_CLIENT(10000, "카카오 서버와의 통신 중 오류가 발생했습니다."),
+    KAKAO_CLIENT(10000, "카카오 서버와의 통신 중 오류가 발생했습니다."),
 
-	/**
-	 * 주소 관련 예외
-	 */
-	INVALID_ADDRESS_FORMAT(2400, "주소 형식이 올바르지 않습니다."),
+    /**
+     * 주소 관련 예외
+     */
+    INVALID_ADDRESS_FORMAT(2400, "주소 형식이 올바르지 않습니다."),
 
-	/**
-	 * 악기 관련 예외
-	 */
-	INSTRUMENT_NOT_FOUND_BY_ID(2600, "일치하는 매물 정보를 찾을 수 없습니다."),
-	INSTRUMENT_DELETE_PERMISSION_DENIED(2601, "악기 매물을 삭제할 권한이 없습니다. 매물은 판매자 본인만 삭제할 수 있습니다."),
-	INSTRUMENT_UPDATE_PERMISSION_DENIED(2602, "악기 매물 정보를 수정할 권한이 없습니다. 매물 정보는 판매자 본인만 수정할 수 있습니다."),
-	UNEXPECTED_LINK_IN_INSTRUMENT_DESCRIPTION(2603, "악기 설명에는 링크(URL)를 첨부할 수 없습니다. 링크 정보를 제거한 후 다시 시도하세요.");
+    /**
+     * 악기 관련 예외
+     */
+    INSTRUMENT_NOT_FOUND_BY_ID(2600, "일치하는 매물 정보를 찾을 수 없습니다."),
+    INSTRUMENT_DELETE_PERMISSION_DENIED(2601, "악기 매물을 삭제할 권한이 없습니다. 매물은 판매자 본인만 삭제할 수 있습니다."),
+    INSTRUMENT_UPDATE_PERMISSION_DENIED(2602, "악기 매물 정보를 수정할 권한이 없습니다. 매물 정보는 판매자 본인만 수정할 수 있습니다."),
+    UNEXPECTED_LINK_IN_INSTRUMENT_DESCRIPTION(2603, "악기 설명에는 링크(URL)를 첨부할 수 없습니다. 링크 정보를 제거한 후 다시 시도하세요."),
 
-	private final Integer code;
-	private final String message;
+    /**
+     * 합주실 관련 예외
+     */
+    UNEXPECTED_LINK_IN_PRACTICEROOM_DESCRIPTION(2800, "합주실 관련 설명에는 링크(URL)를 첨부할 수 없습니다. 링크 정보를 제거한 후 다시 시도하세요.");
+
+    private final Integer code;
+    private final String message;
 }
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/controller/PracticeRoomController.java b/src/main/java/com/ajou/hertz/domain/practice_room/controller/PracticeRoomController.java
new file mode 100644
index 0000000..fe28a91
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/controller/PracticeRoomController.java
@@ -0,0 +1,67 @@
+package com.ajou.hertz.domain.practice_room.controller;
+
+import com.ajou.hertz.common.auth.UserPrincipal;
+import com.ajou.hertz.domain.practice_room.dto.PracticeRoomDto;
+import com.ajou.hertz.domain.practice_room.dto.request.CreateNewPracticeRoomRequest;
+import com.ajou.hertz.domain.practice_room.dto.response.PracticeRoomResponse;
+import com.ajou.hertz.domain.practice_room.service.PracticeRoomCommandService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.net.URI;
+
+import static com.ajou.hertz.common.constant.GlobalConstants.API_VERSION_HEADER_NAME;
+
+@Tag(name = "합주실 관련 API")
+@RequiredArgsConstructor
+@RequestMapping("/api/practice-rooms")
+@RestController
+public class PracticeRoomController {
+
+    private final PracticeRoomCommandService practiceRoomCommandService;
+
+    @Operation(
+            summary = "합주실 매물 등록",
+            description = """
+                    <p>합주실 매물을 등록합니다.
+                    <p>요청 시 <strong>multipart/form-data</strong> content-type으로 요쳥해야 합니다.
+                    """,
+            security = @SecurityRequirement(name = "access-token")
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "201"),
+            @ApiResponse(responseCode = "400", description = "[2800] 악기 설명에 링크(URL)이 첨부되어 있는 경우.", content = @Content)
+    })
+    @PostMapping(
+            headers = API_VERSION_HEADER_NAME + "=" + 1,
+            consumes = MediaType.MULTIPART_FORM_DATA_VALUE
+    )
+    public ResponseEntity<PracticeRoomResponse> createNewPracticeRoomV1(
+            @AuthenticationPrincipal UserPrincipal userPrincipal,
+            @ParameterObject @ModelAttribute @Valid CreateNewPracticeRoomRequest createNewPracticeRoomRequest
+    ) {
+        PracticeRoomDto practiceRoomCreated = practiceRoomCommandService.createNewPracticeRoom(
+                userPrincipal.getUserId(),
+                createNewPracticeRoomRequest
+        );
+        return ResponseEntity
+                .created(URI.create("/api/practice-rooms/" + practiceRoomCreated.getId()))
+                .body(PracticeRoomResponse.from(practiceRoomCreated));
+    }
+
+
+}
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/dto/PracticeRoomDto.java b/src/main/java/com/ajou/hertz/domain/practice_room/dto/PracticeRoomDto.java
new file mode 100644
index 0000000..1ae44f0
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/dto/PracticeRoomDto.java
@@ -0,0 +1,61 @@
+package com.ajou.hertz.domain.practice_room.dto;
+
+import com.ajou.hertz.common.dto.CoordinateDto;
+import com.ajou.hertz.common.dto.FullAddressDto;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;
+import com.ajou.hertz.domain.user.dto.UserDto;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class PracticeRoomDto {
+
+    private Long id;
+    private UserDto seller;
+    private String title;
+    private FullAddressDto tradeAddress;
+    private Boolean hasSoundEquipment;
+    private Boolean hasInstrument;
+    private Integer pricePerDay;
+    private Integer pricePerHour;
+    private Integer pricePerMonth;
+    private Short capacity;
+    private String size;
+    private Boolean hasParkingLot;
+    private String description;
+    private CoordinateDto coordinate;
+    private List<PracticeRoomImageDto> images;
+    private List<String> hashtags;
+
+    public PracticeRoomDto(PracticeRoom practiceRoom) {
+        this(
+                practiceRoom.getId(),
+                UserDto.from(practiceRoom.getSeller()),
+                practiceRoom.getTitle(),
+                FullAddressDto.from(practiceRoom.getTradeAddress()),
+                practiceRoom.getHasSoundEquipment(),
+                practiceRoom.getHasInstrument(),
+                practiceRoom.getPricePerDay(),
+                practiceRoom.getPricePerHour(),
+                practiceRoom.getPricePerMonth(),
+                practiceRoom.getCapacity(),
+                practiceRoom.getSize(),
+                practiceRoom.getHasParkingLot(),
+                practiceRoom.getDescription().getDescription(),
+                CoordinateDto.from(practiceRoom.getCoordinate()),
+                practiceRoom.getImages().toDtos(),
+                practiceRoom.getHashtags().toStrings()
+        );
+    }
+
+    public static PracticeRoomDto from(PracticeRoom practiceRoom) {
+        return new PracticeRoomDto(practiceRoom);
+    }
+
+}
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/dto/request/CreateNewPracticeRoomRequest.java b/src/main/java/com/ajou/hertz/domain/practice_room/dto/request/CreateNewPracticeRoomRequest.java
new file mode 100644
index 0000000..8288167
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/dto/request/CreateNewPracticeRoomRequest.java
@@ -0,0 +1,102 @@
+package com.ajou.hertz.domain.practice_room.dto.request;
+
+import com.ajou.hertz.common.dto.request.CoordinateRequest;
+import com.ajou.hertz.common.dto.request.FullAddressRequest;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;
+import com.ajou.hertz.domain.user.entity.User;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hibernate.validator.constraints.Length;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Setter    // for multipart/form-data with @ModelAttribute
+@Getter
+public class CreateNewPracticeRoomRequest {
+
+    @Schema(description = "매물 제목", example = "Ajou Practice Room")
+    @NotBlank
+    private String title;
+
+    @Schema(description = "연습실 장소")
+    @NotNull
+    private FullAddressRequest tradeAddress;
+
+    @Schema(description = "좌표")
+    @NotNull
+    private CoordinateRequest coordinate;
+
+    @Schema(description = "사운드 장비 여부", example = "true")
+    @NotNull
+    private Boolean hasSoundEquipment;
+
+    @Schema(description = "악기 여부", example = "true")
+    @NotNull
+    private Boolean hasInstrument;
+
+    @Schema(description = "하루 대여 가격", example = "100000")
+    @NotNull
+    private Integer pricePerDay;
+
+    @Schema(description = "시간 당 가격", example = "10000")
+    @NotNull
+    private Integer pricePerHour;
+
+    @Schema(description = "월 별 대여 가격", example = "100000")
+    @NotNull
+    private Integer pricePerMonth;
+
+    @Schema(description = "수용 인원", example = "100")
+    @NotNull
+    private Short capacity;
+
+    @Schema(description = "합주실 크기", example = "1000")
+    @NotNull
+    private String size;
+
+    @Schema(description = "주차장 여부", example = "true")
+    @NotNull
+    private Boolean hasParkingLot;
+
+    @Schema(description = "특이사항 및 상세 설명. 내용이 없을 경우에는 빈 문자열로 요청하면 됩니다.", example = "Ajou PracticeRoom은 수원시에 위치해 있으며...")
+    @NotNull
+    private String description;
+
+    @Schema(description = "합주실 이미지 (최소 4개, 최대 7개)")
+    @NotNull
+    @Size(min = 4, max = 7)
+    private List<MultipartFile> images;
+
+    @Schema(description = "해시태그(각 해시태그마다 최대 10글자)", example = "[\"밴드\", \"락\"]")
+    private List<@NotBlank @Length(max = 10) String> hashtags;
+
+
+    public PracticeRoom toEntity(User seller) {
+        return PracticeRoom.create(
+                seller,
+                getTitle(),
+                getDescription(),
+                getCapacity(),
+                getSize(),
+                getHasSoundEquipment(),
+                getHasInstrument(),
+                getPricePerHour(),
+                getPricePerDay(),
+                getPricePerMonth(),
+                getHasParkingLot(),
+                getTradeAddress().toEntity(),
+                getCoordinate().toEntity()
+        );
+    }
+
+}
+
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomImageResponse.java b/src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomImageResponse.java
new file mode 100644
index 0000000..6c00e18
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomImageResponse.java
@@ -0,0 +1,35 @@
+package com.ajou.hertz.domain.practice_room.dto.response;
+
+import com.ajou.hertz.domain.practice_room.dto.PracticeRoomImageDto;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.annotation.Nullable;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public class PracticeRoomImageResponse {
+
+    @Schema(description = "Id of instrument image", example = "3")
+    private Long id;
+
+    @Schema(description = "이미지 이름", example = "fender-guitar.jpg")
+    private String name;
+
+    @Schema(description = "이미지 url", example = "https://instrument-image-url")
+    private String url;
+
+    public static PracticeRoomImageResponse from(@Nullable PracticeRoomImageDto practiceRoomImageDto) {
+        if (practiceRoomImageDto == null) {
+            return null;
+        }
+        return new PracticeRoomImageResponse(
+                practiceRoomImageDto.getId(),
+                practiceRoomImageDto.getName(),
+                practiceRoomImageDto.getUrl()
+        );
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomResponse.java b/src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomResponse.java
new file mode 100644
index 0000000..a7c0014
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/dto/response/PracticeRoomResponse.java
@@ -0,0 +1,91 @@
+package com.ajou.hertz.domain.practice_room.dto.response;
+
+import com.ajou.hertz.common.dto.response.CoordinateResponse;
+import com.ajou.hertz.common.dto.response.FullAddressResponse;
+import com.ajou.hertz.domain.practice_room.dto.PracticeRoomDto;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class PracticeRoomResponse {
+
+    @Schema(description = "Id of practiceRoom", example = "2")
+    private Long id;
+
+    @Schema(description = "Id of seller", example = "1")
+    private Long sellerId;
+
+    @Schema(description = "매물 제목", example = "Ajou Practice Room")
+    private String title;
+
+    @Schema(description = "합주실 장소")
+    private FullAddressResponse tradeAddress;
+
+    @Schema(description = "사운드 장비 여부", example = "true")
+    private Boolean hasSoundEquipment;
+
+    @Schema(description = "악기 여부", example = "true")
+    private Boolean hasInstrument;
+
+    @Schema(description = "하루 대여 가격", example = "100000")
+    private Integer pricePerDay;
+
+    @Schema(description = "시간 당 가격", example = "10000")
+    private Integer pricePerHour;
+
+    @Schema(description = "월 별 대여 가격", example = "100000")
+    private Integer pricePerMonth;
+
+    @Schema(description = "수용 인원", example = "100")
+    private Short capacity;
+
+    @Schema(description = "합주실 크기", example = "1000")
+    private String size;
+
+    @Schema(description = "주차장 여부", example = "true")
+    private Boolean hasParkingLot;
+
+    @Schema(description = "특이사항 및 상세 설명. 내용이 없을 경우에는 빈 문자열로 요청하면 됩니다.", example = "Ajou PracticeRoom은 수원시에 위치해 있으며...")
+    private String description;
+
+    @Schema(description = "합주실 위치 좌표")
+    private CoordinateResponse coordinate;
+
+    @Schema(description = "합주실 이미지")
+    private List<PracticeRoomImageResponse> images;
+
+    @Schema(description = "해시태그", example = "[\"밴드\", \"락\"]")
+    private List<String> hashtags;
+
+    protected PracticeRoomResponse(PracticeRoomDto practiceRoomDto) {
+        this.id = practiceRoomDto.getId();
+        this.sellerId = practiceRoomDto.getSeller().getId();
+        this.title = practiceRoomDto.getTitle();
+        this.tradeAddress = FullAddressResponse.from(practiceRoomDto.getTradeAddress());
+        this.hasSoundEquipment = practiceRoomDto.getHasSoundEquipment();
+        this.hasInstrument = practiceRoomDto.getHasInstrument();
+        this.pricePerDay = practiceRoomDto.getPricePerDay();
+        this.pricePerHour = practiceRoomDto.getPricePerHour();
+        this.pricePerMonth = practiceRoomDto.getPricePerMonth();
+        this.capacity = practiceRoomDto.getCapacity();
+        this.size = practiceRoomDto.getSize();
+        this.hasParkingLot = practiceRoomDto.getHasParkingLot();
+        this.description = practiceRoomDto.getDescription();
+        this.coordinate = CoordinateResponse.from(practiceRoomDto.getCoordinate());
+        this.images = practiceRoomDto.getImages().stream()
+                .map(PracticeRoomImageResponse::from)
+                .toList();
+        this.hashtags = practiceRoomDto.getHashtags();
+    }
+
+    public static PracticeRoomResponse from(PracticeRoomDto practiceRoomDto) {
+        return new PracticeRoomResponse(practiceRoomDto);
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoom.java b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoom.java
index e3f0aea..ee3a2df 100644
--- a/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoom.java
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoom.java
@@ -4,76 +4,131 @@
 import com.ajou.hertz.common.entity.FullAddress;
 import com.ajou.hertz.common.entity.TimeTrackedBaseEntity;
 import com.ajou.hertz.domain.user.entity.User;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Embedded;
-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 jakarta.persistence.*;
 import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Inheritance(strategy = InheritanceType.JOINED)
 @Getter
 @Entity
 public class PracticeRoom extends TimeTrackedBaseEntity {
 
-	@Id
-	@GeneratedValue(strategy = GenerationType.IDENTITY)
-	@Column(name = "practice_room_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)
-	@Embedded
-	private FullAddress fullAddress;
-
-	@Column(nullable = false)
-	private Boolean hasSoundEquipment;
-
-	@Column(nullable = false)
-	private Boolean hasInstrument;
-
-	@Column(nullable = false)
-	private Integer pricePerDay;
-
-	@Column(nullable = false)
-	private Integer pricePerHour;
-
-	@Column(nullable = false)
-	private Integer pricePerMonth;
-
-	@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 PracticeRoomImages images = new PracticeRoomImages();
-
-	@Embedded
-	private PracticeRoomHashtags hashtags = new PracticeRoomHashtags();
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "practice_room_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;
+
+    @Embedded
+    private FullAddress tradeAddress;
+
+    @Column(nullable = false)
+    private Boolean hasSoundEquipment;
+
+    @Column(nullable = false)
+    private Boolean hasInstrument;
+
+    @Column(nullable = false)
+    private Integer pricePerDay;
+
+    @Column(nullable = false)
+    private Integer pricePerHour;
+
+    @Column(nullable = false)
+    private Integer pricePerMonth;
+
+    @Column(nullable = false)
+    private Short capacity;
+
+    @Column(nullable = false)
+    private String size;
+
+    @Column(nullable = false)
+    private Boolean hasParkingLot;
+
+    @Embedded
+    private PracticeRoomDescription description;
+
+    @Embedded
+    private Coordinate coordinate;
+
+    @Embedded
+    private PracticeRoomImages images = new PracticeRoomImages();
+
+    @Embedded
+    private PracticeRoomHashtags hashtags = new PracticeRoomHashtags();
+
+    protected PracticeRoom(
+            Long id,
+            User seller,
+            String title,
+            FullAddress tradeAddress,
+            Boolean hasSoundEquipment,
+            Boolean hasInstrument,
+            Integer pricePerDay,
+            Integer pricePerHour,
+            Integer pricePerMonth,
+            Short capacity,
+            String size,
+            Boolean hasParkingLot,
+            String description,
+            Coordinate coordinate
+
+    ) {
+        this.id = id;
+        this.seller = seller;
+        this.title = title;
+        this.tradeAddress = tradeAddress;
+        this.hasSoundEquipment = hasSoundEquipment;
+        this.hasInstrument = hasInstrument;
+        this.pricePerDay = pricePerDay;
+        this.pricePerHour = pricePerHour;
+        this.pricePerMonth = pricePerMonth;
+        this.capacity = capacity;
+        this.size = size;
+        this.hasParkingLot = hasParkingLot;
+        this.description = new PracticeRoomDescription(description);
+        this.coordinate = coordinate;
+    }
+
+    public static PracticeRoom create(
+            User seller,
+            String title,
+            String description,
+            Short capacity,
+            String size,
+            Boolean hasSoundEquipment,
+            Boolean hasInstrument,
+            Integer pricePerHour,
+            Integer pricePerDay,
+            Integer pricePerMonth,
+            Boolean hasParkingLot,
+            FullAddress tradeAddress,
+            Coordinate coordinate
+    ) {
+        return new PracticeRoom(
+                null,
+                seller,
+                title,
+                tradeAddress,
+                hasSoundEquipment,
+                hasInstrument,
+                pricePerDay,
+                pricePerHour,
+                pricePerMonth,
+                capacity,
+                size,
+                hasParkingLot,
+                description,
+                coordinate
+        );
+    }
 }
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomDescription.java b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomDescription.java
new file mode 100644
index 0000000..09e730e
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomDescription.java
@@ -0,0 +1,49 @@
+package com.ajou.hertz.domain.practice_room.entity;
+
+import com.ajou.hertz.domain.practice_room.exception.UnexpectedLinkInPracticeRoomDescriptionException;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Embeddable
+public class PracticeRoomDescription {
+
+    @Column(length = 1000, nullable = false)
+    private String description;
+
+    public PracticeRoomDescription(String description) {
+        validateDescription(description);
+        this.description = description;
+    }
+
+    private void validateDescription(String description) {
+        Matcher descriptionUrlMatcher = Pattern.compile("http://|https://|www\\.").matcher(description);
+        if (descriptionUrlMatcher.find()) {
+            throw new UnexpectedLinkInPracticeRoomDescriptionException();
+        }
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof PracticeRoomDescription that)) {
+            return false;
+        }
+        return Objects.equals(this.getDescription(), that.getDescription());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(getDescription());
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomHashtags.java b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomHashtags.java
index f620389..498d047 100644
--- a/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomHashtags.java
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomHashtags.java
@@ -1,20 +1,30 @@
 package com.ajou.hertz.domain.practice_room.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;
 
+import java.util.LinkedList;
+import java.util.List;
+
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @Embeddable
 public class PracticeRoomHashtags {
 
-	@OneToMany(mappedBy = "practiceRoom")
-	private List<PracticeRoomHashtag> content = new LinkedList<>();
+    @OneToMany(mappedBy = "practiceRoom")
+    private List<PracticeRoomHashtag> content = new LinkedList<>();
+
+    public void addAll(List<PracticeRoomHashtag> hashtags) {
+        content.addAll(hashtags);
+    }
+
 
+    public List<String> toStrings() {
+        return content.stream()
+                .map(PracticeRoomHashtag::getContent)
+                .toList();
+    }
 }
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomImages.java b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomImages.java
index 96e34b4..abedc78 100644
--- a/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomImages.java
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/entity/PracticeRoomImages.java
@@ -1,20 +1,31 @@
 package com.ajou.hertz.domain.practice_room.entity;
 
-import java.util.LinkedList;
-import java.util.List;
-
+import com.ajou.hertz.domain.practice_room.dto.PracticeRoomImageDto;
 import jakarta.persistence.Embeddable;
 import jakarta.persistence.OneToMany;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.NoArgsConstructor;
 
+import java.util.LinkedList;
+import java.util.List;
+
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @Embeddable
 
 public class PracticeRoomImages {
 
-	@OneToMany(mappedBy = "practiceRoom")
-	private List<PracticeRoomImage> content = new LinkedList<>();
+    @OneToMany(mappedBy = "practiceRoom")
+    private List<PracticeRoomImage> content = new LinkedList<>();
+
+    public void addAll(List<PracticeRoomImage> images) {
+        content.addAll(images);
+    }
+
+    public List<PracticeRoomImageDto> toDtos() {
+        return content.stream()
+                .map(PracticeRoomImageDto::from)
+                .toList();
+    }
 }
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/exception/UnexpectedLinkInPracticeRoomDescriptionException.java b/src/main/java/com/ajou/hertz/domain/practice_room/exception/UnexpectedLinkInPracticeRoomDescriptionException.java
new file mode 100644
index 0000000..b9f35f7
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/exception/UnexpectedLinkInPracticeRoomDescriptionException.java
@@ -0,0 +1,10 @@
+package com.ajou.hertz.domain.practice_room.exception;
+
+import com.ajou.hertz.common.exception.BadRequestException;
+import com.ajou.hertz.common.exception.constant.CustomExceptionType;
+
+public class UnexpectedLinkInPracticeRoomDescriptionException extends BadRequestException {
+    public UnexpectedLinkInPracticeRoomDescriptionException() {
+        super(CustomExceptionType.UNEXPECTED_LINK_IN_PRACTICEROOM_DESCRIPTION);
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomCommandService.java b/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomCommandService.java
new file mode 100644
index 0000000..c288fb2
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomCommandService.java
@@ -0,0 +1,54 @@
+package com.ajou.hertz.domain.practice_room.service;
+
+import com.ajou.hertz.domain.practice_room.dto.PracticeRoomDto;
+import com.ajou.hertz.domain.practice_room.dto.request.CreateNewPracticeRoomRequest;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoomHashtag;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoomImage;
+import com.ajou.hertz.domain.practice_room.repository.PracticeRoomRepository;
+import com.ajou.hertz.domain.user.entity.User;
+import com.ajou.hertz.domain.user.service.UserQueryService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Transactional
+@Service
+public class PracticeRoomCommandService {
+
+    private final PracticeRoomRepository practiceRoomRepository;
+    private final UserQueryService userQueryService;
+    private final PracticeRoomImageCommandService practiceRoomImageCommandService;
+    private final PracticeRoomHashtagCommandService practiceRoomHashtagCommandService;
+
+    /**
+     * 신규 합주실 매물을 생성 및 저장한다.
+     *
+     * @param sellerId                     악기 판매자의 id
+     * @param createNewPracticeRoomRequest 판매하고자 하는 합주실의 정보
+     * @return 생성된 합주실 entity
+     */
+    public PracticeRoomDto createNewPracticeRoom(Long sellerId, CreateNewPracticeRoomRequest createNewPracticeRoomRequest) {
+        User seller = userQueryService.getById(sellerId);
+        PracticeRoom practiceRoom = practiceRoomRepository.save(createNewPracticeRoomRequest.toEntity(seller));
+
+        List<PracticeRoomImage> savedPracticeRoomImages = practiceRoomImageCommandService.saveImages(
+                practiceRoom,
+                createNewPracticeRoomRequest.getImages()
+        );
+        practiceRoom.getImages().addAll(savedPracticeRoomImages);
+
+        List<PracticeRoomHashtag> savedPracticeRoomHashtags = practiceRoomHashtagCommandService.saveHashtags(
+                practiceRoom,
+                createNewPracticeRoomRequest.getHashtags()
+        );
+        practiceRoom.getHashtags().addAll(savedPracticeRoomHashtags);
+
+        PracticeRoom updatedPracticeRoom = practiceRoomRepository.save(practiceRoom);
+
+        return new PracticeRoomDto(updatedPracticeRoom);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomHashtagCommandService.java b/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomHashtagCommandService.java
new file mode 100644
index 0000000..6a9b014
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomHashtagCommandService.java
@@ -0,0 +1,32 @@
+package com.ajou.hertz.domain.practice_room.service;
+
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoomHashtag;
+import com.ajou.hertz.domain.practice_room.repository.PracticeRoomHashtagRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Transactional
+@Service
+public class PracticeRoomHashtagCommandService {
+
+    private final PracticeRoomHashtagRepository practiceRoomHashtagRepository;
+
+    /**
+     * 전달된 hashtag content list로 <code>PracticeRoomHashtag</code> list를 만들어 저장 및 등록한다.
+     *
+     * @param practiceRoom hashtag가 작성된 합주실
+     * @param hashtags     hashtag list
+     * @return 생성된 hashtag list
+     */
+    public List<PracticeRoomHashtag> saveHashtags(PracticeRoom practiceRoom, List<String> hashtags) {
+        List<PracticeRoomHashtag> practiceRoomHashtags = hashtags.stream()
+                .map(hashtagContent -> PracticeRoomHashtag.create(practiceRoom, hashtagContent))
+                .toList();
+        return practiceRoomHashtagRepository.saveAll(practiceRoomHashtags);
+    }
+}
diff --git a/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomImageCommandService.java b/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomImageCommandService.java
new file mode 100644
index 0000000..e30ec20
--- /dev/null
+++ b/src/main/java/com/ajou/hertz/domain/practice_room/service/PracticeRoomImageCommandService.java
@@ -0,0 +1,43 @@
+package com.ajou.hertz.domain.practice_room.service;
+
+import com.ajou.hertz.common.file.service.FileService;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;
+import com.ajou.hertz.domain.practice_room.entity.PracticeRoomImage;
+import com.ajou.hertz.domain.practice_room.repository.PracticeRoomImageRepository;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Transactional
+@Service
+public class PracticeRoomImageCommandService {
+
+    private static final String PRACTICEROOM_IMAGE_UPLOAD_PATH = "practiceRoom-image/";
+
+    private final FileService fileService;
+    private final PracticeRoomImageRepository practiceRoomImageRepository;
+
+    /**
+     * 전달받은 multipart files로 practiceRoom image entity를 생성 후 저장한다.
+     *
+     * @param practiceRoom 이미지의 대상이 되는 practiceRoom entity
+     * @param images       저장하고자 하는 image
+     * @return 저장된 practiceRoom images
+     */
+    public List<PracticeRoomImage> saveImages(PracticeRoom practiceRoom, Iterable<MultipartFile> images) {
+        List<PracticeRoomImage> practiceRoomImages = fileService
+                .uploadFiles(images, PRACTICEROOM_IMAGE_UPLOAD_PATH)
+                .stream()
+                .map(fileDto -> PracticeRoomImage.create(
+                        practiceRoom,
+                        fileDto.getOriginalName(),
+                        fileDto.getStoredName(),
+                        fileDto.getUrl()
+                )).toList();
+        return practiceRoomImageRepository.saveAll(practiceRoomImages);
+    }
+}