Skip to content

Commit 9264042

Browse files
EpicFnEpicFn
andauthored
[Feat/OPS-275] 스페이스 썸네일 이미지 저장 (#71)
* chore : S3 환경 설정 * feat : S3Service 구성 * chore : multipart 데이터 크기 설정 * feat : S3 업로드 테스트용 엔드포인트 생성 * feat : S3 연동 완료 * feat : space entity에 imageUrl 항목 추가 * feat : 썸네일 캡처 테스트용 * feat : 썸네일 촬영 테스트 엔드포인트 작성 * feat : 스페이스 썸네일 갱신 API 생성 * feat : 스페이스 이미지 갱신 기능 완성 * feat : 스페이스 목록 반환 시 썸네일 url 같이 반환 * feat : 스페이스 단건 조회 테스트 케이스 작성 * feat : 스페이스 단건 조회 구현 * feat : 스페이스 목록 조회 페이징 기능 추가 --------- Co-authored-by: EpicFn <[email protected]>
1 parent 9fb37c7 commit 9264042

File tree

22 files changed

+527
-38
lines changed

22 files changed

+527
-38
lines changed

build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ dependencies {
7777
// Spring AI
7878
implementation "org.springframework.ai:spring-ai-starter-model-openai"
7979

80-
8180
// 크롤링
8281
implementation("org.jsoup:jsoup:1.21.2")
8382

@@ -87,6 +86,12 @@ dependencies {
8786
// Mysql driver
8887
implementation 'mysql:mysql-connector-java:8.0.33'
8988

89+
// AWS SDK for S3
90+
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.0'
91+
92+
// Playwright for Java
93+
implementation 'com.microsoft.playwright:playwright:1.54.0'
94+
9095
}
9196

9297
dependencyManagement {

src/main/java/org/tuna/zoopzoop/backend/domain/home/controller/HomeController.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public String main() {
3333
String kakaoLoginUrl = "/oauth2/authorization/kakao";
3434
String googleLoginUrl = "/oauth2/authorization/google";
3535
String logoutUrl = "/api/v1/auth/logout";
36+
String testS3UploadUrl = "/test/upload-file";
37+
String testThumbnailUrl = "/test/generate-thumbnail";
3638

3739
return """
3840
<h1>API 서버</h1>
@@ -56,6 +58,23 @@ public String main() {
5658
<input type="text" name="query" placeholder="검색어 입력"/>
5759
<input type="submit" value="검색"/>
5860
</form>
59-
""".formatted(localHost.getHostName(), localHost.getHostAddress(), kakaoLoginUrl, googleLoginUrl, logoutUrl);
61+
62+
<h2>S3 파일 업로드 테스트</h2>
63+
<form action="%s" method="post" enctype="multipart/form-data">
64+
<input type="text" name="fileName" placeholder="S3에 저장될 파일명 (예: images/test.jpg)"/>
65+
<br><br>
66+
<input type="file" name="file"/>
67+
<br><br>
68+
<input type="submit" value="업로드"/>
69+
</form>
70+
71+
<hr>
72+
<h2>썸네일 생성 테스트</h2>
73+
<div>
74+
<a href="%s">
75+
<button>썸네일 생성 및 S3 업로드 테스트</button>
76+
</a>
77+
</div>
78+
""".formatted(localHost.getHostName(), localHost.getHostAddress(), kakaoLoginUrl, googleLoginUrl, logoutUrl, testS3UploadUrl, testThumbnailUrl);
6079
}
6180
}

src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1InviteController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ public RsData<ResBodyForSpaceInviteList> getMyInvites(
8484
List<SpaceMembershipInfoWithoutAuthority> invitationInfos = invitations.stream()
8585
.map(membership -> new SpaceMembershipInfoWithoutAuthority(
8686
membership.getSpace().getId(),
87-
membership.getSpace().getName()
87+
membership.getSpace().getName(),
88+
membership.getSpace().getThumbnailUrl()
8889
))
8990
.toList();
9091

src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/repository/MembershipRepository.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.tuna.zoopzoop.backend.domain.space.membership.repository;
22

3+
import org.springframework.data.domain.Page;
4+
import org.springframework.data.domain.Pageable;
35
import org.springframework.data.jpa.repository.JpaRepository;
46
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
57
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
@@ -12,10 +14,12 @@
1214
public interface MembershipRepository extends JpaRepository<Membership, Integer> {
1315
boolean existsByMemberAndSpace(Member member, Space space);
1416

15-
List<Membership> findAllByMemberAndAuthority(Member member, Authority authority);
17+
Page<Membership> findAllByMemberAndAuthority(Member member, Authority authority, Pageable pageable);
18+
Page<Membership> findAllByMemberAndAuthorityIsNot(Member member, Authority authority, Pageable pageable);
19+
Page<Membership> findAllByMember(Member member, Pageable pageable);
1620

21+
List<Membership> findAllByMemberAndAuthority(Member member, Authority authority);
1722
List<Membership> findAllByMemberAndAuthorityIsNot(Member member, Authority authority);
18-
1923
List<Membership> findAllByMember(Member member);
2024

2125
boolean existsByMemberAndSpaceAndAuthorityIsNot(Member member, Space space, Authority authority);

src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import jakarta.validation.constraints.PositiveOrZero;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.dao.DataIntegrityViolationException;
8+
import org.springframework.data.domain.Page;
9+
import org.springframework.data.domain.Pageable;
810
import org.springframework.stereotype.Service;
911
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
1012
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;
@@ -50,6 +52,22 @@ public Membership findByMemberAndSpace(Member member, Space space) {
5052
.orElseThrow(() -> new NoResultException("해당 멤버는 스페이스에 속해있지 않습니다."));
5153
}
5254

55+
/**
56+
* 멤버가 속한 스페이스 목록 조회
57+
* @param member 조회할 멤버
58+
* @param state 멤버의 가입 상태로 필터링 (PENDING, JOINED, ALL)
59+
* @return 멤버가 속한 스페이스 목록
60+
*/
61+
public Page<Membership> findByMember(Member member, String state, Pageable pageable) {
62+
if (state.equalsIgnoreCase("PENDING")) {
63+
return membershipRepository.findAllByMemberAndAuthority(member, Authority.PENDING, pageable);
64+
} else if (state.equalsIgnoreCase("JOINED")) {
65+
return membershipRepository.findAllByMemberAndAuthorityIsNot(member, Authority.PENDING, pageable);
66+
} else {
67+
return membershipRepository.findAllByMember(member, pageable);
68+
}
69+
}
70+
5371
/**
5472
* 멤버가 속한 스페이스 목록 조회
5573
* @param member 조회할 멤버

src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,24 @@
44
import io.swagger.v3.oas.annotations.tags.Tag;
55
import jakarta.validation.Valid;
66
import lombok.RequiredArgsConstructor;
7+
import org.springframework.data.domain.Page;
8+
import org.springframework.data.domain.Pageable;
9+
import org.springframework.data.domain.Sort;
10+
import org.springframework.data.web.PageableDefault;
711
import org.springframework.security.core.annotation.AuthenticationPrincipal;
12+
import org.springframework.transaction.annotation.Transactional;
813
import org.springframework.web.bind.annotation.*;
14+
import org.springframework.web.multipart.MultipartFile;
915
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
1016
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
1117
import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority;
1218
import org.tuna.zoopzoop.backend.domain.space.membership.enums.JoinState;
1319
import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService;
1420
import org.tuna.zoopzoop.backend.domain.space.space.dto.req.ReqBodyForSpaceSave;
21+
import org.tuna.zoopzoop.backend.domain.space.space.dto.res.ResBodyForSpaceInfo;
1522
import org.tuna.zoopzoop.backend.domain.space.space.dto.res.ResBodyForSpaceList;
1623
import org.tuna.zoopzoop.backend.domain.space.space.dto.etc.SpaceMembershipInfo;
24+
import org.tuna.zoopzoop.backend.domain.space.space.dto.res.ResBodyForSpaceListPage;
1725
import org.tuna.zoopzoop.backend.domain.space.space.dto.res.ResBodyForSpaceSave;
1826
import org.tuna.zoopzoop.backend.domain.space.space.entity.Space;
1927
import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService;
@@ -22,6 +30,7 @@
2230

2331
import java.nio.file.AccessDeniedException;
2432
import java.util.List;
33+
import java.util.Map;
2534
import java.util.stream.Collectors;
2635

2736
@RestController
@@ -99,33 +108,49 @@ public RsData<ResBodyForSpaceSave> updateSpaceName(
99108
);
100109
}
101110

111+
@PutMapping(path = "/thumbnail/{spaceId}", consumes = {"multipart/form-data"})
112+
@Operation(summary = "스페이스 썸네일 이미지 갱신")
113+
public RsData<Void> updateSpaceThumbnail(
114+
@AuthenticationPrincipal CustomUserDetails userDetails,
115+
@PathVariable Integer spaceId,
116+
@RequestPart(value = "image", required = false) MultipartFile image
117+
) {
118+
Member member = userDetails.getMember();
119+
120+
spaceService.updateSpaceThumbnail(spaceId, member, image);
121+
122+
return new RsData<>(
123+
"200",
124+
"스페이스 썸네일 이미지가 갱신됐습니다.",
125+
null
126+
);
127+
}
128+
102129
@GetMapping
103-
@Operation(summary = "스페이스 목록 조회")
104-
public RsData<ResBodyForSpaceList> getAllSpaces(
130+
@Operation(summary = "나의 스페이스 목록 조회")
131+
public RsData<ResBodyForSpaceListPage> getAllSpaces(
105132
@AuthenticationPrincipal CustomUserDetails userDetails,
106-
@RequestParam(required = false) JoinState state
133+
@RequestParam(required = false) JoinState state,
134+
@PageableDefault(size = 10, sort = "createDate", direction = Sort.Direction.DESC) Pageable pageable
107135
) {
108136
// 현재 로그인한 사용자 정보 가져오기
109137
Member member = userDetails.getMember();
110138

111139
// 멤버가 속한 스페이스 목록 조회
112-
List<Membership> memberships;
113-
if (state == null) {
114-
memberships = membershipService.findByMember(member, "ALL");
115-
}
116-
else {
117-
memberships = membershipService.findByMember(member, state.name());
118-
}
119-
120-
// 반환 값 생성
121-
List<SpaceMembershipInfo> spaceInfos = memberships.stream()
122-
.map(membership -> new SpaceMembershipInfo(
123-
membership.getSpace().getId(),
124-
membership.getSpace().getName(),
125-
membership.getAuthority()
126-
))
127-
.collect(Collectors.toList());
128-
ResBodyForSpaceList resBody = new ResBodyForSpaceList(spaceInfos);
140+
String stateStr = (state == null) ? "ALL" : state.name();
141+
Page<Membership> membershipsPage = membershipService.findByMember(member, stateStr, pageable);
142+
143+
// Page<Membership>를 Page<SpaceMembershipInfo>로 변환
144+
// Page 객체의 map() 메서드를 사용하면 페이징 정보는 그대로 유지하면서 내용물만 쉽게 바꿀 수 있습니다.
145+
Page<SpaceMembershipInfo> spaceInfosPage = membershipsPage.map(membership -> new SpaceMembershipInfo(
146+
membership.getSpace().getId(),
147+
membership.getSpace().getName(),
148+
membership.getSpace().getThumbnailUrl(),
149+
membership.getAuthority()
150+
));
151+
152+
// 새로운 응답 DTO 생성
153+
ResBodyForSpaceListPage resBody = new ResBodyForSpaceListPage(spaceInfosPage);
129154

130155
return new RsData<>(
131156
"200",
@@ -134,6 +159,32 @@ public RsData<ResBodyForSpaceList> getAllSpaces(
134159
);
135160
}
136161

162+
@GetMapping("/{spaceId}")
163+
@Operation(summary = "스페이스 단건 조회")
164+
public RsData<ResBodyForSpaceInfo> getSpace(
165+
@AuthenticationPrincipal CustomUserDetails userDetails,
166+
@PathVariable Integer spaceId
167+
) {
168+
Member member = userDetails.getMember();
169+
Space space = spaceService.findById(spaceId);
170+
171+
// 해당 스페이스에 속한 멤버인지 확인
172+
Membership membership = membershipService.findByMemberAndSpace(member, space);
173+
174+
ResBodyForSpaceInfo resBody = new ResBodyForSpaceInfo(
175+
space.getId(),
176+
space.getName(),
177+
space.getThumbnailUrl(),
178+
membership.getAuthority().name(),
179+
space.getSharingArchive().getId()
180+
);
181+
182+
return new RsData<>(
183+
"200",
184+
String.format("%s - 스페이스가 조회됐습니다.", space.getName()),
185+
resBody
186+
);
187+
}
137188

138189

139190
}

src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/etc/SpaceMembershipInfo.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
public record SpaceMembershipInfo(
66
Integer id,
77
String name,
8+
String thumbnailUrl,
89
Authority authority
910
) {
1011
}

src/main/java/org/tuna/zoopzoop/backend/domain/space/space/dto/etc/SpaceMembershipInfoWithoutAuthority.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public record SpaceMembershipInfoWithoutAuthority(
44
Integer id,
5-
String name
5+
String name,
6+
String thumbnailUrl
67
) {
78
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.tuna.zoopzoop.backend.domain.space.space.dto.res;
2+
3+
public record ResBodyForSpaceInfo (
4+
Integer spaceId,
5+
String spaceName,
6+
String thumbnailUrl,
7+
String userAuthority,
8+
Integer sharingArchiveId
9+
){
10+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.tuna.zoopzoop.backend.domain.space.space.dto.res;
2+
3+
import lombok.Getter;
4+
import org.springframework.data.domain.Page;
5+
import org.tuna.zoopzoop.backend.domain.space.space.dto.etc.SpaceMembershipInfo;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
public class ResBodyForSpaceListPage {
11+
private final List<SpaceMembershipInfo> spaces; // 현재 페이지의 데이터
12+
private final int page; // 현재 페이지 번호 (0부터 시작)
13+
private final int size; // 페이지 크기
14+
private final long totalElements; // 전체 요소 수
15+
private final int totalPages; // 전체 페이지 수
16+
private final boolean isLast; // 마지막 페이지 여부
17+
18+
public ResBodyForSpaceListPage(Page<SpaceMembershipInfo> page) {
19+
this.spaces = page.getContent();
20+
this.page = page.getNumber();
21+
this.size = page.getSize();
22+
this.totalElements = page.getTotalElements();
23+
this.totalPages = page.getTotalPages();
24+
this.isLast = page.isLast();
25+
}
26+
}

0 commit comments

Comments
 (0)