Skip to content

Commit c43e0d5

Browse files
authored
[feat] 펀딩 찜 기능 구현 (#357)
* [feat] ã펀딩 찜 기능 구현 * work * work
1 parent f5cdfa5 commit c43e0d5

File tree

11 files changed

+412
-12
lines changed

11 files changed

+412
-12
lines changed

src/main/java/com/back/domain/funding/controller/FundingCommunityController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ public ResponseEntity<RsData<?>> createFundingCommunity(
3535
.body(new RsData<>("201", "커뮤니티 글이 등록되었습니다.", createdId));
3636
}
3737

38-
@DeleteMapping("/{fundingId}/communities/{communityId}")
38+
@DeleteMapping("/{id}/communities/{communityId}")
3939
@PreAuthorize("isAuthenticated()")
4040
@Operation(summary = "펀딩 커뮤니티 글 삭제")
4141
public ResponseEntity<RsData<?>> deleteFundingCommunity(
42-
@PathVariable @Positive Long fundingId,
42+
@PathVariable @Positive Long id,
4343
@PathVariable @Positive Long communityId,
4444
@AuthenticationPrincipal(expression = "username") String userEmail
4545
) {
46-
fundingCommunityService.delete(fundingId, communityId, userEmail);
46+
fundingCommunityService.delete(id, communityId, userEmail);
4747
return ResponseEntity.ok(new RsData<>("200", "커뮤니티 글이 삭제되었습니다."));
4848
}
4949
}

