Skip to content

Commit 7c1e18c

Browse files
committed
feat: 세모피드 CRUD API 구현
1 parent 4cde2e8 commit 7c1e18c

6 files changed

Lines changed: 191 additions & 1 deletion

File tree

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public enum ErrorStatus implements BaseStatus {
116116
TRACKING_COURSE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "TRK_400_2", "자유 기록이 아니면 코스 ID는 필수입니다."),
117117
TRACKING_PHOTO_DUPLICATE(HttpStatus.CONFLICT, "TRK_409_3", "해당 마일스톤에 이미 업로드된 사진이 있습니다."),
118118
TRACKING_PHOTO_SESSION_INACTIVE(HttpStatus.CONFLICT, "TRK_409_4", "활성 상태가 아닌 세션에는 사진을 업로드할 수 없습니다."),
119+
TRACKING_PHOTO_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_2", "트래킹 사진을 찾을 수 없습니다."),
119120
COURSE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_3", "코스를 찾을 수 없습니다."),
120121

121122
/**
@@ -133,7 +134,13 @@ public enum ErrorStatus implements BaseStatus {
133134
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "CMT_404_1", "댓글을 찾을 수 없습니다."),
134135
COMMENT_DELETED(HttpStatus.NOT_FOUND, "CMT_404_2", "삭제된 댓글입니다."),
135136
COMMENT_FORBIDDEN(HttpStatus.FORBIDDEN, "CMT_403_1", "본인의 댓글만 처리할 수 있습니다."),
136-
COMMENT_PARENT_POST_MISMATCH(HttpStatus.BAD_REQUEST, "CMT_400_1", "부모 댓글이 같은 게시글의 댓글이 아닙니다.");
137+
COMMENT_PARENT_POST_MISMATCH(HttpStatus.BAD_REQUEST, "CMT_400_1", "부모 댓글이 같은 게시글의 댓글이 아닙니다."),
138+
139+
/**
140+
* SemoFeed (세모피드)
141+
*/
142+
SEMOFEED_NOT_FOUND(HttpStatus.NOT_FOUND, "SF_404_1", "세모피드를 찾을 수 없습니다."),
143+
SEMOFEED_FORBIDDEN(HttpStatus.FORBIDDEN, "SF_403_1", "본인의 세모피드만 처리할 수 있습니다.");
137144

