Skip to content

Commit 343e29c

Browse files
authored
Merge pull request #66 from Apple-Square/develop
게시글 검색 & 게시글 단일 조회 추가
2 parents 21e6375 + 0e80197 commit 343e29c

13 files changed

+368
-3
lines changed

src/main/java/applesquare/moment/file/service/impl/CloudinaryServiceImpl.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
import com.cloudinary.utils.ObjectUtils;
88
import lombok.RequiredArgsConstructor;
99
import lombok.extern.log4j.Log4j2;
10+
import org.springframework.http.HttpEntity;
11+
import org.springframework.http.HttpHeaders;
12+
import org.springframework.http.HttpMethod;
13+
import org.springframework.http.ResponseEntity;
1014
import org.springframework.stereotype.Service;
1115
import org.springframework.web.client.RestTemplate;
1216
import org.springframework.web.multipart.MultipartFile;
1317

1418
import java.io.IOException;
19+
import java.net.URLDecoder;
1520
import java.nio.file.Files;
1621
import java.nio.file.Path;
1722
import java.util.Arrays;
@@ -24,6 +29,7 @@
2429
@Service
2530
@RequiredArgsConstructor
2631
public class CloudinaryServiceImpl implements CloudinaryService {
32+
private final RestTemplate restTemplate;
2733
private final Cloudinary cloudinary;
2834

2935
@Override
@@ -59,9 +65,20 @@ public byte[] convertVideoToThumbnail(MultipartFile video, String thumbFilename,
5965
// 변환된 파일 URL 추출
6066
List<Map<String, Object>> eagerResults = (List<Map<String, Object>>) uploadResult.get("eager");
6167
String transformedUrl = (String) eagerResults.get(0).get("secure_url");
68+
String decodedURL = URLDecoder.decode(transformedUrl, "UTF-8");
6269

6370
// 파일 다운로드 및 저장
64-
byte[] fileBytes = new RestTemplate().getForObject(transformedUrl, byte[].class);
71+
HttpHeaders headers = new HttpHeaders();
72+
headers.set("Accept", "video/mp4"); // 동영상 데이터 수락
73+
74+
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
75+
ResponseEntity<byte[]> response = restTemplate.exchange(
76+
decodedURL, // 요청 URL
77+
HttpMethod.GET, // HTTP 메서드
78+
requestEntity, // 요청 헤더 및 엔터티
79+
byte[].class // 응답 타입 (바이너리 데이터)
80+
);
81+
byte[] fileBytes=response.getBody();
6582

6683
// 썸네일 영상의 비트 배열 반환
6784
return fileBytes;

src/main/java/applesquare/moment/post/controller/MomentSearchController.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ public class MomentSearchController {
2222
private final MomentSearchService momentSearchService;
2323

2424

25+
/**
26+
* 키워드 기반 모먼트 검색 API
27+
* @param type 게시물 조회 타입
28+
* @param size 페이지 크기
29+
* @param cursor 페이지 커서
30+
* @param keyword 검색 키워드
31+
* @return (status) 200,
32+
* (body) 검색 성공 메세지,
33+
* 모먼트 목록
34+
*/
2535
@GetMapping("/search")
2636
public ResponseEntity<Map<String, Object>> searchDetail(@RequestParam(value = "type", required = false, defaultValue = "DETAIL") PostReadType type,
2737
@RequestParam(value = "size", required = false, defaultValue = "10") int size,
@@ -52,4 +62,45 @@ public ResponseEntity<Map<String, Object>> searchDetail(@RequestParam(value = "t
5262

5363
return ResponseEntity.status(HttpStatus.OK).body(responseMap.getMap());
5464
}
65+
66+
/**
67+
* 태그 기반 모먼트 검색 API
68+
* @param type 게시물 조회 타입
69+
* @param size 페이지 크기
70+
* @param cursor 페이지 커서
71+
* @param keyword 검색 키워드
72+
* @return (status) 200,
73+
* (body) 검색 성공 메세지,
74+
* 모먼트 목록
75+
*/
76+
@GetMapping("/tags/search")
77+
public ResponseEntity<Map<String, Object>> searchDetailByTag(@RequestParam(value = "type", required = false, defaultValue = "DETAIL") PostReadType type,
78+
@RequestParam(value = "size", required = false, defaultValue = "10") int size,
79+
@RequestParam(value = "cursor", required = false) String cursor,
80+
@RequestParam(value = "keyword", required = false) String keyword){
81+
// 페이지 요청 설정
82+
PageRequestDTO pageRequestDTO=PageRequestDTO.builder()
83+
.size(size)
84+
.cursor(cursor)
85+
.keyword(keyword)
86+
.build();
87+
88+
// 응답 객체 구성
89+
ResponseMap responseMap=new ResponseMap();
90+
91+
switch(type){
92+
case DETAIL:
93+
// 모먼트 목록 조회
94+
PageResponseDTO<MomentDetailReadAllResponseDTO> pageResponseDTO=momentSearchService.searchDetailByTag(pageRequestDTO);
95+
responseMap.put("content", pageResponseDTO.getContent());
96+
responseMap.put("hasNext", pageResponseDTO.isHasNext());
97+
break;
98+
case THUMBNAIL:
99+
throw new IllegalArgumentException("지원하지 않는 조회 타입입니다. (type="+type+")");
100+
}
101+
102+
responseMap.put("message", "모먼트 검색에 성공했습니다.");
103+
104+
return ResponseEntity.status(HttpStatus.OK).body(responseMap.getMap());
105+
}
55106
}

src/main/java/applesquare/moment/post/controller/PostReadController.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ public class PostReadController {
2020
private final PostReadService postReadService;
2121

2222

23+
/**
24+
* 특정 게시물 조회 API
25+
* @param postId 게시물 ID
26+
* @return 게시물 상세 정보
27+
*/
28+
@GetMapping("/posts/{postId}")
29+
public ResponseEntity<Map<String, Object>> readOne(@PathVariable Long postId){
30+
PostDetailReadAllResponseDTO postDetailDTO=postReadService.read(postId);
31+
32+
// 응답 객체 구성
33+
ResponseMap responseMap=new ResponseMap();
34+
responseMap.put("post", postDetailDTO);
35+
responseMap.put("message", "게시글 조회에 성공했습니다.");
36+
37+
return ResponseEntity.status(HttpStatus.OK).body(responseMap.getMap());
38+
}
39+
2340
/**
2441
* 게시물 목록 조회 API
2542
* @param type 게시물 조회 타입

src/main/java/applesquare/moment/post/controller/PostSearchController.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class PostSearchController {
2424

2525

2626
/**
27-
* 게시물 검색 API
27+
* 키워드 기반 게시물 검색 API
2828
* @param type 게시물 조회 타입
2929
* @param size 페이지 크기
3030
* @param cursor 페이지 커서
@@ -67,4 +67,49 @@ public ResponseEntity<Map<String, Object>> searchDetail(@RequestParam(value = "t
6767

6868
return ResponseEntity.status(HttpStatus.OK).body(responseMap.getMap());
6969
}
70+
71+
/**
72+
* 태그 기반 게시물 검색 API
73+
* @param type 게시물 조회 타입
74+
* @param size 페이지 크기
75+
* @param cursor 페이지 커서
76+
* @param keyword 검색 키워드
77+
* @return (status) 200,
78+
* (body) 검색 성공 메세지,
79+
* 게시물 목록
80+
*/
81+
@GetMapping("/tags/search")
82+
public ResponseEntity<Map<String, Object>> searchDetailByTag(@RequestParam(value = "type", required = false, defaultValue = "DETAIL") PostReadType type,
83+
@RequestParam(value = "size", required = false, defaultValue = "10") int size,
84+
@RequestParam(value = "cursor", required = false) String cursor,
85+
@RequestParam(value = "keyword", required = false) String keyword){
86+
// 페이지 요청 설정
87+
PageRequestDTO pageRequestDTO=PageRequestDTO.builder()
88+
.size(size)
89+
.cursor(cursor)
90+
.keyword(keyword)
91+
.build();
92+
93+
// 응답 객체 구성
94+
ResponseMap responseMap=new ResponseMap();
95+
96+
switch (type){
97+
case DETAIL:
98+
// 게시글 세부사항 목록 검색
99+
PageResponseDTO<PostDetailReadAllResponseDTO> detailPageResponseDTO=postSearchService.searchDetailByTag(pageRequestDTO);
100+
responseMap.put("content", detailPageResponseDTO.getContent());
101+
responseMap.put("hasNext", detailPageResponseDTO.isHasNext());
102+
break;
103+
case THUMBNAIL:
104+
// 게시글 썸네일 목록 검색
105+
PageResponseDTO<PostThumbnailReadAllResponseDTO> thumbPageResponseDTO=postSearchService.searchThumbnailByTag(pageRequestDTO);
106+
responseMap.put("content", thumbPageResponseDTO.getContent());
107+
responseMap.put("hasNext", thumbPageResponseDTO.isHasNext());
108+
break;
109+
}
110+
111+
responseMap.put("message", "게시글 검색에 성공했습니다.");
112+
113+
return ResponseEntity.status(HttpStatus.OK).body(responseMap.getMap());
114+
}
70115
}

src/main/java/applesquare/moment/post/repository/CustomPostRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44

55
public interface CustomPostRepository {
66
List<Long> searchPostIdsByKeyword(String keyword, Long cursor, int size);
7+
List<Long> searchPostIdsByTag(String keyword, Long cursor, int size);
78
List<Long> searchMomentIdsByKeyword(String keyword, Long cursor, int size);
9+
List<Long> searchMomentIdsByTag(String keyword, Long cursor, int size);
810
}

src/main/java/applesquare/moment/post/repository/PostRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
public interface PostRepository extends JpaRepository<Post, Long>, CustomPostRepository {
1414
long countByWriterId(String userId);
1515

16+
1617
// 게시물 목록 조회 (커서 페이징)
1718
@EntityGraph(attributePaths = {"files"})
1819
@Query("SELECT p " +

src/main/java/applesquare/moment/post/repository/impl/CustomPostRepositoryImpl.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,67 @@ public List<Long> searchMomentIdsByKeyword(String keyword, Long cursor, int size
8686
.limit(size)
8787
.fetch();
8888
}
89+
90+
/**
91+
* 태그로 게시물 검색
92+
* - 검색 속성 : 태그명
93+
* - 정렬 기준 : 최신순
94+
*
95+
* @param keyword 검색 키워드
96+
* @param cursor 페이지 커서
97+
* @param size 페이지 크기
98+
* @return 검색 조건에 부합하는 Post ID 목록
99+
*/
100+
@Override
101+
public List<Long> searchPostIdsByTag(String keyword, Long cursor, int size){
102+
// 검색 조건에 부합하는 게시물 ID 조회
103+
BooleanExpression cursorCondition=null;
104+
if(cursor!=null){
105+
cursorCondition=post.id.lt(cursor);
106+
}
107+
BooleanExpression keywordCondition=tag.name.contains(keyword);
108+
109+
return queryFactory
110+
.selectDistinct(post.id)
111+
.from(post)
112+
.leftJoin(post.tags, tag)
113+
.leftJoin(post.writer, userInfo)
114+
.where(keywordCondition.and(cursorCondition))
115+
.orderBy(post.id.desc())
116+
.limit(size)
117+
.fetch();
118+
}
119+
120+
/**
121+
* 태그로 모먼트 검색
122+
* - 검색 속성 : 태그명
123+
* - 정렬 기준 : 최신순
124+
*
125+
* @param keyword 검색 키워드
126+
* @param cursor 페이지 커서
127+
* @param size 페이지 크기
128+
* @return 검색 조건에 부합하는 모먼트 목록
129+
*/
130+
@Override
131+
public List<Long> searchMomentIdsByTag(String keyword, Long cursor, int size){
132+
// 검색 조건에 부합하는 게시물 ID 조회
133+
BooleanExpression cursorCondition=null;
134+
if(cursor!=null){
135+
cursorCondition=post.id.lt(cursor);
136+
}
137+
138+
return queryFactory
139+
.selectDistinct(post.id)
140+
.from(post)
141+
.leftJoin(post.tags, tag)
142+
.leftJoin(post.files, storageFile)
143+
.leftJoin(post.writer, userInfo)
144+
.where(storageFile.contentType.startsWith("video")
145+
.and(tag.name.contains(keyword))
146+
.and(cursorCondition)
147+
)
148+
.orderBy(post.id.desc())
149+
.limit(size)
150+
.fetch();
151+
}
89152
}

src/main/java/applesquare/moment/post/service/MomentSearchService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
public interface MomentSearchService {
88
// 게시글 검색 > 모먼츠 탭
99
PageResponseDTO<MomentDetailReadAllResponseDTO> searchDetail(PageRequestDTO pageRequestDTO);
10+
PageResponseDTO<MomentDetailReadAllResponseDTO> searchDetailByTag(PageRequestDTO pageRequestDTO);
1011
}

src/main/java/applesquare/moment/post/service/PostReadService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import applesquare.moment.post.dto.PostThumbnailReadAllResponseDTO;
77

88
public interface PostReadService {
9+
PostDetailReadAllResponseDTO read(Long postId);
10+
911
// 추천 게시물 목록 조회
1012
PageResponseDTO<PostDetailReadAllResponseDTO> readDetailAll(PageRequestDTO pageRequestDTO);
1113
PageResponseDTO<PostThumbnailReadAllResponseDTO> readThumbnailAll(PageRequestDTO pageRequestDTO);

src/main/java/applesquare/moment/post/service/PostSearchService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
public interface PostSearchService {
99
// 게시물 검색 > 1열 피드
1010
PageResponseDTO<PostDetailReadAllResponseDTO> searchDetail(PageRequestDTO pageRequestDTO);
11+
PageResponseDTO<PostDetailReadAllResponseDTO> searchDetailByTag(PageRequestDTO pageRequestDTO);
12+
1113
// 게시물 검색 > 3열 피드
1214
PageResponseDTO<PostThumbnailReadAllResponseDTO> searchThumbnail(PageRequestDTO pageRequestDTO);
15+
PageResponseDTO<PostThumbnailReadAllResponseDTO> searchThumbnailByTag(PageRequestDTO pageRequestDTO);
1316
}

0 commit comments

Comments
 (0)