src/main/java/com/back/domain/funding/controller/FundingController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class FundingController {
5555
"title": "한정판 키링 펀딩",
5656
"description": "한정판 키링입니다.",
5757
"categoryId": 1,
58+
"imageUrl": "https://test.jpg",
5859
"targetAmount": 500000,
5960
"price": 30000,
6061
"stock": 200,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.back.domain.funding.controller;
2+
3+
import com.back.domain.funding.dto.response.FundingCardDto;
4+
import com.back.domain.funding.service.FundingWishService;
5+
import com.back.global.rsData.RsData;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.data.domain.Page;
11+
import org.springframework.data.domain.PageRequest;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
15+
import org.springframework.web.bind.annotation.*;
16+
17+
@RestController
18+
@RequiredArgsConstructor
19+
@RequestMapping("/api/fundings")
20+
@Tag(name = "펀딩 찜", description = "펀딩 찜 API")
21+
public class FundingWishController {
22+
23+
private final FundingWishService fundingWishService;
24+
25+
@PostMapping("/{id}/wish")
26+
@Operation(summary = "펀딩 찜 추가", description = "특정 펀딩을 찜 목록에 추가합니다.")
27+
public ResponseEntity<RsData<Void>> addWish(
28+
@PathVariable Long id,
29+
@AuthenticationPrincipal(expression = "username") String userEmail) {
30+
31+
fundingWishService.addWish(id, userEmail);
32+
return ResponseEntity.ok(new RsData<>("200", "찜 목록에 추가되었습니다.", null));
33+
}
34+
35+
@DeleteMapping("/{id}/wish")
36+
@Operation(summary = "펀딩 찜 취소", description = "찜 목록에서 펀딩을 제거합니다.")
37+
public ResponseEntity<RsData<Void>> removeWish(
38+
@PathVariable Long id,
39+
@AuthenticationPrincipal(expression = "username") String userEmail) {
40+
41+
fundingWishService.removeWish(id, userEmail);
42+
return ResponseEntity.ok(new RsData<>("200", "찜 목록에서 제거되었습니다.", null));
43+
}
44+
45+
@GetMapping("/{id}/wish/check")
46+
@Operation(summary = "찜 여부 확인", description = "현재 사용자가 해당 펀딩을 찜했는지 확인합니다.")
47+
public ResponseEntity<RsData<Boolean>> checkWish(
48+
@PathVariable Long id,
49+
@AuthenticationPrincipal(expression = "username") String userEmail) {
50+
51+
boolean isWished = fundingWishService.isWished(id, userEmail);
52+
return ResponseEntity.ok(new RsData<>("200", "찜 여부 조회 성공", isWished));
53+
}
54+
55+
@GetMapping("/wishes")
56+
@Operation(summary = "내 찜 목록 조회", description = "현재 사용자의 찜 목록을 조회합니다.")
57+
public ResponseEntity<RsData<Page<FundingCardDto>>> getMyWishList(
58+
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0")
59+
@RequestParam(defaultValue = "0") int page,
60+
@Parameter(description = "페이지 크기", example = "12")
61+
@RequestParam(defaultValue = "12") int size,
62+
@AuthenticationPrincipal(expression = "username") String userEmail) {
63+
64+
Pageable pageable = PageRequest.of(page, size);
65+
Page<FundingCardDto> wishList = fundingWishService.getMyWishList(userEmail, pageable);
66+
return ResponseEntity.ok(new RsData<>("200", "찜 목록 조회 성공", wishList));
67+
}
68+
}

src/main/java/com/back/domain/funding/dto/request/FundingUpdateRequest.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package com.back.domain.funding.dto.request;
22

3-
import com.back.global.s3.S3FileRequest;
43
import io.swagger.v3.oas.annotations.media.Schema;
54
import jakarta.validation.constraints.Future;
65
import jakarta.validation.constraints.Positive;
76
import jakarta.validation.constraints.Size;
87

98
import java.time.LocalDateTime;
10-
import java.util.List;
119

1210
@Schema(description = "펀딩 수정 요청")
1311
public record FundingUpdateRequest(
@@ -35,7 +33,5 @@ public record FundingUpdateRequest(
3533

3634
@Schema(description = "펀딩 종료일", example = "2025-12-31T12:30:00")
3735
@Future
38-
LocalDateTime endDate,
39-
40-
List<S3FileRequest> images
36+
LocalDateTime endDate
4137
) {}

src/main/java/com/back/domain/funding/dto/response/FundingDetailResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ public static FundingDetailResponse.FundingImageResponse fromEntity(FundingImage
7676
}
7777

7878
// 작성자 DTO
79-
public record AuthorDto(Long id, String name, String profileImageUrl, String artistDescription) {
79+
public record AuthorDto(Long id, String name, String email, String profileImageUrl, String artistDescription) {
8080
public static AuthorDto from(User user, String artistDescription) {
81-
return new AuthorDto(user.getId(), user.getName(), user.getProfileImageUrl(), artistDescription);
81+
return new AuthorDto(user.getId(), user.getName(), user.getEmail(), user.getProfileImageUrl(), artistDescription);
8282
}
8383
}
8484

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.back.domain.funding.entity;
2+
3+
import com.back.domain.user.entity.User;
4+
import com.back.global.jpa.entity.BaseEntity;
5+
import jakarta.persistence.*;
6+
import lombok.*;
7+
8+
@Entity
9+
@Getter
10+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
11+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
12+
@Builder
13+
@Table(name = "funding_wishes", uniqueConstraints = {@UniqueConstraint(name = "uk_funding_wish_user_funding", columnNames = {"user_id", "funding_id"})},
14+
indexes = {@Index(name = "idx_funding_wish_user", columnList = "user_id"), @Index(name = "idx_funding_wish_funding", columnList = "funding_id")})
15+
public class FundingWish extends BaseEntity {
16+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
17+
@JoinColumn(name = "user_id", nullable = false)
18+
private User user;
19+
20+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
21+
@JoinColumn(name = "funding_id", nullable = false)
22+
private Funding funding;
23+
24+
public static FundingWish create(User user, Funding funding) {
25+
return FundingWish.builder()
26+
.user(user)
27+
.funding(funding)
28+
.build();
29+
}
30+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.back.domain.funding.repository;
2+
3+
import com.back.domain.funding.entity.FundingWish;
4+
import org.springframework.data.repository.query.Param;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Query;
9+
10+
import java.util.Optional;
11+
12+
public interface FundingWishRepository extends JpaRepository<FundingWish, Long> {
13+
14+
// 특정 사용자의 특정 펀딩 찜 여부 확인
15+
boolean existsByUserIdAndFundingId(Long userId, Long fundingId);
16+
17+
// 특정 사용자의 특정 펀딩 찜 조회
18+
Optional<FundingWish> findByUserIdAndFundingId(Long userId, Long fundingId);
19+
20+
// 특정 사용자의 찜 목록 조회 (페이징)
21+
@Query("SELECT fw FROM FundingWish fw " +
22+
"JOIN FETCH fw.funding f " +
23+
"WHERE fw.user.id = :userId " +
24+
"ORDER BY fw.createDate DESC")
25+
Page<FundingWish> findByUserIdWithFunding(@Param("userId") Long userId, Pageable pageable);
26+
}

src/main/java/com/back/domain/funding/service/FundingCommunityService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ public Long create(Long fundingId, FundingCommunityCreateRequest req, String use
3333
}
3434

3535
@Transactional
36-
public void delete(Long fundingId, Long communityId, String username) {
37-
Funding funding = fundingRepository.findById(fundingId)
36+
public void delete(Long id, Long communityId, String username) {
37+
Funding funding = fundingRepository.findById(id)
3838
.orElseThrow(() -> new ServiceException("404", "펀딩을 찾을 수 없습니다."));
3939
FundingCommunity post = fundingCommunityRepository.findById(communityId)
4040
.orElseThrow(() -> new ServiceException("404", "존재하지 않는 글입니다."));
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.back.domain.funding.service;
2+
3+
import com.back.domain.funding.dto.response.FundingCardDto;
4+
import com.back.domain.funding.entity.Funding;
5+
import com.back.domain.funding.entity.FundingWish;
6+
import com.back.domain.funding.repository.FundingRepository;
7+
import com.back.domain.funding.repository.FundingWishRepository;
8+
import com.back.domain.user.entity.User;
9+
import com.back.domain.user.repository.UserRepository;
10+
import com.back.global.exception.ServiceException;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.data.domain.Page;
13+
import org.springframework.data.domain.Pageable;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
public class FundingWishService {
20+
21+
private final FundingWishRepository fundingWishRepository;
22+
private final FundingRepository fundingRepository;
23+
private final UserRepository userRepository;
24+
25+
// 펀딩 찜 추가
26+
@Transactional
27+
public void addWish(Long fundingId, String userEmail) {
28+
User user = userRepository.findByEmail(userEmail)
29+
.orElseThrow(() -> new ServiceException("403", "존재하지 않는 사용자입니다."));
30+
31+
Funding funding = fundingRepository.findById(fundingId)
32+
.orElseThrow(() -> new ServiceException("404", "존재하지 않는 펀딩입니다."));
33+
34+
// 이미 찜했는지 확인
35+
if (fundingWishRepository.existsByUserIdAndFundingId(user.getId(), fundingId)) {
36+
throw new ServiceException("400", "이미 찜한 펀딩입니다.");
37+
}
38+
39+
FundingWish wish = FundingWish.create(user, funding);
40+
fundingWishRepository.save(wish);
41+
}
42+
43+
// 펀딩 찜 취소
44+
@Transactional
45+
public void removeWish(Long fundingId, String userEmail) {
46+
User user = userRepository.findByEmail(userEmail)
47+
.orElseThrow(() -> new ServiceException("403", "존재하지 않는 사용자입니다."));
48+
49+
FundingWish wish = fundingWishRepository.findByUserIdAndFundingId(user.getId(), fundingId)
50+
.orElseThrow(() -> new ServiceException("404", "찜하지 않은 펀딩입니다."));
51+
52+
fundingWishRepository.delete(wish);
53+
}
54+
55+
// 찜 여부 확인
56+
@Transactional(readOnly = true)
57+
public boolean isWished(Long fundingId, String userEmail) {
58+
User user = userRepository.findByEmail(userEmail)
59+
.orElseThrow(() -> new ServiceException("403", "존재하지 않는 사용자입니다."));
60+
61+
return fundingWishRepository.existsByUserIdAndFundingId(user.getId(), fundingId);
62+
}
63+
64+
// 사용자의 찜 목록 조회
65+
@Transactional(readOnly = true)
66+
public Page<FundingCardDto> getMyWishList(String userEmail, Pageable pageable) {
67+
User user = userRepository.findByEmail(userEmail)
68+
.orElseThrow(() -> new ServiceException("403", "존재하지 않는 사용자입니다."));
69+
70+
Page<FundingWish> wishPage = fundingWishRepository.findByUserIdWithFunding(
71+
user.getId(), pageable
72+
);
73+
74+
return wishPage.map(wish -> {
75+
Funding funding = wish.getFunding();
76+
// FundingService의 toCardDto 로직 재사용
77+
return toCardDto(funding);
78+
});
79+
}
80+
81+
private FundingCardDto toCardDto(Funding funding) {
82+
long currentAmount = funding.getCollectedAmount();
83+
double progress = (funding.getTargetAmount() > 0)
84+
? (double) currentAmount / funding.getTargetAmount() * 100
85+
: 0;
86+
int remainingDays = (int) java.time.temporal.ChronoUnit.DAYS.between(
87+
java.time.LocalDateTime.now(),
88+
funding.getEndDate()
89+
);
90+
91+
return new FundingCardDto(funding, currentAmount, progress, remainingDays);
92+
}
93+
}

src/main/java/com/back/global/security/config/SecurityConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
104104

105105
// 펀딩 관련 공개 API - 로그인 없이 접근 허용
106106
.requestMatchers(HttpMethod.GET, "/api/fundings/**").permitAll()
107+
// 펀딩 커뮤니티, 찜 - 로그인한 사용자만 접근 가능
108+
.requestMatchers("/api/fundings/{id}/communities", "/api/fundings/{id}/communities/{communityId}", "/api/fundings/{id}/wish", "/api/fundings/{id}/wish/check", "/api/fundings/wishes").authenticated()
107109

108110
// 개발 도구들
109111
.requestMatchers("/h2-console/**").permitAll()

0 commit comments

Comments
 (0)