Skip to content

Commit 6785eac

Browse files
authored
Merge branch 'develop' into feat/#49-live-activity
2 parents a096dca + 366a9e2 commit 6785eac

44 files changed

Lines changed: 2257 additions & 43 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

k8s/kustomization.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ resources:
1414
- minio/service.yaml
1515
images:
1616
- name: ghcr.io/semosan/semosan_be
17-
newTag: b976ab6dd6006e94df6af125add740f1b4ed681f
17+
newTag: 315ebde74cf55f400ce5f23811db838ddf8dab6b

src/main/java/com/semosan/api/common/jwt/JwtFilter.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,16 @@ public class JwtFilter extends OncePerRequestFilter {
3232
private final JwtService jwtService;
3333
private final ObjectMapper objectMapper;
3434

35-
// SecurityConfig의 permitAll 경로와 동일하게 맞춰줍니다
35+
// SecurityConfig의 permitAll 경로와 동일하게 맞춰줍니다.
36+
// WebSocket 핸드셰이크(/ws/tracking)는 HTTP JWT 필터 우회 — STOMP CONNECT 프레임의
37+
// StompAuthChannelInterceptor 가 별도로 인증한다.
3638
private static final List<PathPatternRequestMatcher> EXCLUDED_PATHS =
37-
Stream.of(SecurityConfig.SWAGGER_URIS, SecurityConfig.OAUTH_URIS, SecurityConfig.AUTH_URIS)
39+
Stream.of(
40+
SecurityConfig.SWAGGER_URIS,
41+
SecurityConfig.OAUTH_URIS,
42+
SecurityConfig.AUTH_URIS,
43+
SecurityConfig.WEBSOCKET_URIS
44+
)
3845
.flatMap(Arrays::stream)
3946
.map(PathPatternRequestMatcher.withDefaults()::matcher)
4047
.toList();

src/main/java/com/semosan/api/common/status/ErrorStatus.java

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,22 @@ public enum ErrorStatus implements BaseStatus {
4949
/**
5050
* User
5151
*/
52-
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_1", "사용자를 찾을 수 없습니다."),
53-
ONBOARDING_ALREADY_COMPLETED(HttpStatus.CONFLICT, "USER_409_1", "이미 온보딩을 완료한 사용자입니다."),
5452
PREFERRED_DIFFICULTY_REQUIRED(HttpStatus.BAD_REQUEST, "USER_400_1", "숙련자는 선호 난이도를 선택해야 합니다."),
5553
PREFERRED_DIFFICULTY_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "USER_400_2", "선호 난이도는 숙련자만 선택할 수 있습니다."),
5654
PROFILE_UPDATE_FIELD_REQUIRED(HttpStatus.BAD_REQUEST, "USER_400_3", "수정할 프로필 정보가 없습니다."),
57-
NOTIFICATION_SETTING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_2", "알림 설정을 찾을 수 없습니다."),
5855
INVALID_NICKNAME(HttpStatus.BAD_REQUEST, "USER_400_4", "사용할 수 없는 닉네임입니다."),
59-
DUPLICATED_NICKNAME(HttpStatus.CONFLICT, "USER_409_2", "이미 사용 중인 닉네임입니다."),
6056
UNDER_AGE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "USER_400_5", "만 14세 미만은 가입할 수 없습니다."),
6157
INVALID_NICKNAME_FORMAT(HttpStatus.BAD_REQUEST, "USER_400_6", "닉네임 형식이 올바르지 않습니다."),
6258
NICKNAME_RESERVED(HttpStatus.BAD_REQUEST, "USER_400_7", "사용할 수 없는 사칭 표현이 포함된 닉네임입니다."),
6359
NICKNAME_BLOCKED_WORD(HttpStatus.BAD_REQUEST, "USER_400_8", "금칙어가 포함된 닉네임입니다."),
6460
EXERCISE_DETAIL_REQUIRED(HttpStatus.BAD_REQUEST, "USER_400_9", "운동 빈도와 운동 시간을 입력해야 합니다."),
6561
EXERCISE_DETAIL_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "USER_400_10", "운동 안함 선택 시 운동 빈도와 운동 시간을 입력할 수 없습니다."),
66-
ONBOARDING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_3", "온보딩 정보를 찾을 수 없습니다."),
6762
ONBOARDING_NOT_COMPLETED(HttpStatus.FORBIDDEN, "USER_403_1", "온보딩을 완료해야 이용할 수 있습니다."),
63+
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_1", "사용자를 찾을 수 없습니다."),
64+
NOTIFICATION_SETTING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_2", "알림 설정을 찾을 수 없습니다."),
65+
ONBOARDING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_3", "온보딩 정보를 찾을 수 없습니다."),
66+
ONBOARDING_ALREADY_COMPLETED(HttpStatus.CONFLICT, "USER_409_1", "이미 온보딩을 완료한 사용자입니다."),
67+
DUPLICATED_NICKNAME(HttpStatus.CONFLICT, "USER_409_2", "이미 사용 중인 닉네임입니다."),
6868

