Full output
Antigravity 원문 결과물
콘텐츠 상세 조회 API 구현 / Google / Gemini 3.6 Flash / 실행일 2026-07-22
02. 콘텐츠 상세 조회 API 구현 (Antigravity)
추가된 파일 목록
src/main/java/com/samebrief/content/content/domain/ContentStatus.javasrc/main/java/com/samebrief/content/content/domain/Content.javasrc/main/java/com/samebrief/content/content/repository/ContentRepository.javasrc/main/java/com/samebrief/content/content/dto/ContentResponse.javasrc/main/java/com/samebrief/content/content/dto/ErrorResponse.javasrc/main/java/com/samebrief/content/content/service/ContentService.javasrc/main/java/com/samebrief/content/content/controller/ContentController.javasrc/test/java/com/samebrief/content/content/controller/ContentControllerTest.java
src/main/java/com/samebrief/content/content/domain/ContentStatus.java
package com.samebrief.content.content.domain;
public enum ContentStatus {
PUBLISHED,
DELETED
}
src/main/java/com/samebrief/content/content/domain/Content.java
package com.samebrief.content.content.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "contents")
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String slug;
@Column(nullable = false)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String body;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ContentStatus status;
protected Content() {
}
public Content(Long id, String slug, String title, String body, ContentStatus status) {
this.id = id;
this.slug = slug;
this.title = title;
this.body = body;
this.status = status;
}
public Long getId() {
return id;
}
public String getSlug() {
return slug;
}
public String getTitle() {
return title;
}
public String getBody() {
return body;
}
public ContentStatus getStatus() {
return status;
}
}
src/main/java/com/samebrief/content/content/repository/ContentRepository.java
package com.samebrief.content.content.repository;
import com.samebrief.content.content.domain.Content;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ContentRepository extends JpaRepository<Content, Long> {
}
src/main/java/com/samebrief/content/content/dto/ContentResponse.java
package com.samebrief.content.content.dto;
import com.samebrief.content.content.domain.Content;
public record ContentResponse(
Long id,
String slug,
String title,
String body
) {
public static ContentResponse from(Content content) {
return new ContentResponse(
content.getId(),
content.getSlug(),
content.getTitle(),
content.getBody()
);
}
}
src/main/java/com/samebrief/content/content/dto/ErrorResponse.java
package com.samebrief.content.content.dto;
public record ErrorResponse(
String code,
String message
) {
}
src/main/java/com/samebrief/content/content/service/ContentService.java
package com.samebrief.content.content.service;
import com.samebrief.content.content.domain.Content;
import com.samebrief.content.content.domain.ContentStatus;
import com.samebrief.content.content.dto.ContentResponse;
import com.samebrief.content.content.repository.ContentRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
public class ContentService {
private final ContentRepository contentRepository;
public ContentService(ContentRepository contentRepository) {
this.contentRepository = contentRepository;
}
public ContentResponse getContent(Long id) {
if (id == null || id < 1) {
throw new InvalidContentIdException();
}
Content content = contentRepository.findById(id)
.orElseThrow(ContentNotFoundException::new);
if (content.getStatus() == ContentStatus.DELETED) {
throw new ContentDeletedException();
}
return ContentResponse.from(content);
}
public static class ContentNotFoundException extends RuntimeException {
}
public static class ContentDeletedException extends RuntimeException {
}
public static class InvalidContentIdException extends RuntimeException {
}
}
src/main/java/com/samebrief/content/content/controller/ContentController.java
package com.samebrief.content.content.controller;
import com.samebrief.content.content.dto.ContentResponse;
import com.samebrief.content.content.dto.ErrorResponse;
import com.samebrief.content.content.service.ContentService;
import com.samebrief.content.content.service.ContentService.ContentDeletedException;
import com.samebrief.content.content.service.ContentService.ContentNotFoundException;
import com.samebrief.content.content.service.ContentService.InvalidContentIdException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@RestController
@RequestMapping("/api/v1/contents")
public class ContentController {
private final ContentService contentService;
public ContentController(ContentService contentService) {
this.contentService = contentService;
}
@GetMapping("/{id}")
public ResponseEntity<ContentResponse> getContent(@PathVariable Long id) {
ContentResponse response = contentService.getContent(id);
return ResponseEntity.ok(response);
}
@ExceptionHandler(InvalidContentIdException.class)
public ResponseEntity<ErrorResponse> handleInvalidId(InvalidContentIdException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("INVALID_CONTENT_ID", "콘텐츠 ID는 1 이상의 정수여야 합니다."));
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("INVALID_CONTENT_ID", "콘텐츠 ID는 1 이상의 정수여야 합니다."));
}
@ExceptionHandler(ContentNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ContentNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("CONTENT_NOT_FOUND", "콘텐츠를 찾을 수 없습니다."));
}
@ExceptionHandler(ContentDeletedException.class)
public ResponseEntity<ErrorResponse> handleDeleted(ContentDeletedException e) {
return ResponseEntity.status(HttpStatus.GONE)
.body(new ErrorResponse("CONTENT_DELETED", "삭제된 콘텐츠입니다."));
}
}
src/test/java/com/samebrief/content/content/controller/ContentControllerTest.java
package com.samebrief.content.content.controller;
import com.samebrief.content.content.dto.ContentResponse;
import com.samebrief.content.content.service.ContentService;
import com.samebrief.content.content.service.ContentService.ContentDeletedException;
import com.samebrief.content.content.service.ContentService.ContentNotFoundException;
import com.samebrief.content.content.service.ContentService.InvalidContentIdException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(ContentController.class)
class ContentControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ContentService contentService;
@Test
@DisplayName("공개 상태 콘텐츠의 정상 조회: 200 OK 및 응답 필드 검증")
void getContent_success() throws Exception {
// given
Long id = 42L;
ContentResponse response = new ContentResponse(42L, "spring-api-basics", "Spring API 기초", "본문입니다.");
given(contentService.getContent(id)).willReturn(response);
// when & then
mockMvc.perform(get("/api/v1/contents/{id}", id)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(42))
.andExpect(jsonPath("$.slug").value("spring-api-basics"))
.andExpect(jsonPath("$.title").value("Spring API 기초"))
.andExpect(jsonPath("$.body").value("본문입니다."))
.andExpect(jsonPath("$.status").doesNotExist());
}
@Test
@DisplayName("존재하지 않는 ID 조회: 404 Not Found 및 CONTENT_NOT_FOUND 응답")
void getContent_notFound() throws Exception {
// given
Long id = 999L;
given(contentService.getContent(id)).willThrow(new ContentNotFoundException());
// when & then
mockMvc.perform(get("/api/v1/contents/{id}", id)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("CONTENT_NOT_FOUND"))
.andExpect(jsonPath("$.message").value("콘텐츠를 찾을 수 없습니다."));
}
@Test
@DisplayName("유효하지 않은 ID (0 이하 정수) 조회: 400 Bad Request 및 INVALID_CONTENT_ID 응답")
void getContent_invalidId_nonPositive() throws Exception {
// given
Long id = 0L;
given(contentService.getContent(id)).willThrow(new InvalidContentIdException());
// when & then
mockMvc.perform(get("/api/v1/contents/{id}", id)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("INVALID_CONTENT_ID"))
.andExpect(jsonPath("$.message").value("콘텐츠 ID는 1 이상의 정수여야 합니다."));
}
@Test
@DisplayName("유효하지 않은 ID (비숫자) 조회: 400 Bad Request 및 INVALID_CONTENT_ID 응답")
void getContent_invalidId_typeMismatch() throws Exception {
// when & then
mockMvc.perform(get("/api/v1/contents/{id}", "invalid-id")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("INVALID_CONTENT_ID"))
.andExpect(jsonPath("$.message").value("콘텐츠 ID는 1 이상의 정수여야 합니다."));
}
@Test
@DisplayName("삭제된 콘텐츠 조회: 410 Gone 및 CONTENT_DELETED 응답")
void getContent_deleted() throws Exception {
// given
Long id = 42L;
given(contentService.getContent(id)).willThrow(new ContentDeletedException());
// when & then
mockMvc.perform(get("/api/v1/contents/{id}", id)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isGone())
.andExpect(jsonPath("$.code").value("CONTENT_DELETED"))
.andExpect(jsonPath("$.message").value("삭제된 콘텐츠입니다."));
}
}
실행 명령 및 예상 테스트 결과
테스트 실행 명령
./gradlew test --tests "com.samebrief.content.content.controller.ContentControllerTest"
예상 테스트 결과
BUILD SUCCESSFUL in 2s
5 tests completed, 0 failed