diff --git a/back/src/main/java/com/back/domain/post/controller/PostController.java b/back/src/main/java/com/back/domain/post/controller/PostController.java index 2e50ef5..5bbbbe4 100644 --- a/back/src/main/java/com/back/domain/post/controller/PostController.java +++ b/back/src/main/java/com/back/domain/post/controller/PostController.java @@ -1,9 +1,19 @@ package com.back.domain.post.controller; +import com.back.domain.post.dto.PostRequest; +import com.back.domain.post.dto.PostResponse; import com.back.domain.post.service.PostService; +import com.back.global.common.ApiResponse; +import com.back.global.common.PageResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; /** * 게시글 관련 API 요청을 처리하는 컨트롤러. @@ -15,4 +25,40 @@ public class PostController { private final PostService postService; + // 게시글 생성 + @PostMapping + public ApiResponse createPost( + @RequestBody @Valid PostRequest request) { + Long userId = 1L; // fixme 임시 사용자 ID + PostResponse response = postService.createPost(userId, request); + return ApiResponse.success(response, "성공적으로 생성되었습니다.", HttpStatus.OK); + } + + // 게시글 목록 조회 + @GetMapping + public ApiResponse> getPosts(Pageable pageable) { + Page responses = postService.getPosts(pageable); + return ApiResponse.success(PageResponse.of(responses), "성공적으로 조회되었습니다.", HttpStatus.OK); + } + + // 게시글 단건 조회 + @GetMapping("/{postId}") + public ApiResponse getPost(@PathVariable Long postId) { + return ApiResponse.success(postService.getPost(postId), "성공적으로 조회되었습니다.", HttpStatus.OK); + } + + @PutMapping("/{postId}") + public ApiResponse updatePost( + @PathVariable Long postId, + @RequestBody @Valid PostRequest request) { + Long userId = 1L; // fixme 임시 사용자 ID + return ApiResponse.success(postService.updatePost(userId, postId, request), "성공적으로 수정되었습니다.", HttpStatus.OK); + } + + @DeleteMapping("/{postId}") + public ApiResponse deletePost(@PathVariable Long postId) { + Long userId = 1L; // fixme 임시 사용자 ID + postService.deletePost(userId, postId); + return ApiResponse.success(null, "성공적으로 삭제되었습니다.", HttpStatus.OK); + } } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/post/dto/PostRequest.java b/back/src/main/java/com/back/domain/post/dto/PostRequest.java new file mode 100644 index 0000000..badd03e --- /dev/null +++ b/back/src/main/java/com/back/domain/post/dto/PostRequest.java @@ -0,0 +1,18 @@ +package com.back.domain.post.dto; + +import com.back.domain.post.enums.PostCategory; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record PostRequest( + @NotBlank(message = "제목은 필수입니다") + @Size(max = 200, message = "제목은 200자를 초과할 수 없습니다") + String title, + + @NotBlank(message = "내용은 필수입니다") + String content, + + @NotNull(message = "카테고리는 필수입니다") + PostCategory category +) { } diff --git a/back/src/main/java/com/back/domain/post/dto/PostResponse.java b/back/src/main/java/com/back/domain/post/dto/PostResponse.java new file mode 100644 index 0000000..31008eb --- /dev/null +++ b/back/src/main/java/com/back/domain/post/dto/PostResponse.java @@ -0,0 +1,25 @@ +package com.back.domain.post.dto; + +import com.back.domain.post.enums.PostCategory; + +import java.time.LocalDateTime; + +/** + * @param id + * @param title + * @param content + * @param category + * @param hide + * @param likeCount + * @param createdDate + * fixme @param createdBy 추가 예정 + */ +public record PostResponse( + Long id, + String title, + String content, + PostCategory category, + boolean hide, + int likeCount, + LocalDateTime createdDate +) { } diff --git a/back/src/main/java/com/back/domain/post/entity/Post.java b/back/src/main/java/com/back/domain/post/entity/Post.java index 4de5159..b322cf9 100644 --- a/back/src/main/java/com/back/domain/post/entity/Post.java +++ b/back/src/main/java/com/back/domain/post/entity/Post.java @@ -1,14 +1,14 @@ package com.back.domain.post.entity; +import com.back.domain.post.enums.PostCategory; import com.back.domain.user.entity.User; import com.back.global.baseentity.BaseEntity; +import com.back.global.exception.ApiException; +import com.back.global.exception.ErrorCode; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.service.spi.ServiceException; import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDateTime; @@ -19,9 +19,8 @@ */ @Entity @Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) @Builder public class Post extends BaseEntity { @@ -32,13 +31,16 @@ public class Post extends BaseEntity { @Column(length = 200) private String title; - @Column(length = 200) - private String category; + @Column(length = 20) + @Enumerated(EnumType.STRING) + private PostCategory category; @Column(columnDefinition = "TEXT") private String content; - @Column(columnDefinition = "jsonb") + /** + * JSON 데이터를 단순 문자열로 저장 (예: {"option1": 10, "option2": 5}) + */ private String voteContent; @Column(nullable = false) @@ -49,4 +51,19 @@ public class Post extends BaseEntity { @LastModifiedDate private LocalDateTime updatedAt; + + public void assignUser(User user) { + this.user = user; + } + + public void updatePost(String title, String content, PostCategory category) { + this.title = title; + this.content = content; + this.category = category; + } + + public void checkUser(User targetUser) { + if (!user.equals(targetUser)) + throw new ApiException(ErrorCode.UNAUTHORIZED_USER); + } } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/post/enums/PostCategory.java b/back/src/main/java/com/back/domain/post/enums/PostCategory.java new file mode 100644 index 0000000..b6b4ed8 --- /dev/null +++ b/back/src/main/java/com/back/domain/post/enums/PostCategory.java @@ -0,0 +1,7 @@ +package com.back.domain.post.enums; + +public enum PostCategory { + CHAT, // 잡담 + SCENARIO, // 시나리오 첨부 + POLL // 투표 +} diff --git a/back/src/main/java/com/back/domain/post/mapper/PostMapper.java b/back/src/main/java/com/back/domain/post/mapper/PostMapper.java new file mode 100644 index 0000000..c1c1cd3 --- /dev/null +++ b/back/src/main/java/com/back/domain/post/mapper/PostMapper.java @@ -0,0 +1,41 @@ +package com.back.domain.post.mapper; + +import com.back.domain.post.dto.PostRequest; +import com.back.domain.post.dto.PostResponse; +import com.back.domain.post.entity.Post; + +import java.util.List; + +/** + * PostMapper + * 엔티티와 PostRequest, PostResponse 간의 변환을 담당하는 매퍼 클래스 + */ +public abstract class PostMapper { + public static Post toEntity(PostRequest request) { + return Post.builder() + .title(request.title()) + .content(request.content()) + .category(request.category()) + .hide(false) + .likeCount(0) + .build(); + } + + public static PostResponse toResponse(Post post) { + return new PostResponse( + post.getId(), + post.getTitle(), + post.getContent(), + post.getCategory(), + post.isHide(), + post.getLikeCount(), + post.getCreatedDate() + ); + } + + public static List toResponseList(List posts) { + return posts.stream() + .map(PostMapper::toResponse) + .toList(); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/post/service/PostService.java b/back/src/main/java/com/back/domain/post/service/PostService.java index 63f4233..5e48028 100644 --- a/back/src/main/java/com/back/domain/post/service/PostService.java +++ b/back/src/main/java/com/back/domain/post/service/PostService.java @@ -1,16 +1,82 @@ package com.back.domain.post.service; +import com.back.domain.post.dto.PostRequest; +import com.back.domain.post.dto.PostResponse; +import com.back.domain.post.entity.Post; +import com.back.domain.post.mapper.PostMapper; import com.back.domain.post.repository.PostRepository; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.global.exception.ApiException; +import com.back.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; /** * 게시글 관련 비즈니스 로직을 처리하는 서비스. */ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class PostService { + private final UserRepository userRepository; private final PostRepository postRepository; + @Transactional + public PostResponse createPost(Long userId, PostRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + Post post = PostMapper.toEntity(request); + post.assignUser(user); + Post savedPost = postRepository.save(post); + + return PostMapper.toResponse(savedPost); + } + + public PostResponse getPost(Long postId) { + return postRepository.findById(postId) + .filter(post -> !post.isHide()) + .map(PostMapper::toResponse) + .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); + } + + public Page getPosts(Pageable pageable) { + return postRepository.findAll(pageable) + .map(PostMapper::toResponse); + } + + @Transactional + public PostResponse updatePost(Long userId, Long postId, PostRequest request) { + Post post = validatePostOwnership(userId, postId); + + post.updatePost(request.title(), request.content(), request.category()); + + return PostMapper.toResponse(post); + } + + @Transactional + public void deletePost(Long userId, Long postId) { + Post post = validatePostOwnership(userId, postId); + postRepository.delete(post); + } + + private Post validatePostOwnership(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); + + User requestUser = userRepository.findById(userId) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + post.checkUser(requestUser); + + return post; + } } \ No newline at end of file diff --git a/back/src/main/java/com/back/global/common/ApiResponse.java b/back/src/main/java/com/back/global/common/ApiResponse.java new file mode 100644 index 0000000..97b4f4d --- /dev/null +++ b/back/src/main/java/com/back/global/common/ApiResponse.java @@ -0,0 +1,37 @@ +package com.back.global.common; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +/** + * 공통 응답 형태 + * { + * "data": { ... }, + * "message": "성공적으로 조회되었습니다.", + * "status": 200 + * } + */ +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + + private final T data; + private final String message; + private final int status; + + private ApiResponse(T data, String message, int status) { + this.data = data; + this.message = message; + this.status = status; + } + + public static ApiResponse success(T data, String message) { + return new ApiResponse<>(data, message, HttpStatus.OK.value()); + } + + public static ApiResponse success(T data, String message, HttpStatus status) { + return new ApiResponse<>(data, message, status.value()); + } +} diff --git a/back/src/main/java/com/back/global/common/PageResponse.java b/back/src/main/java/com/back/global/common/PageResponse.java new file mode 100644 index 0000000..bb05f64 --- /dev/null +++ b/back/src/main/java/com/back/global/common/PageResponse.java @@ -0,0 +1,29 @@ +package com.back.global.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class PageResponse { + private List items; + private int page; + private int size; + private long totalElements; + private int totalPages; + private boolean last; + + public static PageResponse of(Page page) { + return new PageResponse<>( + page.getContent(), + page.getNumber() + 1, // 응답도 1페이지 시작으로 반환, Page 객체는 0페이지부터 시작 + page.getSize(), + page.getTotalElements(), + page.getTotalPages(), + page.isLast() + ); + } +} diff --git a/back/src/main/java/com/back/global/config/WebConfig.java b/back/src/main/java/com/back/global/config/WebConfig.java new file mode 100644 index 0000000..4b2d260 --- /dev/null +++ b/back/src/main/java/com/back/global/config/WebConfig.java @@ -0,0 +1,24 @@ +package com.back.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.web.config.PageableHandlerMethodArgumentResolverCustomizer; + +/** + * 웹 관련 설정을 정의하는 구성 클래스. + */ +@Configuration +public class WebConfig { + + // 스프링의 페이지 요청을 1부터 시작하도록 설정 + // 최대 페이지 크기 및 기본 페이지 설정 + @Bean + public PageableHandlerMethodArgumentResolverCustomizer customizer() { + return pageableResolver -> { + pageableResolver.setOneIndexedParameters(true); // page 요청이 1로 오면 0으로 인식 + pageableResolver.setMaxPageSize(50); + pageableResolver.setFallbackPageable(PageRequest.of(0, 5)); + }; + } +} diff --git a/back/src/test/java/com/back/domain/post/controller/PostControllerTest.java b/back/src/test/java/com/back/domain/post/controller/PostControllerTest.java new file mode 100644 index 0000000..d5ef6dc --- /dev/null +++ b/back/src/test/java/com/back/domain/post/controller/PostControllerTest.java @@ -0,0 +1,285 @@ +package com.back.domain.post.controller; + +import com.back.domain.post.dto.PostRequest; +import com.back.domain.post.entity.Post; +import com.back.domain.post.enums.PostCategory; +import com.back.domain.post.repository.PostRepository; +import com.back.domain.user.entity.Gender; +import com.back.domain.user.entity.Mbti; +import com.back.domain.user.entity.Role; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.global.exception.ErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.stream.IntStream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +class PostControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private PostRepository postRepository; + + @Autowired + private UserRepository userRepository; + + private User testUser; + private User anotherUser; + + @BeforeEach + void setUp() { + testUser = User.builder() + .loginId("testLoginId") + .email("test@example.com") + .password("testPassword") + .beliefs("도전") + .gender(Gender.M) + .role(Role.USER) + .mbti(Mbti.ISFJ) + .birthdayAt(LocalDateTime.of(2000, 3, 1, 0, 0)) + .build(); + userRepository.save(testUser); + + anotherUser = User.builder() + .loginId("anotherLoginId") + .email("another@example.com") + .password("another") + .beliefs("도전") + .gender(Gender.F) + .role(Role.USER) + .mbti(Mbti.ISFJ) + .birthdayAt(LocalDateTime.of(2001, 4, 1, 0, 0)) + .build(); + userRepository.save(anotherUser); + + IntStream.rangeClosed(1, 5).forEach(i -> { + postRepository.save( + Post.builder() + .title("목록 게시글 " + i) + .content("목록 내용 " + i) + .category(PostCategory.CHAT) + .user(testUser) + .build() + ); + }); + } + + @Nested + @DisplayName("게시글 생성") + class CreatePost { + + @Test + @DisplayName("성공 - 정상 요청") + void success() throws Exception { + // given + PostRequest request = new PostRequest("테스트 게시글", "테스트 내용입니다.", PostCategory.CHAT); + + // when + mockMvc.perform(post("/api/v1/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.title").value("테스트 게시글")) + .andExpect(jsonPath("$.data.content").value("테스트 내용입니다.")) + .andExpect(jsonPath("$.data.category").value("CHAT")) + .andExpect(jsonPath("$.message").value("성공적으로 생성되었습니다.")) + .andExpect(jsonPath("$.status").value(200)); + } + + @Test + @DisplayName("실패 - 유효성 검사 실패") + void fail_ValidationError() throws Exception { + // given + PostRequest request = new PostRequest("", "테스트 내용입니다.", PostCategory.CHAT); + + // when & then + mockMvc.perform(post("/api/v1/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.INVALID_INPUT_VALUE.getCode())) + .andExpect(jsonPath("$.message").exists()); + } + } + + @Nested + @DisplayName("게시글 조회") + class GetPost { + + @Test + @DisplayName("성공 - 존재하는 게시글 조회") + void success() throws Exception { + // given + Post savedPost = postRepository.save( + Post.builder() + .title("조회 테스트 게시글") + .content("조회 테스트 내용입니다.") + .category(PostCategory.CHAT) + .user(userRepository.findAll().get(0)) + .build() + ); + + // when & then + mockMvc.perform(get("/api/v1/posts/{postId}", savedPost.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.title").value("조회 테스트 게시글")) + .andExpect(jsonPath("$.data.content").value("조회 테스트 내용입니다.")) + .andExpect(jsonPath("$.data.category").value("CHAT")) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message").value("성공적으로 조회되었습니다.")); + } + + @Test + @DisplayName("실패 - 존재하지 않는 게시글 ID") + void fail_NotFound() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/posts/{postId}", 9999L)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value())) + .andExpect(jsonPath("$.code").value(ErrorCode.POST_NOT_FOUND.getCode())) + .andExpect(jsonPath("$.message").value(ErrorCode.POST_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.path").value("/api/v1/posts/9999")); + } + } + + @Nested + @DisplayName("게시글 목록 조회") + class GetPosts { + + @Test + @DisplayName("성공 - 페이징 파라미터가 없는 경우") + void successWithDefaultParameters() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/posts")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.page").value(1)) + .andExpect(jsonPath("$.data.size").value(5)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message").value("성공적으로 조회되었습니다.")); + } + + @Test + @DisplayName("성공 - page와 size 모두 지정") + void successWithBothParameters() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/posts") + .param("page", "1") + .param("size", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.items.length()").value(5)) + .andExpect(jsonPath("$.data.items[0].title").value("목록 게시글 1")) + .andExpect(jsonPath("$.data.items[1].title").value("목록 게시글 2")) + .andExpect(jsonPath("$.data.page").value(1)) + .andExpect(jsonPath("$.data.size").value(5)) + .andExpect(jsonPath("$.data.totalElements").value(5)) + .andExpect(jsonPath("$.data.totalPages").value(1)) + .andExpect(jsonPath("$.data.last").value(true)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message").value("성공적으로 조회되었습니다.")); + } + + @Test + @DisplayName("성공 - 정렬 파라미터 포함") + void successWithSortParameters() throws Exception { + // when & then + mockMvc.perform(get("/api/v1/posts") + .param("page", "1") + .param("size", "5") + .param("sort", "createdDate,desc") + .param("sort", "title,asc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.page").value(1)) + .andExpect(jsonPath("$.data.size").value(5)) + .andExpect(jsonPath("$.status").value(200)); + } + } + + @Nested + @DisplayName("게시글 수정") + class UpdatePost { + + @Test + @DisplayName("성공 - 본인 게시글 수정") + @Sql(statements = { + "UPDATE users SET id = 1 WHERE login_id = 'testLoginId'" + }) + void success() throws Exception { + // given - ID=1인 사용자의 게시글 생성 + User user1 = userRepository.findById(1L).orElseThrow(); + Post savedPost = postRepository.save( + Post.builder() + .title("수정 전 제목") + .content("수정 전 내용") + .category(PostCategory.CHAT) + .user(user1) + .build() + ); + + PostRequest updateRequest = new PostRequest("수정된 제목", "수정된 내용", PostCategory.CHAT); + + // when & then + mockMvc.perform(put("/api/v1/posts/{postId}", savedPost.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.title").value("수정된 제목")) + .andExpect(jsonPath("$.data.content").value("수정된 내용")) + .andExpect(jsonPath("$.data.category").value("CHAT")); + } + + @Test + @DisplayName("실패 - 다른 사용자 게시글 수정") + @Sql(statements = { + "UPDATE users SET id = 1 WHERE login_id = 'testLoginId'", + "UPDATE users SET id = 2 WHERE login_id = 'anotherLoginId'" + }) + void fail_UnauthorizedUser() throws Exception { + // given - ID=2인 사용자의 게시글 (ID=1 사용자가 수정 시도) + User user2 = userRepository.findById(2L).orElseThrow(); + Post savedPost = postRepository.save( + Post.builder() + .title("다른 사용자 게시글") + .content("다른 사용자 내용") + .category(PostCategory.CHAT) + .user(user2) + .build() + ); + + PostRequest updateRequest = new PostRequest("수정 시도", "수정 시도 내용", PostCategory.CHAT); + + // when & then + mockMvc.perform(put("/api/v1/posts/{postId}", savedPost.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateRequest))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(ErrorCode.UNAUTHORIZED_USER.getCode())); + } + } +} \ No newline at end of file