6969
/**
7070
* Notification
@@ -74,17 +74,18 @@ public enum ErrorStatus implements BaseStatus {
7474
/**
7575
* Apple OAuth
7676
*/
77-
APPLE_PUBLIC_KEY_REQUEST_FAILED(HttpStatus.BAD_GATEWAY, "APPLE_502_1", "애플 공개키 조회에 실패했습니다."),
7877
APPLE_PUBLIC_KEY_NOT_FOUND(HttpStatus.UNAUTHORIZED, "APPLE_401_1", "유효한 애플 공개키를 찾을 수 없습니다."),
7978
APPLE_IDENTITY_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "APPLE_401_2", "애플 identity token이 유효하지 않습니다."),
79+
APPLE_PUBLIC_KEY_REQUEST_FAILED(HttpStatus.BAD_GATEWAY, "APPLE_502_1", "애플 공개키 조회에 실패했습니다."),
8080

8181
/**
8282
* Mountain
8383
*/
8484
MOUNTAIN_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_1", "산을 찾을 수 없습니다."),
8585
MOUNTAIN_LIKE_ALREADY_EXISTS(HttpStatus.CONFLICT, "MTN_409_1", "이미 좋아요한 산입니다."),
86-
MOUNTAIN_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_2", "좋아요한 산이 아닙니다."),
8786
MOUNTAIN_BBOX_PARTIAL(HttpStatus.BAD_REQUEST, "MTN_400_1", "BBox 좌표는 4개(swLat, swLng, neLat, neLng) 모두 보내거나 모두 비워주세요."),
87+
MOUNTAIN_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_2", "좋아요한 산이 아닙니다."),
88+
COURSE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_3", "코스를 찾을 수 없습니다."),
8889

8990
/**
9091
* Image
@@ -102,37 +103,45 @@ public enum ErrorStatus implements BaseStatus {
102103
/**
103104
* Hiking (등산 기록)
104105
*/
105-
HIKING_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "HIKE_404_1", "등산 기록을 찾을 수 없습니다."),
106106
HIKING_RECORD_FORBIDDEN(HttpStatus.FORBIDDEN, "HIKE_403_1", "본인이 참여한 등산 기록만 공유할 수 있습니다."),
107+
HIKING_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "HIKE_404_1", "등산 기록을 찾을 수 없습니다."),
107108

