From 67eccf6f1ed1171fdae12ebc4b29e07830820977 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 14:50:09 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/db/migration/V33__ announcement.sql | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 backend/core/src/main/resources/db/migration/V33__ announcement.sql diff --git a/backend/core/src/main/resources/db/migration/V33__ announcement.sql b/backend/core/src/main/resources/db/migration/V33__ announcement.sql new file mode 100644 index 000000000..fabd7fe37 --- /dev/null +++ b/backend/core/src/main/resources/db/migration/V33__ announcement.sql @@ -0,0 +1,8 @@ +create table announcement ( + announcement_id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255), + content TEXT, + version VARCHAR(30), + created_at timestamp NULL, + updated_at timestamp NULL +) \ No newline at end of file From e4932140fda9e289ff9f84cf99b441b0abf67af1 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 14:50:18 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../announcement/entity/Announcement.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/entity/Announcement.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/entity/Announcement.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/entity/Announcement.java new file mode 100644 index 000000000..e61925423 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/entity/Announcement.java @@ -0,0 +1,33 @@ +package site.timecapsulearchive.core.domain.announcement.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.timecapsulearchive.core.global.entity.BaseEntity; + +@Entity +@Table(name = "announcement") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Announcement extends BaseEntity { + + @Id + @Column(name = "announcement_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title") + private String title; + + @Column(name = "content") + private String content; + + @Column(name = "version") + private String version; +} From a096ab8d4652836c2a25fd72d9ecae8e0c5e99ff Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 14:50:32 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=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 --- .../data/dto/AnnouncementDto.java | 16 ++++++++++ .../data/response/AnnouncementResponse.java | 27 ++++++++++++++++ .../repository/AnnouncementRepository.java | 32 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/dto/AnnouncementDto.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementResponse.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/repository/AnnouncementRepository.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/dto/AnnouncementDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/dto/AnnouncementDto.java new file mode 100644 index 000000000..a9f44e243 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/dto/AnnouncementDto.java @@ -0,0 +1,16 @@ +package site.timecapsulearchive.core.domain.announcement.data.dto; + +import java.time.ZonedDateTime; +import site.timecapsulearchive.core.domain.announcement.data.response.AnnouncementResponse; + +public record AnnouncementDto( + String title, + String content, + String version, + ZonedDateTime createdAt +) { + + public AnnouncementResponse toResponse() { + return new AnnouncementResponse(title, content, version, createdAt); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementResponse.java new file mode 100644 index 000000000..b359ab911 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementResponse.java @@ -0,0 +1,27 @@ +package site.timecapsulearchive.core.domain.announcement.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; + +@Schema(description = "공지사항 응답") +public record AnnouncementResponse( + @Schema(description = "제목") + String title, + + @Schema(description = "내용") + String content, + + @Schema(description = "공지사항 버전") + String version, + + @Schema(description = "공지사항 생성일") + ZonedDateTime createdAt +) { + + public AnnouncementResponse { + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/repository/AnnouncementRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/repository/AnnouncementRepository.java new file mode 100644 index 000000000..dc5b9182a --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/repository/AnnouncementRepository.java @@ -0,0 +1,32 @@ +package site.timecapsulearchive.core.domain.announcement.repository; + +import static site.timecapsulearchive.core.domain.announcement.entity.QAnnouncement.announcement; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import site.timecapsulearchive.core.domain.announcement.data.dto.AnnouncementDto; + +@Repository +@RequiredArgsConstructor +public class AnnouncementRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public List findAll() { + return jpaQueryFactory + .select( + Projections.constructor( + AnnouncementDto.class, + announcement.title, + announcement.content, + announcement.version, + announcement.createdAt + ) + ) + .from(announcement) + .fetch(); + } +} From 20f05eeb5f368c57e0cd9250b9d5c5a01e9e71b6 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 14:51:35 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AnnouncementService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java new file mode 100644 index 000000000..10f31b177 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java @@ -0,0 +1,18 @@ +package site.timecapsulearchive.core.domain.announcement.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import site.timecapsulearchive.core.domain.announcement.data.dto.AnnouncementDto; +import site.timecapsulearchive.core.domain.announcement.repository.AnnouncementRepository; + +@Service +@RequiredArgsConstructor +public class AnnouncementService { + + private final AnnouncementRepository announcementRepository; + + public List findAll() { + return announcementRepository.findAll(); + } +} From ae7f3a17d1fa3348ab55390d87c3722ffa1a5ad9 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 14:51:46 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../announcement/api/AnnouncementApi.java | 23 +++++++++++++ .../api/AnnouncementApiController.java | 34 +++++++++++++++++++ .../data/response/AnnouncementsResponse.java | 20 +++++++++++ 3 files changed, 77 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementsResponse.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java new file mode 100644 index 000000000..c52ab275c --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java @@ -0,0 +1,23 @@ +package site.timecapsulearchive.core.domain.announcement.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.ResponseEntity; +import site.timecapsulearchive.core.domain.announcement.data.response.AnnouncementsResponse; +import site.timecapsulearchive.core.global.common.response.ApiSpec; + +public interface AnnouncementApi { + @Operation( + summary = "공지사항 페이지", + description = "모든 공지사항을 가져온다. 최신 공지가 리스트의 맨 끝에 위치한다.", + tags = {"oauth2"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "ok" + ) + }) + ResponseEntity> getAnnouncements(); +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java new file mode 100644 index 000000000..df965e4f2 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java @@ -0,0 +1,34 @@ +package site.timecapsulearchive.core.domain.announcement.api; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import site.timecapsulearchive.core.domain.announcement.data.response.AnnouncementsResponse; +import site.timecapsulearchive.core.domain.announcement.data.dto.AnnouncementDto; +import site.timecapsulearchive.core.domain.announcement.service.AnnouncementService; +import site.timecapsulearchive.core.global.common.response.ApiSpec; +import site.timecapsulearchive.core.global.common.response.SuccessCode; + +@RestController +@RequestMapping +@RequiredArgsConstructor +public class AnnouncementApiController implements AnnouncementApi { + + private final AnnouncementService announcementService; + + @GetMapping + @Override + public ResponseEntity> getAnnouncements() { + List announcements = announcementService.findAll(); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + AnnouncementsResponse.createOf(announcements) + ) + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementsResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementsResponse.java new file mode 100644 index 000000000..aca4a54e2 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/data/response/AnnouncementsResponse.java @@ -0,0 +1,20 @@ +package site.timecapsulearchive.core.domain.announcement.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import site.timecapsulearchive.core.domain.announcement.data.dto.AnnouncementDto; + +@Schema(description = "공지사항 목록 응답") +public record AnnouncementsResponse( + @Schema(description = "공지사항 목록") + List announcements +) { + + public static AnnouncementsResponse createOf(List announcementDtos) { + return new AnnouncementsResponse( + announcementDtos.stream() + .map(AnnouncementDto::toResponse) + .toList() + ); + } +} From 61194e9be623fb4e0afe56d29ab724cd2a60b9ac Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 14:52:22 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix=20:=20gitignore=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 51e94bb56..0fd891e42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea -backend/data \ No newline at end of file +backend/data + +.env \ No newline at end of file From 65a79b8ce401eba244f5dad2a96acaed0cee7568 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 20:40:50 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat=20:=20url=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/announcement/api/AnnouncementApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java index df965e4f2..5f2f2e136 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApiController.java @@ -13,7 +13,7 @@ import site.timecapsulearchive.core.global.common.response.SuccessCode; @RestController -@RequestMapping +@RequestMapping("/announcement") @RequiredArgsConstructor public class AnnouncementApiController implements AnnouncementApi { From 07fc4e6952c1be58f26060b8a52b493b7e93bb3a Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 20:47:21 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix=20:=20=EB=AC=B8=EC=84=9C=ED=99=94=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/announcement/api/AnnouncementApi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java index c52ab275c..1787aa40b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/api/AnnouncementApi.java @@ -10,8 +10,8 @@ public interface AnnouncementApi { @Operation( summary = "공지사항 페이지", - description = "모든 공지사항을 가져온다. 최신 공지가 리스트의 맨 끝에 위치한다.", - tags = {"oauth2"} + description = "모든 공지사항을 가져온다. 최신 공지가 리스트의 맨 처음에 위치한다.", + tags = {"announcement"} ) @ApiResponses(value = { @ApiResponse( From 61cff96110b815fb5b708a98682899eca5cf6888 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 20:47:38 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat=20:=20=EC=B5=9C=EC=8B=A0=EC=88=9C?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/announcement/service/AnnouncementService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java index 10f31b177..bd6da8190 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementService.java @@ -1,5 +1,6 @@ package site.timecapsulearchive.core.domain.announcement.service; +import java.util.Comparator; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -13,6 +14,10 @@ public class AnnouncementService { private final AnnouncementRepository announcementRepository; public List findAll() { - return announcementRepository.findAll(); + List announcements = announcementRepository.findAll(); + + announcements.sort(Comparator.comparing(AnnouncementDto::createdAt).reversed()); + + return announcements; } } From 787faf33b7239b840bf5b61993ad7fd3b84d6c3a Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 25 Jul 2024 20:59:39 +0900 Subject: [PATCH 10/10] =?UTF-8?q?test=20:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/AnnouncementTestFixture.java | 22 +++++++++++++ .../service/AnnouncementServiceTest.java | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/AnnouncementTestFixture.java create mode 100644 backend/core/src/test/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementServiceTest.java diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/AnnouncementTestFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/AnnouncementTestFixture.java new file mode 100644 index 000000000..84877598a --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/AnnouncementTestFixture.java @@ -0,0 +1,22 @@ +package site.timecapsulearchive.core.common.fixture.domain; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import site.timecapsulearchive.core.domain.announcement.data.dto.AnnouncementDto; + +public class AnnouncementTestFixture { + + public static List announcementDtos(int size) { + List result = new ArrayList<>(); + ZonedDateTime now = ZonedDateTime.now(); + for (int index = 0; index < size; index++) { + result.add( + new AnnouncementDto("title" + index, "content" + index, String.valueOf(index), + now.plusSeconds(index)) + ); + } + + return result; + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementServiceTest.java new file mode 100644 index 000000000..090578723 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/announcement/service/AnnouncementServiceTest.java @@ -0,0 +1,33 @@ +package site.timecapsulearchive.core.domain.announcement.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.util.Comparator; +import java.util.List; +import org.junit.jupiter.api.Test; +import site.timecapsulearchive.core.common.fixture.domain.AnnouncementTestFixture; +import site.timecapsulearchive.core.domain.announcement.data.dto.AnnouncementDto; +import site.timecapsulearchive.core.domain.announcement.repository.AnnouncementRepository; + +class AnnouncementServiceTest { + + private final AnnouncementRepository announcementRepository = mock( + AnnouncementRepository.class); + private final AnnouncementService announcementService = new AnnouncementService( + announcementRepository); + + @Test + void 공지사항을_조회하면_가장_최근에_생성된_공지사항이_최상단에_위치한다() { + //given + given(announcementRepository.findAll()).willReturn(AnnouncementTestFixture.announcementDtos(10)); + + //when + List announcements = announcementService.findAll(); + + //then + assertThat(announcements) + .isSortedAccordingTo(Comparator.comparing(AnnouncementDto::createdAt).reversed()); + } +}