138145
private final HttpStatus httpStatus;
139146
private final String code;

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ public enum SuccessStatus implements BaseStatus {
6666
TRACKING_PHOTO_UPLOAD_SUCCESS(HttpStatus.CREATED, "TRK_201_2", "트래킹 사진이 저장되었습니다."),
6767
TRACKING_PHOTO_LIST_SUCCESS(HttpStatus.OK, "TRK_200_8", "트래킹 사진 목록 조회에 성공했습니다."),
6868

69+
/**
70+
* SemoFeed
71+
*/
72+
SEMOFEED_CREATE_SUCCESS(HttpStatus.CREATED, "SF_201_1", "세모피드가 저장되었습니다."),
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+
6978
/**
7079
* Image
7180
*/
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.semosan.api.domain.semofeed.controller;
2+
3+
import com.semosan.api.common.response.ApiResponse;
4+
import com.semosan.api.common.response.PageResponse;
5+
import com.semosan.api.common.status.SuccessStatus;
6+
import com.semosan.api.domain.semofeed.controller.docs.SemoFeedControllerDocs;
7+
import com.semosan.api.domain.semofeed.dto.SemoFeedResponse;
8+
import com.semosan.api.domain.semofeed.service.SemoFeedService;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.data.domain.Pageable;
11+
import org.springframework.data.web.PageableDefault;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
14+
import org.springframework.web.bind.annotation.*;
15+
16+
import java.util.List;
17+
18+
@RestController
19+
@RequestMapping("/api/semofeed")
20+
@RequiredArgsConstructor
21+
public class SemoFeedController implements SemoFeedControllerDocs {
22+
23+
private final SemoFeedService semoFeedService;
24+
25+
@PostMapping
26+
@Override
27+
public ResponseEntity<ApiResponse<SemoFeedResponse>> create(
28+
@AuthenticationPrincipal Long userId,
29+
@RequestBody String imageUrl
30+
) {
31+
SemoFeedResponse response = semoFeedService.create(userId, imageUrl);
32+
return ApiResponse.success(SuccessStatus.SEMOFEED_CREATE_SUCCESS, response);
33+
}
34+
35+
@GetMapping
36+
@Override
37+
public ResponseEntity<ApiResponse<PageResponse<SemoFeedResponse>>> listPublic(
38+
@PageableDefault(size = 100) Pageable pageable
39+
) {
40+
return ApiResponse.success(
41+
SuccessStatus.SEMOFEED_LIST_SUCCESS,
42+
PageResponse.from(semoFeedService.listPublic(pageable))
43+
);
44+
}
45+
46+
@GetMapping("/me")
47+
@Override
48+
public ResponseEntity<ApiResponse<List<SemoFeedResponse>>> listMine(
49+
@AuthenticationPrincipal Long userId
50+
) {
51+
return ApiResponse.success(SuccessStatus.SEMOFEED_MY_LIST_SUCCESS, semoFeedService.listMine(userId));
52+
}
53+
54+
@PatchMapping("/{semoFeedId}/public")
55+
@Override
56+
public ResponseEntity<ApiResponse<Boolean>> togglePublic(
57+
@AuthenticationPrincipal Long userId,
58+
@PathVariable Long semoFeedId
59+
) {
60+
boolean isPublic = semoFeedService.togglePublic(userId, semoFeedId);
61+
return ApiResponse.success(SuccessStatus.SEMOFEED_TOGGLE_PUBLIC_SUCCESS, isPublic);
62+
}
63+
64+
@DeleteMapping("/{semoFeedId}")
65+
@Override
66+
public ResponseEntity<ApiResponse<Void>> delete(
67+
@AuthenticationPrincipal Long userId,
68+
@PathVariable Long semoFeedId
69+
) {
70+
semoFeedService.delete(userId, semoFeedId);
71+
return ApiResponse.success(SuccessStatus.SEMOFEED_DELETE_SUCCESS, null);
72+
}
73+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.semosan.api.domain.semofeed.dto;
2+
3+
import com.semosan.api.domain.semofeed.entity.SemoFeed;
4+
5+
public record SemoFeedResponse(
6+
Long id,
7+
String imageUrl,
8+
boolean isPublic
9+
) {
10+
public static SemoFeedResponse from(SemoFeed semoFeed) {
11+
return new SemoFeedResponse(
12+
semoFeed.getId(),
13+
semoFeed.getImageUrl(),
14+
semoFeed.isPublic()
15+
);
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.semosan.api.domain.semofeed.repository;
2+
3+
import com.semosan.api.domain.semofeed.entity.SemoFeed;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.Pageable;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
9+
import java.util.List;
10+
11+
public interface SemoFeedRepository extends JpaRepository<SemoFeed, Long> {
12+
13+
@Query("SELECT s FROM SemoFeed s WHERE s.isPublic = true ORDER BY s.createdAt DESC")
14+
Page<SemoFeed> findPublic(Pageable pageable);
15+
16+
@Query("SELECT s FROM SemoFeed s WHERE s.user.id = :userId ORDER BY s.createdAt DESC")
17+
List<SemoFeed> findByUserId(Long userId);
18+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.semosan.api.domain.semofeed.service;
2+
3+
import com.semosan.api.common.exception.GeneralException;
4+
import com.semosan.api.common.status.ErrorStatus;
5+
import com.semosan.api.domain.semofeed.dto.SemoFeedResponse;
6+
import com.semosan.api.domain.semofeed.entity.SemoFeed;
7+
import com.semosan.api.domain.semofeed.repository.SemoFeedRepository;
8+
import com.semosan.api.domain.user.entity.User;
9+
import com.semosan.api.domain.user.repository.UserRepository;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.data.domain.Page;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
import java.util.List;
17+
18+
@Service
19+
@RequiredArgsConstructor
20+
@Transactional(readOnly = true)
21+
public class SemoFeedService {
22+
23+
private final SemoFeedRepository semoFeedRepository;
24+
private final UserRepository userRepository;
25+
26+
@Transactional
27+
public SemoFeedResponse create(Long userId, String imageUrl) {
28+
User user = userRepository.findById(userId)
29+
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));
30+
SemoFeed semoFeed = SemoFeed.create(user, imageUrl);
31+
return SemoFeedResponse.from(semoFeedRepository.save(semoFeed));
32+
}
33+
34+
public Page<SemoFeedResponse> listPublic(Pageable pageable) {
35+
return semoFeedRepository.findPublic(pageable)
36+
.map(SemoFeedResponse::from);
37+
}
38+
39+
public List<SemoFeedResponse> listMine(Long userId) {
40+
return semoFeedRepository.findByUserId(userId).stream()
41+
.map(SemoFeedResponse::from)
42+
.toList();
43+
}
44+
45+
@Transactional
46+
public boolean togglePublic(Long userId, Long semoFeedId) {
47+
SemoFeed semoFeed = findOwned(userId, semoFeedId);
48+
semoFeed.togglePublic();
49+
return semoFeed.isPublic();
50+
}
51+
52+
@Transactional
53+
public void delete(Long userId, Long semoFeedId) {
54+
SemoFeed semoFeed = findOwned(userId, semoFeedId);
55+
semoFeedRepository.delete(semoFeed);
56+
}
57+
58+
private SemoFeed findOwned(Long userId, Long semoFeedId) {
59+
SemoFeed semoFeed = semoFeedRepository.findById(semoFeedId)
60+
.orElseThrow(() -> new GeneralException(ErrorStatus.SEMOFEED_NOT_FOUND));
61+
if (!semoFeed.isOwnedBy(userId)) {
62+
throw new GeneralException(ErrorStatus.SEMOFEED_FORBIDDEN);
63+
}
64+
return semoFeed;
65+
}
66+
}

0 commit comments

Comments
 (0)