108109
/**
109110
* Tracking (트래킹 세션)
110111
*/
111-
TRACKING_SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_1", "트래킹 세션을 찾을 수 없습니다."),
112+
TRACKING_COURSE_MOUNTAIN_MISMATCH(HttpStatus.BAD_REQUEST, "TRK_400_1", "선택한 코스가 해당 산의 코스가 아닙니다."),
113+
TRACKING_COURSE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "TRK_400_2", "자유 기록이 아니면 코스 ID는 필수입니다."),
112114
TRACKING_SESSION_FORBIDDEN(HttpStatus.FORBIDDEN, "TRK_403_1", "본인의 트래킹 세션만 조작할 수 있습니다."),
115+
TRACKING_SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_1", "트래킹 세션을 찾을 수 없습니다."),
116+
TRACKING_PHOTO_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_2", "트래킹 사진을 찾을 수 없습니다."),
113117
TRACKING_SESSION_ALREADY_IN_PROGRESS(HttpStatus.CONFLICT, "TRK_409_1", "이미 진행 중인 트래킹 세션이 있습니다."),
114118
TRACKING_SESSION_INVALID_STATE(HttpStatus.CONFLICT, "TRK_409_2", "현재 상태에서는 수행할 수 없는 작업입니다."),
115-
TRACKING_COURSE_MOUNTAIN_MISMATCH(HttpStatus.BAD_REQUEST, "TRK_400_1", "선택한 코스가 해당 산의 코스가 아닙니다."),
116-
TRACKING_COURSE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "TRK_400_2", "자유 기록이 아니면 코스 ID는 필수입니다."),
119+
TRACKING_PHOTO_DUPLICATE(HttpStatus.CONFLICT, "TRK_409_3", "해당 마일스톤에 이미 업로드된 사진이 있습니다."),
120+
TRACKING_PHOTO_SESSION_INACTIVE(HttpStatus.CONFLICT, "TRK_409_4", "활성 상태가 아닌 세션에는 사진을 업로드할 수 없습니다."),
117121
TRACKING_COURSE_POLYLINE_REQUIRED(HttpStatus.UNPROCESSABLE_ENTITY, "TRK_422_1", "코스 경로 좌표가 등록되지 않았습니다."),
118-
COURSE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_3", "코스를 찾을 수 없습니다."),
119122

120123
/**
121124
* Post (게시글 공통)
122125
*/
123-
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_404_1", "게시글을 찾을 수 없습니다."),
124-
POST_DELETED(HttpStatus.NOT_FOUND, "POST_404_2", "삭제된 게시글입니다."),
125-
POST_FORBIDDEN(HttpStatus.FORBIDDEN, "POST_403_1", "본인의 게시글만 처리할 수 있습니다."),
126126
POST_CONTENT_REQUIRED(HttpStatus.BAD_REQUEST, "POST_400_1", "본문은 비어있을 수 없습니다."),
127127
POST_IMAGE_INDEX_INVALID(HttpStatus.BAD_REQUEST, "POST_400_2", "대표 이미지 인덱스가 잘못되었습니다."),
128+
POST_FORBIDDEN(HttpStatus.FORBIDDEN, "POST_403_1", "본인의 게시글만 처리할 수 있습니다."),
129+
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_404_1", "게시글을 찾을 수 없습니다."),
130+
POST_DELETED(HttpStatus.NOT_FOUND, "POST_404_2", "삭제된 게시글입니다."),
128131

129132
/**
130133
* Comment (댓글/대댓글)
131134
*/
135+
COMMENT_PARENT_POST_MISMATCH(HttpStatus.BAD_REQUEST, "CMT_400_1", "부모 댓글이 같은 게시글의 댓글이 아닙니다."),
136+
COMMENT_FORBIDDEN(HttpStatus.FORBIDDEN, "CMT_403_1", "본인의 댓글만 처리할 수 있습니다."),
132137
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "CMT_404_1", "댓글을 찾을 수 없습니다."),
133138
COMMENT_DELETED(HttpStatus.NOT_FOUND, "CMT_404_2", "삭제된 댓글입니다."),
134-
COMMENT_FORBIDDEN(HttpStatus.FORBIDDEN, "CMT_403_1", "본인의 댓글만 처리할 수 있습니다."),
135-
COMMENT_PARENT_POST_MISMATCH(HttpStatus.BAD_REQUEST, "CMT_400_1", "부모 댓글이 같은 게시글의 댓글이 아닙니다.");
139+
140+
/**
141+
* SemoFeed (세모피드)
142+
*/
143+
SEMOFEED_FORBIDDEN(HttpStatus.FORBIDDEN, "SF_403_1", "본인의 세모피드만 처리할 수 있습니다."),
144+
SEMOFEED_NOT_FOUND(HttpStatus.NOT_FOUND, "SF_404_1", "세모피드를 찾을 수 없습니다.");
136145

137146
private final HttpStatus httpStatus;
138147
private final String code;

