From 9fb2d0fb364bc716c63509afa3cc86622c710403 Mon Sep 17 00:00:00 2001 From: lvalentine6 Date: Tue, 22 Jul 2025 00:43:08 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=8C=EC=97=90=20?= =?UTF-8?q?=EB=8B=B4=EA=B8=B4=20=EC=9D=B4=EC=95=BC=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/article/ArticleController.java | 24 +++++++++++++ .../controller/article/ArticleResponse.java | 13 +++++++ .../controller/article/ArticlesResponse.java | 8 +++++ .../repository/article/ArticleRepository.java | 12 +++++++ .../eatda/service/article/ArticleService.java | 35 +++++++++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 src/main/java/eatda/controller/article/ArticleController.java create mode 100644 src/main/java/eatda/controller/article/ArticleResponse.java create mode 100644 src/main/java/eatda/controller/article/ArticlesResponse.java create mode 100644 src/main/java/eatda/repository/article/ArticleRepository.java create mode 100644 src/main/java/eatda/service/article/ArticleService.java diff --git a/src/main/java/eatda/controller/article/ArticleController.java b/src/main/java/eatda/controller/article/ArticleController.java new file mode 100644 index 00000000..478f0a04 --- /dev/null +++ b/src/main/java/eatda/controller/article/ArticleController.java @@ -0,0 +1,24 @@ +package eatda.controller.article; + +import eatda.service.article.ArticleService; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +@RequiredArgsConstructor +public class ArticleController { + + private final ArticleService articleService; + + @GetMapping("/api/articles") + public ResponseEntity getArticles(@RequestParam(defaultValue = "3") @Min(1) @Max(50) int size) { + return ResponseEntity.status(HttpStatus.OK) + .body(articleService.getAllArticles(size)); + } +} diff --git a/src/main/java/eatda/controller/article/ArticleResponse.java b/src/main/java/eatda/controller/article/ArticleResponse.java new file mode 100644 index 00000000..bf3edf72 --- /dev/null +++ b/src/main/java/eatda/controller/article/ArticleResponse.java @@ -0,0 +1,13 @@ +package eatda.controller.article; + +import java.time.LocalDateTime; + +public record ArticleResponse( + Long id, + String title, + String subtitle, + String articleUrl, + String imageUrl, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/eatda/controller/article/ArticlesResponse.java b/src/main/java/eatda/controller/article/ArticlesResponse.java new file mode 100644 index 00000000..d1b9d14c --- /dev/null +++ b/src/main/java/eatda/controller/article/ArticlesResponse.java @@ -0,0 +1,8 @@ +package eatda.controller.article; + +import java.util.List; + +public record ArticlesResponse( + List articles +) { +} diff --git a/src/main/java/eatda/repository/article/ArticleRepository.java b/src/main/java/eatda/repository/article/ArticleRepository.java new file mode 100644 index 00000000..95729476 --- /dev/null +++ b/src/main/java/eatda/repository/article/ArticleRepository.java @@ -0,0 +1,12 @@ +package eatda.repository.article; + +import eatda.domain.article.Article; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleRepository extends JpaRepository { + + Page
findAllByOrderByCreatedAtDesc(Pageable pageable); + +} diff --git a/src/main/java/eatda/service/article/ArticleService.java b/src/main/java/eatda/service/article/ArticleService.java new file mode 100644 index 00000000..3e306bff --- /dev/null +++ b/src/main/java/eatda/service/article/ArticleService.java @@ -0,0 +1,35 @@ +package eatda.service.article; + +import eatda.controller.article.ArticleResponse; +import eatda.controller.article.ArticlesResponse; +import eatda.repository.article.ArticleRepository; +import eatda.service.common.ImageService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ArticleService { + + private final ArticleRepository articleRepository; + private final ImageService imageService; + + public ArticlesResponse getAllArticles(int size) { + PageRequest pageRequest = PageRequest.of(0, size); + List articles = articleRepository.findAllByOrderByCreatedAtDesc(pageRequest) + .stream() + .map(article -> new ArticleResponse( + article.getId(), + article.getTitle(), + article.getSubtitle(), + article.getArticleUrl(), + imageService.getPresignedUrl(article.getImageKey()), + article.getCreatedAt() + )) + .toList(); + + return new ArticlesResponse(articles); + } +} From 4dda45ddd6fc4ae4a4f331b4d1aacddb0dd204ba Mon Sep 17 00:00:00 2001 From: lvalentine6 Date: Tue, 22 Jul 2025 02:44:48 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EA=B0=80=EA=B2=8C=EC=97=90=20?= =?UTF-8?q?=EB=8B=B4=EA=B8=B4=20=EC=9D=B4=EC=95=BC=EA=B8=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatda/controller/BaseControllerTest.java | 5 +- .../article/ArticleControllerTest.java | 31 +++++++ .../java/eatda/document/BaseDocumentTest.java | 5 ++ src/test/java/eatda/document/Tag.java | 1 + .../document/article/ArticleDocumentTest.java | 81 +++++++++++++++++++ .../java/eatda/fixture/ArticleGenerator.java | 53 ++++++++++++ .../java/eatda/service/BaseServiceTest.java | 8 ++ .../service/article/ArticleServiceTest.java | 39 +++++++++ 8 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/test/java/eatda/controller/article/ArticleControllerTest.java create mode 100644 src/test/java/eatda/document/article/ArticleDocumentTest.java create mode 100644 src/test/java/eatda/fixture/ArticleGenerator.java create mode 100644 src/test/java/eatda/service/article/ArticleServiceTest.java diff --git a/src/test/java/eatda/controller/BaseControllerTest.java b/src/test/java/eatda/controller/BaseControllerTest.java index 3827893d..3e16acfb 100644 --- a/src/test/java/eatda/controller/BaseControllerTest.java +++ b/src/test/java/eatda/controller/BaseControllerTest.java @@ -12,6 +12,7 @@ import eatda.client.oauth.OauthToken; import eatda.controller.web.jwt.JwtManager; import eatda.domain.member.Member; +import eatda.fixture.ArticleGenerator; import eatda.fixture.CheerGenerator; import eatda.fixture.MemberGenerator; import eatda.fixture.StoreGenerator; @@ -19,7 +20,6 @@ import eatda.repository.store.CheerRepository; import eatda.repository.store.StoreRepository; import eatda.service.common.ImageService; -import eatda.service.common.ImageService; import eatda.service.story.StoryService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; @@ -59,6 +59,9 @@ public class BaseControllerTest { @Autowired protected CheerGenerator cheerGenerator; + @Autowired + protected ArticleGenerator articleGenerator; + @Autowired protected MemberRepository memberRepository; diff --git a/src/test/java/eatda/controller/article/ArticleControllerTest.java b/src/test/java/eatda/controller/article/ArticleControllerTest.java new file mode 100644 index 00000000..1224e866 --- /dev/null +++ b/src/test/java/eatda/controller/article/ArticleControllerTest.java @@ -0,0 +1,31 @@ +package eatda.controller.article; + +import static org.assertj.core.api.Assertions.assertThat; + +import eatda.controller.BaseControllerTest; +import eatda.domain.article.Article; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class ArticleControllerTest extends BaseControllerTest { + + @Nested + class GetArticles { + + @Test + void 가게의_담긴_이야기_목록을_조회할_수_있다() { + Article article1 = articleGenerator.generate("국밥의 모든 것"); + Article article2 = articleGenerator.generate("순대국의 진실"); + + ArticlesResponse response = given() + .queryParam("size", 3) + .when() + .get("/api/articles") + .then().statusCode(200) + .extract().as(ArticlesResponse.class); + + assertThat(response.articles()).hasSize(2); + assertThat(response.articles().getFirst().title()).isEqualTo("순대국의 진실"); + } + } +} diff --git a/src/test/java/eatda/document/BaseDocumentTest.java b/src/test/java/eatda/document/BaseDocumentTest.java index 3bb276a1..c35b4114 100644 --- a/src/test/java/eatda/document/BaseDocumentTest.java +++ b/src/test/java/eatda/document/BaseDocumentTest.java @@ -7,6 +7,7 @@ import eatda.controller.web.jwt.JwtManager; import eatda.exception.BusinessErrorCode; import eatda.exception.EtcErrorCode; +import eatda.service.article.ArticleService; import eatda.service.auth.AuthService; import eatda.service.common.ImageService; import eatda.service.member.MemberService; @@ -57,6 +58,10 @@ public abstract class BaseDocumentTest { @MockitoBean protected CheerService cheerService; + + @MockitoBean + protected ArticleService articleService; + @MockitoBean protected JwtManager jwtManager; diff --git a/src/test/java/eatda/document/Tag.java b/src/test/java/eatda/document/Tag.java index 61a9c130..dbce12b5 100644 --- a/src/test/java/eatda/document/Tag.java +++ b/src/test/java/eatda/document/Tag.java @@ -7,6 +7,7 @@ public enum Tag { STORE_API("Store API"), STORY_API("Story API"), CHEER_API("Cheer API"), + ARTICLE_API("Article API"), ; private final String displayName; diff --git a/src/test/java/eatda/document/article/ArticleDocumentTest.java b/src/test/java/eatda/document/article/ArticleDocumentTest.java new file mode 100644 index 00000000..a0c652f6 --- /dev/null +++ b/src/test/java/eatda/document/article/ArticleDocumentTest.java @@ -0,0 +1,81 @@ +package eatda.document.article; + +import static org.mockito.Mockito.doReturn; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import eatda.controller.article.ArticleResponse; +import eatda.controller.article.ArticlesResponse; +import eatda.document.BaseDocumentTest; +import eatda.document.RestDocsRequest; +import eatda.document.RestDocsResponse; +import eatda.document.Tag; +import io.restassured.response.Response; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.restassured.RestDocumentationFilter; + +public class ArticleDocumentTest extends BaseDocumentTest { + + @Nested + class GetArticles { + + RestDocsRequest requestDocument = request() + .tag(Tag.ARTICLE_API) + .summary("가게의 담긴 이야기") + .description("게시글을 최신순으로 페이지네이션하여 조회합니다.") + .queryParameter(parameterWithName("size").description("페이지당 조회할 아티클 개수 (default = 3)")); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("articles").description("게시글 응답 리스트"), + fieldWithPath("articles[].id").description("게시글 ID"), + fieldWithPath("articles[].title").description("게시글 제목"), + fieldWithPath("articles[].subtitle").description("게시글 소제목"), + fieldWithPath("articles[].articleUrl").description("게시글 링크 URL"), + fieldWithPath("articles[].imageUrl").description("게시글 이미지 URL"), + fieldWithPath("articles[].createdAt").description("작성 시각") + ); + + @Test + void 가게의_담긴_이야기_목록_조회_성공() { + ArticlesResponse mockResponse = new ArticlesResponse(List.of( + new ArticleResponse( + 1L, + "국밥의 모든 것", + "뜨끈한 국물의 세계", + "https://eatda.com/article/1", + "https://s3.bucket.com/article/1.jpg", + LocalDateTime.now() + ), + new ArticleResponse( + 2L, + "순대국의 진실", + "돼지부속의 미학", + "https://eatda.com/article/2", + "https://s3.bucket.com/article/2.jpg", + LocalDateTime.now() + ) + )); + + doReturn(mockResponse) + .when(articleService) + .getAllArticles(3); + + RestDocumentationFilter document = document("article/get-articles", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + Response response = given(document) + .queryParam("size", 3) + .when() + .get("/api/articles"); + + response.then() + .statusCode(200); + } + } +} diff --git a/src/test/java/eatda/fixture/ArticleGenerator.java b/src/test/java/eatda/fixture/ArticleGenerator.java new file mode 100644 index 00000000..cb5f1908 --- /dev/null +++ b/src/test/java/eatda/fixture/ArticleGenerator.java @@ -0,0 +1,53 @@ +package eatda.fixture; + +import eatda.controller.article.ArticleResponse; +import eatda.domain.article.Article; +import eatda.repository.article.ArticleRepository; +import org.springframework.stereotype.Component; + +@Component +public class ArticleGenerator { + + private static final String DEFAULT_TITLE = "기본 제목"; + private static final String DEFAULT_SUBTITLE = "기본 소제목"; + private static final String DEFAULT_URL = "https://eatda.com/article/default"; + private static final String DEFAULT_IMAGE_KEY = "article/default-image.jpg"; + + private final ArticleRepository articleRepository; + + public ArticleGenerator(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Article generate() { + return generate(DEFAULT_TITLE); + } + + public Article generate(String title) { + return generate(title, DEFAULT_SUBTITLE); + } + + public Article generate(String title, String subtitle) { + return generate(title, subtitle, DEFAULT_URL); + } + + public Article generate(String title, String subtitle, String articleUrl) { + return generate(title, subtitle, articleUrl, DEFAULT_IMAGE_KEY); + } + + public Article generate(String title, String subtitle, String articleUrl, String imageKey) { + Article article = new Article(title, subtitle, articleUrl, imageKey); + return articleRepository.save(article); + } + + public ArticleResponse toResponse(Article article) { + return new ArticleResponse( + article.getId(), + article.getTitle(), + article.getSubtitle(), + article.getArticleUrl(), + "https://s3.bucket.com/" + article.getImageKey(), + article.getCreatedAt() + ); + } +} diff --git a/src/test/java/eatda/service/BaseServiceTest.java b/src/test/java/eatda/service/BaseServiceTest.java index 631d25a9..9d0f2777 100644 --- a/src/test/java/eatda/service/BaseServiceTest.java +++ b/src/test/java/eatda/service/BaseServiceTest.java @@ -7,9 +7,11 @@ import eatda.DatabaseCleaner; import eatda.client.map.MapClient; import eatda.client.oauth.OauthClient; +import eatda.fixture.ArticleGenerator; import eatda.fixture.CheerGenerator; import eatda.fixture.MemberGenerator; import eatda.fixture.StoreGenerator; +import eatda.repository.article.ArticleRepository; import eatda.repository.member.MemberRepository; import eatda.repository.store.CheerRepository; import eatda.repository.store.StoreRepository; @@ -45,6 +47,9 @@ public abstract class BaseServiceTest { @Autowired protected CheerGenerator cheerGenerator; + @Autowired + protected ArticleGenerator articleGenerator; + @Autowired protected MemberRepository memberRepository; @@ -54,6 +59,9 @@ public abstract class BaseServiceTest { @Autowired protected CheerRepository cheerRepository; + @Autowired + protected ArticleRepository articleRepository; + @BeforeEach void mockingImageService() { doReturn(MOCKED_IMAGE_URL).when(imageService).getPresignedUrl(anyString()); diff --git a/src/test/java/eatda/service/article/ArticleServiceTest.java b/src/test/java/eatda/service/article/ArticleServiceTest.java new file mode 100644 index 00000000..10b7f29b --- /dev/null +++ b/src/test/java/eatda/service/article/ArticleServiceTest.java @@ -0,0 +1,39 @@ +package eatda.service.article; + +import static org.assertj.core.api.Assertions.assertThat; + +import eatda.controller.article.ArticleResponse; +import eatda.service.BaseServiceTest; +import java.util.List; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class ArticleServiceTest extends BaseServiceTest { + + @Autowired + private ArticleService articleService; + + @Nested + class GetAllArticles { + + @Test + void 가게의_담긴_이야기를_최신순으로_조회할_수_있다() { + LongStream.rangeClosed(1, 5) + .forEach(i -> articleGenerator.generate("아티클 제목 " + i)); + + var response = articleService.getAllArticles(3); + + assertThat(response.articles()).hasSize(3); + List titles = response.articles().stream() + .map(ArticleResponse::title) + .toList(); + assertThat(titles).containsExactly( + "아티클 제목 5", + "아티클 제목 4", + "아티클 제목 3" + ); + } + } +} From c735ab7b2166f93e7f9eb0749347b732a3d8bfa1 Mon Sep 17 00:00:00 2001 From: lvalentine6 Date: Wed, 23 Jul 2025 04:45:25 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatda/controller/article/ArticleResponse.java | 6 +----- .../java/eatda/service/article/ArticleService.java | 4 +--- .../eatda/document/article/ArticleDocumentTest.java | 13 +++---------- src/test/java/eatda/fixture/ArticleGenerator.java | 4 +--- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/main/java/eatda/controller/article/ArticleResponse.java b/src/main/java/eatda/controller/article/ArticleResponse.java index bf3edf72..9a998fdf 100644 --- a/src/main/java/eatda/controller/article/ArticleResponse.java +++ b/src/main/java/eatda/controller/article/ArticleResponse.java @@ -1,13 +1,9 @@ package eatda.controller.article; -import java.time.LocalDateTime; - public record ArticleResponse( - Long id, String title, String subtitle, String articleUrl, - String imageUrl, - LocalDateTime createdAt + String imageUrl ) { } diff --git a/src/main/java/eatda/service/article/ArticleService.java b/src/main/java/eatda/service/article/ArticleService.java index 3e306bff..29f117af 100644 --- a/src/main/java/eatda/service/article/ArticleService.java +++ b/src/main/java/eatda/service/article/ArticleService.java @@ -21,12 +21,10 @@ public ArticlesResponse getAllArticles(int size) { List articles = articleRepository.findAllByOrderByCreatedAtDesc(pageRequest) .stream() .map(article -> new ArticleResponse( - article.getId(), article.getTitle(), article.getSubtitle(), article.getArticleUrl(), - imageService.getPresignedUrl(article.getImageKey()), - article.getCreatedAt() + imageService.getPresignedUrl(article.getImageKey()) )) .toList(); diff --git a/src/test/java/eatda/document/article/ArticleDocumentTest.java b/src/test/java/eatda/document/article/ArticleDocumentTest.java index a0c652f6..510a1fe7 100644 --- a/src/test/java/eatda/document/article/ArticleDocumentTest.java +++ b/src/test/java/eatda/document/article/ArticleDocumentTest.java @@ -11,7 +11,6 @@ import eatda.document.RestDocsResponse; import eatda.document.Tag; import io.restassured.response.Response; -import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -31,32 +30,26 @@ class GetArticles { RestDocsResponse responseDocument = response() .responseBodyField( fieldWithPath("articles").description("게시글 응답 리스트"), - fieldWithPath("articles[].id").description("게시글 ID"), fieldWithPath("articles[].title").description("게시글 제목"), fieldWithPath("articles[].subtitle").description("게시글 소제목"), fieldWithPath("articles[].articleUrl").description("게시글 링크 URL"), - fieldWithPath("articles[].imageUrl").description("게시글 이미지 URL"), - fieldWithPath("articles[].createdAt").description("작성 시각") + fieldWithPath("articles[].imageUrl").description("게시글 이미지 URL") ); @Test void 가게의_담긴_이야기_목록_조회_성공() { ArticlesResponse mockResponse = new ArticlesResponse(List.of( new ArticleResponse( - 1L, "국밥의 모든 것", "뜨끈한 국물의 세계", "https://eatda.com/article/1", - "https://s3.bucket.com/article/1.jpg", - LocalDateTime.now() + "https://s3.bucket.com/article/1.jpg" ), new ArticleResponse( - 2L, "순대국의 진실", "돼지부속의 미학", "https://eatda.com/article/2", - "https://s3.bucket.com/article/2.jpg", - LocalDateTime.now() + "https://s3.bucket.com/article/2.jpg" ) )); diff --git a/src/test/java/eatda/fixture/ArticleGenerator.java b/src/test/java/eatda/fixture/ArticleGenerator.java index cb5f1908..538133f7 100644 --- a/src/test/java/eatda/fixture/ArticleGenerator.java +++ b/src/test/java/eatda/fixture/ArticleGenerator.java @@ -42,12 +42,10 @@ public Article generate(String title, String subtitle, String articleUrl, String public ArticleResponse toResponse(Article article) { return new ArticleResponse( - article.getId(), article.getTitle(), article.getSubtitle(), article.getArticleUrl(), - "https://s3.bucket.com/" + article.getImageKey(), - article.getCreatedAt() + "https://s3.bucket.com/" + article.getImageKey() ); } } From 495a8563e64c0dbc1049a75f85c8d2a9fd09e206 Mon Sep 17 00:00:00 2001 From: lvalentine6 Date: Wed, 23 Jul 2025 04:47:57 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20=EA=B2=80=EC=A6=9D=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatda/service/article/ArticleServiceTest.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/test/java/eatda/service/article/ArticleServiceTest.java b/src/test/java/eatda/service/article/ArticleServiceTest.java index 10b7f29b..b138cc81 100644 --- a/src/test/java/eatda/service/article/ArticleServiceTest.java +++ b/src/test/java/eatda/service/article/ArticleServiceTest.java @@ -4,7 +4,6 @@ import eatda.controller.article.ArticleResponse; import eatda.service.BaseServiceTest; -import java.util.List; import java.util.stream.LongStream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -25,15 +24,9 @@ class GetAllArticles { var response = articleService.getAllArticles(3); - assertThat(response.articles()).hasSize(3); - List titles = response.articles().stream() - .map(ArticleResponse::title) - .toList(); - assertThat(titles).containsExactly( - "아티클 제목 5", - "아티클 제목 4", - "아티클 제목 3" - ); + assertThat(response.articles()).hasSize(3) + .extracting(ArticleResponse::title) + .containsExactly("아티클 제목 5", "아티클 제목 4", "아티클 제목 3"); } } }