Skip to content

Commit f30a2ad

Browse files
authored
Merge pull request #296 from Moadong/develop/be
[release] 모아동 BE ver 1.0.1
2 parents e755f69 + 3431de9 commit f30a2ad

File tree

17 files changed

+263
-91
lines changed

17 files changed

+263
-91
lines changed

backend/src/main/java/moadong/club/controller/ClubController.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public class ClubController {
4545
+ "category는 분류(취미교양 등), division은 분과(중동 등)입니다.")
4646
@PreAuthorize("isAuthenticated()")
4747
@SecurityRequirement(name = "BearerAuth")
48-
public ResponseEntity<?> createClub(@CurrentUser CustomUserDetails user, @RequestBody ClubCreateRequest request) {
48+
public ResponseEntity<?> createClub(@CurrentUser CustomUserDetails user,
49+
@RequestBody ClubCreateRequest request) {
4950
String clubId = clubCommandService.createClub(request);
5051
return Response.ok("success create club", "clubId : " + clubId);
5152
}
@@ -76,7 +77,11 @@ public ResponseEntity<?> updateClubDescription(
7677
public ResponseEntity<?> getClubDetailedPage(
7778
HttpServletRequest request,
7879
@PathVariable String clubId) {
79-
clubMetricService.patch(clubId, request.getRemoteAddr());
80+
String ip = request.getHeader("X-Forwarded-For");
81+
if (ip == null || ip.isEmpty()) {
82+
ip = request.getRemoteAddr();
83+
}
84+
clubMetricService.patch(clubId, ip);
8085
ClubDetailedResponse clubDetailedPageResponse = clubDetailedPageService.getClubDetailedPage(
8186
clubId);
8287
return Response.ok(clubDetailedPageResponse);

backend/src/main/java/moadong/club/controller/ClubMetricController.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.swagger.v3.oas.annotations.Operation;
44
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
55
import io.swagger.v3.oas.annotations.tags.Tag;
6+
import java.util.List;
67
import lombok.AllArgsConstructor;
78
import moadong.club.service.ClubMetricService;
89
import moadong.global.payload.Response;
@@ -11,21 +12,22 @@
1112
import org.springframework.web.bind.annotation.GetMapping;
1213
import org.springframework.web.bind.annotation.PathVariable;
1314
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RequestParam;
1416
import org.springframework.web.bind.annotation.RestController;
1517

1618
@RestController
1719
@RequestMapping("/api/club/metric")
1820
@AllArgsConstructor
1921
@Tag(name = "Club_Metric", description = "클럽 통계 API")
20-
@PreAuthorize("isAuthenticated()")
21-
@SecurityRequirement(name = "BearerAuth")
2222
public class ClubMetricController {
2323

2424
private final ClubMetricService clubMetricService;
2525

2626
@GetMapping("/{clubId}/daily")
2727
@Operation(summary = "클럽 일간 통계 조회", description = "클럽 일간 통계를 조회합니다.<br>"
2828
+ "오늘부터 30일이내의 통계를 순서대로 조회합니다.")
29+
@PreAuthorize("isAuthenticated()")
30+
@SecurityRequirement(name = "BearerAuth")
2931
public ResponseEntity<?> getDailyActiveUserWitClub(@PathVariable String clubId) {
3032
int[] metric = clubMetricService.getDailyActiveUserWitClub(clubId);
3133
return Response.ok(metric);
@@ -34,6 +36,8 @@ public ResponseEntity<?> getDailyActiveUserWitClub(@PathVariable String clubId)
3436
@GetMapping("/{clubId}/weekly")
3537
@Operation(summary = "클럽 주간 통계 조회", description = "클럽 주간 통계를 조회합니다.<br>"
3638
+ "현재주부터 12주전까지의 통계를 순서대로 조회합니다.<br>")
39+
@PreAuthorize("isAuthenticated()")
40+
@SecurityRequirement(name = "BearerAuth")
3741
public ResponseEntity<?> getWeeklyActiveUserWitClub(@PathVariable String clubId) {
3842
int[] metric = clubMetricService.getWeeklyActiveUserWitClub(clubId);
3943
return Response.ok(metric);
@@ -42,9 +46,18 @@ public ResponseEntity<?> getWeeklyActiveUserWitClub(@PathVariable String clubId)
4246
@GetMapping("/{clubId}/monthly")
4347
@Operation(summary = "클럽 월간 통계 조회", description = "클럽 월간 통계를 조회합니다.<br>"
4448
+ "현재월부터 12개월전까지의 통계를 순서대로 조회합니다.<br>")
49+
@PreAuthorize("isAuthenticated()")
50+
@SecurityRequirement(name = "BearerAuth")
4551
public ResponseEntity<?> getMonthlyActiveUserWitClub(@PathVariable String clubId) {
4652
int[] metric = clubMetricService.getMonthlyActiveUserWitClub(clubId);
4753
return Response.ok(metric);
4854
}
4955

56+
@GetMapping("/ranking")
57+
@Operation(summary = "동아리 조회순 조회", description = "당일 기준으로 동아리 상세페이지가 많이 조회된 동아리들을 순서대로 n개 조회합니다.<br>")
58+
public ResponseEntity<?> getClubDailyRanking(@RequestParam int n) {
59+
List<String> clubs = clubMetricService.getDailyRanking(n);
60+
return Response.ok(clubs);
61+
}
62+
5063
}

backend/src/main/java/moadong/club/entity/Club.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,21 @@ public class Club {
4444

4545
@Field("recruitmentInformation")
4646
private ClubRecruitmentInformation clubRecruitmentInformation;
47-
4847
public Club() {
4948
this.name = "";
5049
this.category = "";
5150
this.division = "";
5251
this.state = ClubState.UNAVAILABLE;
5352
this.clubRecruitmentInformation = ClubRecruitmentInformation.builder().build();
5453
}
54+
public Club(String userId) {
55+
this.name = "";
56+
this.category = "";
57+
this.division = "";
58+
this.state = ClubState.UNAVAILABLE;
59+
this.clubRecruitmentInformation = ClubRecruitmentInformation.builder().build();
60+
this.userId = userId;
61+
}
5562

5663
@Builder
5764
public Club(String name, String category, String division,

backend/src/main/java/moadong/club/repository/ClubMetricRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ public interface ClubMetricRepository extends MongoRepository<ClubMetric, String
1414

1515
List<ClubMetric> findByClubIdAndDateAfter(String clubId, LocalDate date);
1616

17+
List<ClubMetric> findAllByDate(LocalDate now);
1718
}

backend/src/main/java/moadong/club/repository/ClubRepository.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.util.List;
44
import java.util.Optional;
5-
65
import moadong.club.entity.Club;
76
import moadong.club.enums.ClubState;
87
import org.bson.types.ObjectId;
@@ -12,25 +11,34 @@
1211

1312
@Repository
1413
public interface ClubRepository extends MongoRepository<Club, String> {
14+
1515
Optional<Club> findClubById(ObjectId id);
16+
1617
Optional<List<Club>> findClubByState(ClubState clubState);
1718

1819
Optional<Club> findClubByUserId(String userId);
20+
1921
@Query("{'division': {$regex: '^?0$', $options: 'i'}}")
2022
Optional<List<Club>> findClubByDivisionIgnoreCaseExact(String division);
2123

2224
@Query("{'category': {$regex: '^?0$', $options: 'i'}}")
2325
Optional<List<Club>> findClubByCategoryIgnoreCaseExact(String category);
2426

2527
@Query("{'state': ?0, 'category': {$regex: '^?1$', $options: 'i'}}")
26-
Optional<List<Club>> findClubByStateAndCategoryIgnoreCaseExact(ClubState clubState, String category);
28+
Optional<List<Club>> findClubByStateAndCategoryIgnoreCaseExact(ClubState clubState,
29+
String category);
2730

2831
@Query("{'state': ?0, 'division': {$regex: '^?1$', $options: 'i'}}")
29-
Optional<List<Club>> findClubByStateAndDivisionIgnoreCaseExact(ClubState clubState, String division);
32+
Optional<List<Club>> findClubByStateAndDivisionIgnoreCaseExact(ClubState clubState,
33+
String division);
3034

3135
@Query("{'category': {$regex: '^?0$', $options: 'i'}, 'division': {$regex: '^?1$', $options: 'i'}}")
32-
Optional<List<Club>> findClubByCategoryAndDivisionIgnoreCaseExact(String category, String division);
36+
Optional<List<Club>> findClubByCategoryAndDivisionIgnoreCaseExact(String category,
37+
String division);
3338

3439
@Query("{'state': ?0, 'category': {$regex: '^?1$', $options: 'i'}, 'division': {$regex: '^?2$', $options: 'i'}}")
35-
Optional<List<Club>> findClubByStateAndCategoryAndDivisionIgnoreCaseExact(ClubState clubState, String category, String division);
40+
Optional<List<Club>> findClubByStateAndCategoryAndDivisionIgnoreCaseExact(ClubState clubState,
41+
String category, String division);
42+
43+
List<Club> findAllByName(List<String> clubs);
3644
}

backend/src/main/java/moadong/club/service/ClubMetricService.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@
55
import java.time.YearMonth;
66
import java.time.temporal.ChronoField;
77
import java.time.temporal.ChronoUnit;
8+
import java.util.ArrayList;
89
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Map.Entry;
912
import java.util.Optional;
13+
import java.util.stream.Collectors;
1014
import lombok.AllArgsConstructor;
15+
import moadong.club.entity.Club;
1116
import moadong.club.entity.ClubMetric;
1217
import moadong.club.repository.ClubMetricRepository;
18+
import moadong.club.repository.ClubRepository;
1319
import org.springframework.stereotype.Service;
1420

1521
@Service
1622
@AllArgsConstructor
1723
public class ClubMetricService {
1824

25+
private final ClubRepository clubRepository;
1926
private final ClubMetricRepository clubMetricRepository;
2027

2128
public void patch(String clubId, String remoteAddr) {
@@ -79,7 +86,8 @@ public int[] getMonthlyActiveUserWitClub(String clubId) {
7986
YearMonth currentMonth = YearMonth.from(now); // 현재 년-월
8087
YearMonth fromMonth = currentMonth.minusMonths(12); // 12개월 전
8188

82-
List<ClubMetric> metrics = clubMetricRepository.findByClubIdAndDateAfter(clubId, fromMonth.atDay(1));
89+
List<ClubMetric> metrics = clubMetricRepository.findByClubIdAndDateAfter(clubId,
90+
fromMonth.atDay(1));
8391

8492
// 12개월간의 통계를 월별로 저장할 배열
8593
int[] monthlyMetric = new int[12];
@@ -95,4 +103,27 @@ public int[] getMonthlyActiveUserWitClub(String clubId) {
95103

96104
return monthlyMetric;
97105
}
106+
107+
public List<String> getDailyRanking(int n) {
108+
List<ClubMetric> todayMetrics = clubMetricRepository.findAllByDate(LocalDate.now());
109+
Map<String, Long> clubViewCount = todayMetrics.stream()
110+
.collect(Collectors.groupingBy(ClubMetric::getClubId, Collectors.counting()));
111+
List<Map.Entry<String, Long>> sortedList = new ArrayList<>(clubViewCount.entrySet());
112+
sortedList.sort((a, b) -> Math.toIntExact(b.getValue()) - Math.toIntExact(a.getValue()));
113+
114+
List<String> clubIds = sortedList.stream()
115+
.limit(n)
116+
.map(Entry::getKey)
117+
.collect(Collectors.toList());
118+
119+
List<Club> clubs = clubRepository.findAllById(clubIds);
120+
121+
return clubIds.stream()
122+
.map(id -> clubs.stream()
123+
.filter(club -> club.getId().equals(id))
124+
.findFirst()
125+
.map(Club::getName)
126+
.orElse(null))
127+
.toList();
128+
}
98129
}

backend/src/main/java/moadong/gcs/controller/ClubImageController.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,14 @@ public ResponseEntity<?> deleteLogo(@PathVariable String clubId) {
4949
// TODO : Signed URL 을 통한 업로드로 추후 변경
5050
@PostMapping(value = "/{clubId}/feed", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
5151
@Operation(summary = "피드 이미지 업로드", description = "피드에 사용할 이미지를 업로드하고 주소를 반환받습니다.")
52-
public ResponseEntity<?> uploadFeed(@PathVariable String clubId,
53-
@RequestPart("feed") MultipartFile file) {
54-
return Response.ok(clubImageService.uploadFile(clubId, file, FileType.FEED));
52+
public ResponseEntity<?> uploadFeed(@PathVariable String clubId, @RequestPart("feed") MultipartFile file) {
53+
return Response.ok(clubImageService.uploadFeed(clubId, file));
5554
}
5655

5756
@PostMapping(value = "/{clubId}/feeds")
58-
@Operation(summary = "피드 이미지 업데이트", description = "피드 이미지들을 업데이트 합니다.")
59-
public ResponseEntity<?> putFeeds(@PathVariable String clubId,
60-
@RequestBody FeedUpdateRequest feeds) {
57+
@Operation(summary = "저장된 피드 이미지 업데이트(순서, 삭제 등..)", description = "피드 이미지의 설정을 업데이트 합니다.")
58+
public ResponseEntity<?> putFeeds(@PathVariable String clubId, @RequestBody FeedUpdateRequest feeds) {
6159
clubImageService.updateFeeds(clubId, feeds.feeds());
6260
return Response.ok("success put feeds");
6361
}
64-
6562
}

backend/src/main/java/moadong/gcs/service/ClubImageService.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public class ClubImageService {
3131
private final int MAX_FEED_COUNT = 5;
3232

3333

34-
// TODO : Signed URL 을 통한 업로드로 추후 변경
3534
public String uploadLogo(String clubId, MultipartFile file) {
3635
ObjectId objectId = new ObjectId(clubId);
3736
Club club = clubRepository.findClubById(objectId)
@@ -57,37 +56,45 @@ public void deleteLogo(String clubId) {
5756
}
5857
}
5958

59+
public String uploadFeed(String clubId, MultipartFile file) {
60+
ObjectId objectId = new ObjectId(clubId);
61+
int feedImagesCount = clubRepository.findClubById(objectId)
62+
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND))
63+
.getClubRecruitmentInformation().getFeedImages().size();
64+
65+
if (feedImagesCount + 1 > MAX_FEED_COUNT) {
66+
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
67+
}
68+
return uploadFile(clubId, file, FileType.FEED);
69+
}
70+
6071
public void updateFeeds(String clubId, List<String> newFeedImageList) {
6172
ObjectId objectId = new ObjectId(clubId);
6273
Club club = clubRepository.findClubById(objectId)
6374
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
6475

65-
if (newFeedImageList.isEmpty()) {
66-
throw new RestApiException(ErrorCode.FILE_NOT_FOUND);
67-
} else if (newFeedImageList.size() > MAX_FEED_COUNT) {
76+
if (newFeedImageList.size() > MAX_FEED_COUNT) {
6877
throw new RestApiException(ErrorCode.TOO_MANY_FILES);
6978
}
7079

7180
List<String> feedImages = club.getClubRecruitmentInformation().getFeedImages();
72-
if (feedImages != null) {
73-
updateFeedList(club, feedImages, newFeedImageList);
81+
if (feedImages != null && !feedImages.isEmpty()) {
82+
deleteFeedImages(club, feedImages, newFeedImageList);
7483
}
7584
club.updateFeedImages(newFeedImageList);
7685
clubRepository.save(club);
7786
}
7887

79-
private void updateFeedList(Club club, List<String> feedImages, List<String> newFeedImages) {
88+
private void deleteFeedImages(Club club, List<String> feedImages, List<String> newFeedImages) {
8089
for (String feedsImage : feedImages) {
8190
if (!newFeedImages.contains(feedsImage)) {
8291
deleteFile(club, feedsImage);
8392
}
8493
}
85-
club.updateFeedImages(newFeedImages);
86-
clubRepository.save(club);
8794
}
8895

8996
// TODO : Signed URL 을 통한 업로드 URL 반환으로 추후 변경
90-
public String uploadFile(String clubId, MultipartFile file, FileType fileType) {
97+
private String uploadFile(String clubId, MultipartFile file, FileType fileType) {
9198
if (file == null) {
9299
throw new RestApiException(ErrorCode.FILE_NOT_FOUND);
93100
}

backend/src/main/java/moadong/global/exception/ErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public enum ErrorCode {
1717
USER_NOT_EXIST(HttpStatus.BAD_REQUEST, "700-2","존재하지 않는 계정입니다."),
1818
USER_INVALID_FORMAT(HttpStatus.BAD_REQUEST, "700-3","올바르지 않은 유저 형식입니다."),
1919
USER_INVALID_LOGIN(HttpStatus.BAD_REQUEST, "700-4","올바르지 않은 로그인"),
20-
TOKEN_INVALID(HttpStatus.BAD_REQUEST, "701-1", "올바르지 않은 토큰 양식입니다."),
20+
TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "701-1", "올바르지 않은 토큰 양식입니다."),
21+
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "701-2", "토큰이 만료되었습니다."),
2122
;
2223

2324
private final HttpStatus httpStatus;

backend/src/main/java/moadong/global/util/JwtProvider.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package moadong.global.util;
22

33
import io.jsonwebtoken.Claims;
4+
import io.jsonwebtoken.JwtException;
45
import io.jsonwebtoken.Jwts;
56
import io.jsonwebtoken.SignatureAlgorithm;
67
import io.jsonwebtoken.security.Keys;
8+
import moadong.global.exception.ErrorCode;
9+
import moadong.global.exception.RestApiException;
10+
import moadong.user.entity.RefreshToken;
711
import org.springframework.beans.factory.annotation.Value;
812
import org.springframework.stereotype.Component;
913

@@ -23,18 +27,20 @@ public String generateAccessToken(String username) {
2327
return Jwts.builder()
2428
.setSubject(username)
2529
.setIssuedAt(new Date())
26-
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRATION_MIN * 1000 * 60))
30+
.setExpiration(new Date(System.currentTimeMillis() + (long) ACCESS_EXPIRATION_MIN * 1000 * 60))
2731
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
2832
.compact();
2933
}
3034

31-
public String generateRefreshToken(String username) {
32-
return Jwts.builder()
35+
public RefreshToken generateRefreshToken(String username) {
36+
Date expiresAt = new Date(System.currentTimeMillis() + (long) REFRESH_EXPIRATION_HOUR * 1000 * 60 * 60);
37+
String refreshToken = Jwts.builder()
3338
.setSubject(username)
3439
.setIssuedAt(new Date())
35-
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRATION_HOUR * 1000 * 60 * 60))
40+
.setExpiration(expiresAt)
3641
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
3742
.compact();
43+
return new RefreshToken(refreshToken,expiresAt);
3844
}
3945

4046
// 토큰에서 사용자 이름 추출
@@ -54,10 +60,14 @@ public boolean validateToken(String token, String username) {
5460

5561
// Claims 추출
5662
private Claims getClaims(String token) {
57-
return Jwts.parser()
58-
.setSigningKey(SECRET_KEY)
59-
.parseClaimsJws(token)
60-
.getBody();
63+
try {
64+
return Jwts.parser()
65+
.setSigningKey(SECRET_KEY)
66+
.parseClaimsJws(token)
67+
.getBody();
68+
} catch (JwtException e){
69+
throw new RestApiException(ErrorCode.TOKEN_INVALID);
70+
}
6171
}
6272

6373
private Key getSigningKey() {

0 commit comments

Comments
 (0)