[BE:OUR] 공간 데이터 조회/수정(PUT)/삭제 API 구현
[기능 흐름과 필요한 개발 스택]
1. 기능 흐름: space id 받으면 space에 관한 데이터를 조회/수정/삭제 가능
- 조회
1) 간단(요약) 정보 조회: hostId를 통해 이름, 주소(동까지), 가격, 태그 목록, 썸네일 이미지 URL 반환.
+ 주소 데이터를 동까지만 표시하도록 추가적인 메소드 구현
2) 상세 정보 조회: hostId를 통해 공간 정보와 관련된 모든 데이터 반환
- 수정(PUT, PATCH 버전)
- 삭제(소프트 삭제 방식)
2. 필요한 기술 스택
- Spring Boot(REST API 서버)
- JPA (Hibernate): DB 저장용 ORM
- MySQL(RDB)
1. 간단 정보 조회 API
SpaceSimpleResponseDto
@Getter
@AllArgsConstructor
public class SpaceSimpleResponseDto {
private String name;
private String address;
private int pricePerHour;
private List<String> tags;
private String thumbnailUrl;
}
SpaceService
@Transactional(readOnly = true)
public SpaceSimpleResponseDto getSimpleSpaceInfo(Long spaceId) {
Space space = spaceRepository.findById(spaceId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 공간입니다."));
List<String> tagContents = space.getTags().stream()
.map(Tag::getContents)
.collect(Collectors.toList());
return new SpaceSimpleResponseDto(
space.getName(),
extractDongFromAddress(space.getAddress()),
space.getPricePerHour(),
tagContents,
space.getThumbnailUrl()
);
}
// ex: "서울시 강남구 역삼동 어딘가 123" -> "서울시 강남구 역삼동"
private String extractDongFromAddress(String address) {
String[] parts = address.split(" ");
return parts.length >= 3 ? String.join(" ", parts[0], parts[1], parts[2]) : address;
}
*@Transactional(readOnly = true)
- @Transactional: 메서드나 클래스에 트랜잭션(=데이터베이스의 '하나의 작업 단위')을 설정.
- readOnly = true: 이 옵션을 주면 쓰기 작업은 막고, 읽기 성능을 최적화함. 즉, 조회만 하는 메서드에 붙인다.
*space.getTags(): space객체와 연관된(@OneToMany(mappedBy = "space")기반으로) Tag 엔티티들의 리스트를 가져옴.
SpaceController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/spaces")
public class SpaceController {
private final SpaceService spaceService;
@GetMapping("/{id}/simple")
public ResponseEntity<SpaceSimpleResponseDto> getSimpleInfo(@PathVariable Long id) {
return ResponseEntity.ok(spaceService.getSimpleSpaceInfo(id));
}
}
*ResponseEntity.ok(): 정상적인 응답을 의미(응답 200 OK + 본문(body) 객체)
HTTP/1.1 200 OK
Content-Type: application/json
{
"name": "비워 공간",
"price_per_hour": 15000,
"tags": ["카페", "단독", "브런치"]
}
2. 상세 정보 조회 API
SpaceDetailResponseDto
@Getter
@Builder
public class SpaceDetailResponseDto {
private Long id;
private String name;
private String address;
private String detailAddress;
private int pricePerHour;
private int maxCapacity;
private SpaceCategory spaceCategory;
private UseCategory useCategory;
private Double avgRating;
// Description
private String description;
private String priceGuide;
private String facilityNotice;
private String notice;
private String locationDescription;
private String refundPolicy;
private String websiteUrl;
private List<String> tags;
private List<AvailableTimeDto> availableTimes;
private List<String> imageUrls;
@Getter
@AllArgsConstructor
public static class AvailableTimeDto {
private LocalDate date;
private LocalTime startTime;
private LocalTime endTime;
}
}
SpaceService
@Transactional(readOnly = true)
public SpaceDetailResponseDto getDetailedSpaceInfo(Long spaceId) {
Space space = spaceRepository.findById(spaceId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 공간입니다."));
Description desc = space.getDescription();
return SpaceDetailResponseDto.builder()
.id(space.getId())
.name(space.getName())
.address(space.getAddress())
.detailAddress(space.getDetailAddress())
.pricePerHour(space.getPricePerHour())
.maxCapacity(space.getMaxCapacity())
.spaceCategory(space.getSpaceCategory())
.useCategory(space.getUseCategory())
.avgRating(space.getAvgRating())
.description(desc.getDescription())
.priceGuide(desc.getPriceGuide())
.facilityNotice(desc.getFacilityNotice())
.notice(desc.getNotice())
.locationDescription(desc.getLocationDescription())
.refundPolicy(desc.getRefundPolicy())
.websiteUrl(desc.getWebsiteUrl())
.tags(space.getTags().stream().map(Tag::getContents).toList())
.availableTimes(space.getAvailableTimes().stream()
.map(t -> new SpaceDetailResponseDto.AvailableTimeDto(
t.getDate(), t.getStartTime(), t.getEndTime()))
.toList())
.imageUrls(space.getSpaceImages().stream().map(SpaceImage::getImageUrl).toList())
.build();
}
SpaceController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/spaces")
public class SpaceController {
private final SpaceService spaceService;
@GetMapping("/{id}")
public ResponseEntity<SpaceDetailResponseDto> getDetailInfo(@PathVariable Long id) {
return ResponseEntity.ok(spaceService.getDetailedSpaceInfo(id));
}
}
3. 정보 수정 API - PUT(PATCH는 다음 글에 작성 예정)
SpaceUpdateRequestDto
@Getter
public class SpaceUpdateRequestDto {
private String name;
private String address;
private String detailAddress;
private int pricePerHour;
private int maxCapacity;
private SpaceCategory spaceCategory;
private UseCategory useCategory;
private String thumbnailUrl;
// Description
private String description;
private String priceGuide;
private String facilityNotice;
private String notice;
private String locationDescription;
private String refundPolicy;
private String websiteUrl;
private List<String> tags;
private List<AvailableTimeDto> availableTimes;
private List<String> imageUrls;
@Getter
public static class AvailableTimeDto {
private LocalDate date;
private LocalTime startTime;
private LocalTime endTime;
}
}
SpaceService
@Transactional
public void updateSpace(Long spaceId, SpaceUpdateRequestDto dto) {
Space space = spaceRepository.findById(spaceId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 공간입니다."));
double[] latLng = kakaoMapService.getLatLng(dto.getAddress());
// 1. Space 수정
space.update(
dto.getName(), dto.getAddress(), dto.getDetailAddress(), dto.getPricePerHour(),
dto.getMaxCapacity(), dto.getSpaceCategory(), dto.getUseCategory(),
dto.getThumbnailUrl(), latLng[0], latLng[1]
);
// 2. Description 수정
Description desc = space.getDescription();
desc.update(
dto.getDescription(), dto.getPriceGuide(), dto.getFacilityNotice(), dto.getNotice(),
dto.getLocationDescription(), dto.getRefundPolicy(), dto.getWebsiteUrl()
);
// 3. Tags 재저장
tagRepository.deleteAll(space.getTags());
List<Tag> tags = dto.getTags().stream()
.map(content -> Tag.builder().space(space).contents(content).build())
.toList();
tagRepository.saveAll(tags);
// 4. AvailableTimes 재저장
availableTimeRepository.deleteAll(space.getAvailableTimes());
List<AvailableTime> times = dto.getAvailableTimes().stream()
.map(t -> AvailableTime.builder()
.space(space)
.date(t.getDate())
.startTime(t.getStartTime())
.endTime(t.getEndTime())
.build())
.toList();
availableTimeRepository.saveAll(times);
// 5. Images 재저장
spaceImageRepository.deleteAll(space.getSpaceImages());
List<SpaceImage> images = dto.getImageUrls().stream()
.map(url -> SpaceImage.builder().space(space).imageUrl(url).build())
.toList();
spaceImageRepository.saveAll(images);
}
Space
public void update(String name, String address, String detailAddress, int pricePerHour,
int maxCapacity, SpaceCategory spaceCategory, UseCategory useCategory,
String thumbnailUrl, double lat, double lng) {
this.name = name;
this.address = address;
this.detailAddress = detailAddress;
this.pricePerHour = pricePerHour;
this.maxCapacity = maxCapacity;
this.spaceCategory = spaceCategory;
this.useCategory = useCategory;
this.thumbnailUrl = thumbnailUrl;
this.latitude = lat;
this.longitude = lng;
}
Description
public void update(String description, String priceGuide, String facilityNotice,
String notice, String locationDescription, String refundPolicy, String websiteUrl) {
this.description = description;
this.priceGuide = priceGuide;
this.facilityNotice = facilityNotice;
this.notice = notice;
this.locationDescription = locationDescription;
this.refundPolicy = refundPolicy;
this.websiteUrl = websiteUrl;
}
SpaceController
@PutMapping("/{id}")
public ResponseEntity<Void> updateSpace(@PathVariable Long id,
@RequestBody SpaceUpdateRequestDto dto) {
spaceService.updateSpace(id, dto);
return ResponseEntity.noContent().build();
}
*@PutMapping: RESTful API에서 리소스 전체를 수정(갱신)할 때 사용
*ResponseEntity.noContent().build(): HTTP 응답 204 No Content를 반환
(요청은 성공적으로 처리 + 응답 본문은 없음 -> 삭제 요청 시 자주 사용)
4. 정보 삭제 API(소프트 삭제 방식)
Space
public void delete() {
this.deletedAt = LocalDateTime.now();
}
소프트 삭제(Soft Delete) 방식
: DB에서 데이터를 실제로 지우지 않고, 'deleted_at', 'is_deleted' 같은 컬럼으로 삭제된 것처럼 처리하는 방식.
-> 보통은 사용자 정보, 게시글, 주문 등 이력이 중요한 데이터에 소프트 삭제를 사용함.
SpaceService
@Transactional
public void deleteSpace(Long spaceId) {
Space space = spaceRepository.findById(spaceId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 공간입니다."));
space.delete();
}
SpaceController
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteSpace(@PathVariable Long id) {
spaceService.deleteSpace(id);
return ResponseEntity.noContent().build();
}