src/main/java/com/semosan/api/common/status/SuccessStatus.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public enum SuccessStatus implements BaseStatus {
4242
LIKED_MOUNTAIN_LIST_SUCCESS(HttpStatus.OK, "MTN_200_6", "좋아요한 산 목록 조회에 성공했습니다."),
4343
MOUNTAIN_MAP_SUCCESS(HttpStatus.OK, "MTN_200_7", "지도 영역 내 산 조회에 성공했습니다."),
4444
MOUNTAIN_RECOMMENDATION_SUCCESS(HttpStatus.OK, "MTN_200_8", "레벨 맞춤 산 추천 조회에 성공했습니다."),
45+
COURSE_DETAIL_SUCCESS(HttpStatus.OK, "MTN_200_9", "코스 상세 정보 조회에 성공했습니다."),
4546

4647
/**
4748
* Hiking Record
@@ -55,14 +56,25 @@ public enum SuccessStatus implements BaseStatus {
5556
* Tracking — 진입 화면 (#45) & 세션 (#18)
5657
*/
5758
TRACKING_NEAREST_MOUNTAIN_SUCCESS(HttpStatus.OK, "TRK_200_1", "현재 위치 기준 가까운 산과 코스 조회에 성공했습니다."),
58-
TRACKING_SESSION_CREATE_SUCCESS(HttpStatus.CREATED, "TRK_201_1", "트래킹 세션이 시작되었습니다."),
5959
TRACKING_SESSION_GET_ACTIVE_SUCCESS(HttpStatus.OK, "TRK_200_2", "현재 진행 중인 트래킹 세션 조회에 성공했습니다."),
6060
TRACKING_SESSION_GET_SUCCESS(HttpStatus.OK, "TRK_200_3", "트래킹 세션 상세 조회에 성공했습니다."),
6161
TRACKING_SESSION_PAUSE_SUCCESS(HttpStatus.OK, "TRK_200_4", "트래킹 세션을 일시정지했습니다."),
6262
TRACKING_SESSION_RESUME_SUCCESS(HttpStatus.OK, "TRK_200_5", "트래킹 세션을 재개했습니다."),
6363
TRACKING_SESSION_COMPLETE_SUCCESS(HttpStatus.OK, "TRK_200_6", "트래킹 세션을 종료했습니다."),
6464
TRACKING_SESSION_ABANDON_SUCCESS(HttpStatus.OK, "TRK_200_7", "트래킹 세션을 포기 처리했습니다."),
6565
TRACKING_LIVE_ACTIVITY_COURSE_SUCCESS(HttpStatus.OK, "TRK_200_8", "라이브 액티비티용 코스 정보 조회에 성공했습니다."),
66+
TRACKING_PHOTO_LIST_SUCCESS(HttpStatus.OK, "TRK_200_9", "트래킹 사진 목록 조회에 성공했습니다."),
67+
TRACKING_SESSION_CREATE_SUCCESS(HttpStatus.CREATED, "TRK_201_1", "트래킹 세션이 시작되었습니다."),
68+
TRACKING_PHOTO_UPLOAD_SUCCESS(HttpStatus.CREATED, "TRK_201_2", "트래킹 사진이 저장되었습니다."),
69+
70+
/**
71+
* SemoFeed
72+
*/
73+
SEMOFEED_LIST_SUCCESS(HttpStatus.OK, "SF_200_1", "세모피드 목록 조회에 성공했습니다."),
74+
SEMOFEED_MY_LIST_SUCCESS(HttpStatus.OK, "SF_200_2", "내 세모피드 목록 조회에 성공했습니다."),
75+
SEMOFEED_TOGGLE_PUBLIC_SUCCESS(HttpStatus.OK, "SF_200_3", "세모피드 공개 상태가 변경되었습니다."),
76+
SEMOFEED_DELETE_SUCCESS(HttpStatus.OK, "SF_200_4", "세모피드가 삭제되었습니다."),
77+
SEMOFEED_CREATE_SUCCESS(HttpStatus.CREATED, "SF_201_1", "세모피드가 저장되었습니다."),
6678

6779
/**
6880
* Image
@@ -88,11 +100,11 @@ public enum SuccessStatus implements BaseStatus {
88100
/**
89101
* Comment (댓글/대댓글)
90102
*/
91-
COMMENT_CREATE_SUCCESS(HttpStatus.CREATED, "CMT_201_1", "댓글이 작성되었습니다."),
92-
COMMENT_REPLY_SUCCESS(HttpStatus.CREATED, "CMT_201_2", "대댓글이 작성되었습니다."),
93103
COMMENT_LIST_SUCCESS(HttpStatus.OK, "CMT_200_1", "댓글 목록 조회에 성공했습니다."),
94104
COMMENT_REPLY_LIST_SUCCESS(HttpStatus.OK, "CMT_200_2", "대댓글 목록 조회에 성공했습니다."),
95105
COMMENT_DELETE_SUCCESS(HttpStatus.OK, "CMT_200_3", "댓글이 삭제되었습니다."),
106+
COMMENT_CREATE_SUCCESS(HttpStatus.CREATED, "CMT_201_1", "댓글이 작성되었습니다."),
107+
COMMENT_REPLY_SUCCESS(HttpStatus.CREATED, "CMT_201_2", "대댓글이 작성되었습니다."),
96108

97109
/**
98110
* Post Like (좋아요)
@@ -103,11 +115,12 @@ public enum SuccessStatus implements BaseStatus {
103115
/**
104116
* Free Post (자유게시판 게시글)
105117
*/
106-
FREE_POST_CREATE_SUCCESS(HttpStatus.CREATED, "FPOST_201_1", "자유게시판 게시글이 작성되었습니다."),
107118
FREE_POST_LIST_SUCCESS(HttpStatus.OK, "FPOST_200_1", "자유게시판 게시글 목록 조회에 성공했습니다."),
108119
FREE_POST_MY_LIST_SUCCESS(HttpStatus.OK, "FPOST_200_2", "내 자유게시판 게시글 목록 조회에 성공했습니다."),
109120
FREE_POST_DETAIL_SUCCESS(HttpStatus.OK, "FPOST_200_3", "자유게시판 게시글 상세 조회에 성공했습니다."),
110-
FREE_POST_DELETE_SUCCESS(HttpStatus.OK, "FPOST_200_4", "자유게시판 게시글이 삭제되었습니다.");
121+
FREE_POST_DELETE_SUCCESS(HttpStatus.OK, "FPOST_200_4", "자유게시판 게시글이 삭제되었습니다."),
122+
FREE_POST_SEARCH_SUCCESS(HttpStatus.OK, "FPOST_200_5", "자유게시판 게시글 검색에 성공했습니다."),
123+
FREE_POST_CREATE_SUCCESS(HttpStatus.CREATED, "FPOST_201_1", "자유게시판 게시글이 작성되었습니다.");
111124

112125
private final HttpStatus httpStatus;
113126
private final String code;

src/main/java/com/semosan/api/domain/community/post/controller/FreePostController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ public ResponseEntity<ApiResponse<PageResponse<FreePostListResponse>>> getList(
4949
);
5050
}
5151

52+
@GetMapping("/search")
53+
public ResponseEntity<ApiResponse<PageResponse<FreePostListResponse>>> search(
54+
@RequestParam String keyword,
55+
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
56+
) {
57+
return ApiResponse.success(
58+
SuccessStatus.FREE_POST_SEARCH_SUCCESS,
59+
PageResponse.from(freePostService.search(keyword, pageable))
60+
);
61+
}
62+
5263
@GetMapping("/me")
5364
public ResponseEntity<ApiResponse<PageResponse<FreePostListResponse>>> getMyList(
5465
@AuthenticationPrincipal Long userId,

src/main/java/com/semosan/api/domain/community/post/controller/docs/FreePostControllerDocs.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ ResponseEntity<ApiResponse<FreePostDetailResponse>> create(
3535
})
3636
ResponseEntity<ApiResponse<PageResponse<FreePostListResponse>>> getList(Pageable pageable);
3737

38+
@Operation(summary = "자유게시판 검색", description = "제목/내용 기반 키워드 검색. 최신순 페이징.")
39+
@ApiResponses({
40+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "검색 성공")
41+
})
42+
ResponseEntity<ApiResponse<PageResponse<FreePostListResponse>>> search(
43+
String keyword,
44+
Pageable pageable
45+
);
46+
3847
@Operation(summary = "내 자유게시판 목록", description = "내가 쓴 자유게시판 게시글 목록.")
3948
@ApiResponses({
4049
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공")

src/main/java/com/semosan/api/domain/community/post/dto/AuthorResponse.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,19 @@
44

55
public record AuthorResponse(
66
Long id,
7-
String name,
7+
String nickname,
88
String profileUrl,
99
boolean isDeleted
1010
) {
1111

12-
//TODO: 앱 정책에 따라서 탈퇴한 회원 이름 어떻게 보내줄지 결정되면 통일하기
13-
private static final String DELETED_USER_NAME = "탈퇴한 사용자";
14-
private static final String UNKNOWN_USER_NAME = "이름없음";
12+
private static final String DELETED_USER_NICKNAME = "탈퇴한 사용자";
13+
private static final String UNKNOWN_USER_NICKNAME = "이름없음";
1514

1615
public static AuthorResponse from(User user) {
1716
if (user.isDeleted()) {
18-
return new AuthorResponse(user.getId(), DELETED_USER_NAME, null, true);
17+
return new AuthorResponse(user.getId(), DELETED_USER_NICKNAME, null, true);
1918
}
20-
String name = user.getName() != null ? user.getName() : UNKNOWN_USER_NAME;
21-
return new AuthorResponse(user.getId(), name, user.getProfileUrl(), false);
19+
String nickname = user.getNickname() != null ? user.getNickname() : UNKNOWN_USER_NICKNAME;
20+
return new AuthorResponse(user.getId(), nickname, user.getProfileUrl(), false);
2221
}
2322
}

src/main/java/com/semosan/api/domain/community/post/dto/FreePostListResponse.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public record FreePostListResponse(
99
AuthorResponse author,
1010
String title,
1111
String contentPreview,
12-
String representativeImageUrl,
13-
Integer viewCount,
12+
String thumbnailUrl,
13+
Integer extraImageCount,
1414
Long likeCount,
1515
Long commentCount,
1616
LocalDateTime createdAt
@@ -19,7 +19,8 @@ public record FreePostListResponse(
1919

2020
public static FreePostListResponse from(
2121
FreePost post,
22-
String representativeImageUrl,
22+
String thumbnailUrl,
23+
Integer extraImageCount,
2324
long likeCount,
2425
long commentCount
2526
) {
@@ -28,8 +29,8 @@ public static FreePostListResponse from(
2829
AuthorResponse.from(post.getAuthor()),
2930
post.getTitle(),
3031
preview(post.getContent()),
31-
representativeImageUrl,
32-
post.getViewCount(),
32+
thumbnailUrl,
33+
extraImageCount != null && extraImageCount > 1 ? extraImageCount - 1 : null,
3334
likeCount,
3435
commentCount,
3536
post.getCreatedAt()

src/main/java/com/semosan/api/domain/community/post/repository/FreePostRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@
55
import org.springframework.data.domain.Page;
66
import org.springframework.data.domain.Pageable;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
810

911
public interface FreePostRepository extends JpaRepository<FreePost, Long> {
1012

1113
Page<FreePost> findAllByDeletedFalse(Pageable pageable);
1214

1315
Page<FreePost> findByAuthorAndDeletedFalse(User author, Pageable pageable);
16+
17+
@Query("SELECT fp FROM FreePost fp " +
18+
"WHERE fp.deleted = false " +
19+
"AND (LOWER(fp.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " +
20+
"OR LOWER(fp.content) LIKE LOWER(CONCAT('%', :keyword, '%'))) " +
21+
"ORDER BY fp.createdAt DESC")
22+
Page<FreePost> searchByKeyword(@Param("keyword") String keyword, Pageable pageable);
1423
}

src/main/java/com/semosan/api/domain/community/post/repository/PostImageRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ public interface PostImageRepository extends JpaRepository<PostImage, Long> {
1414

1515
@Query("SELECT pi FROM PostImage pi WHERE pi.post.id IN :postIds AND pi.main = true")
1616
List<PostImage> findMainImagesByPostIds(@Param("postIds") List<Long> postIds);
17+
18+
@Query("SELECT pi.post.id, COUNT(pi) FROM PostImage pi WHERE pi.post.id IN :postIds GROUP BY pi.post.id")
19+
List<Object[]> countByPostIdsGrouped(@Param("postIds") List<Long> postIds);
1720
}

0 commit comments

Comments
 (0)