Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.capstone.bszip.Bookstore;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class BookstoreLikeTrendingScheduler {

private final RedisTemplate<String,String> redisTemplate;

@Scheduled(cron="0 0 0 * * MON")
public void resetWeeklyTrend(){
// 백업
redisTemplate.rename("trending:bookstores:weekly", "trending:bookstores:lastweek");
// 초기화
redisTemplate.delete("trending:bookstores:weekly");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -30,6 +31,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;


@RestController
Expand Down Expand Up @@ -307,4 +309,59 @@ public ResponseEntity<?> getBookstoreDetails(@PathVariable Long bookstoreId,
.build());
}
}

@Operation(
summary = "급상승 독립서점 목록 조회",
description = "관심 급상승(weekly 인기순) 독립서점 이름 리스트를 반환합니다. " +
"데이터 10개를 제공하며 부족할 경우 지난 주 랭킹/전체 서점에서 순차적으로 채워집니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "급상승 서점 목록 조회 성공",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = SuccessResponse.class))),
@ApiResponse(responseCode = "404", description = "급상승 서점 정보가 존재하지 않음",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "503", description = "Redis 연결 실패 등 서비스 일시적 장애",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "서버 오류 발생",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/trending")
public ResponseEntity<?> getTrendingBookstores(){
try {
List<String> trendingBookstores = bookstoreService.getTrendingBookstoresNames();
return ResponseEntity.ok(SuccessResponse.<List<String>>builder()
.result(true)
.status(HttpStatus.OK.value())
.message("급상승 서점 목록 조회 성공")
.data(trendingBookstores)
.build());

} catch (NoSuchElementException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.builder()
.result(false)
.status(HttpStatus.NOT_FOUND.value())
.message("급상승 서점 정보가 존재하지 않습니다")
.detail(e.getMessage())
.build());

} catch (RedisConnectionFailureException e) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ErrorResponse.builder()
.result(false)
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
.message("서비스 일시적으로 이용할 수 없습니다")
.detail("Redis 연결 실패: " + e.getMessage())
.build());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.builder()
.result(false)
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("서버 오류가 발생했습니다")
.detail(e.getMessage())
.build());
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ List<Bookstore> findAllByIdOrderByDistance(@Param("bookstoreIds") List<Long> boo
List<Bookstore> findAllByNameContaining(String name);

List<Bookstore> findTop10ByOrderByBookstoreIdDesc();

@Query("SELECT b.bookstoreId, b.name FROM Bookstore b WHERE b.bookstoreId IN :ids")
List<Object[]> findIdAndNameByIds(@Param("ids") List<Long> ids);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -89,10 +86,12 @@ public void toggleLikeBookstore(Long memberId, Long bookstoreId){
if(redisTemplate.opsForSet().isMember(memberKey,bookstoreId.toString())){
redisTemplate.opsForSet().remove(memberKey,bookstoreId.toString());//찜 취소
redisTemplate.opsForValue().decrement(bookstoreKey); //찜한 수 -1
redisTemplate.opsForZSet().incrementScore("trending:bookstores:weekly", String.valueOf(bookstoreId), -1);
}
else{
redisTemplate.opsForSet().add(memberKey,bookstoreId.toString());//찜
redisTemplate.opsForValue().increment(bookstoreKey); //찜한 수 +1
redisTemplate.opsForZSet().incrementScore("trending:bookstores:weekly", String.valueOf(bookstoreId), 1);
}

}
Expand Down Expand Up @@ -212,5 +211,48 @@ private static String extractMatch(Pattern pattern, String text) {
return matcher.find() ? matcher.group(1).trim() : null;
}

public List<String> getTrendingBookstoresNames(){
Set<String> topBookstoreIds = redisTemplate.opsForZSet()
.reverseRange("trending:bookstores:weekly", 0, 9);
List<Long> idList = (topBookstoreIds == null) ? new ArrayList<>()
: topBookstoreIds.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
List<String> nameList = new ArrayList<>();
System.out.println(idList);
if(idList.size() < 10){
Set<String> lastWeekIds = redisTemplate.opsForZSet()
.reverseRange("trending:bookstores:lastweek", 0, 9);
if (lastWeekIds != null && !lastWeekIds.isEmpty()) {
List<Long> lastWeekIdList = lastWeekIds.stream()
.map(Long::valueOf)
.filter(id -> !idList.contains(id))
.collect(Collectors.toList());
int need = 10 - idList.size();
idList.addAll(lastWeekIdList.stream().limit(need).toList());
}
}
if (idList.size() < 10) {
int need = 10 - idList.size();
for (long i = 1; idList.size() < 10; i++) {
System.out.println("i = " + i + ", contains = " + idList.contains(i));
if (!idList.contains(i)) {
idList.add(i);
}
}
}
List<Object[]> idNamePairs = bookstoreRepository.findIdAndNameByIds(idList);
Map<Long, String> idNameMap = idNamePairs.stream()
.collect(Collectors.toMap(
row -> (Long)row[0],
row -> (String)row[1]
));

List<String> orderedNames = idList.stream()
.map(idNameMap::get)
.collect(Collectors.toList());
return orderedNames;
}

}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.capstone.bszip.Member.controller;

import com.capstone.bszip.Member.service.dto.LoginResponse;
import com.capstone.bszip.auth.dto.TokenRequest;
import com.capstone.bszip.auth.dto.TokenResponse;
import com.capstone.bszip.auth.AuthService;
Expand Down Expand Up @@ -127,13 +128,13 @@ public ResponseEntity<?> add(

public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response){
try{
TokenResponse tokens = memberService.loginUser(loginRequest);
authService.login(loginRequest.getEmail(), tokens.getRefreshToken());
LoginResponse loginResponse = memberService.loginUser(loginRequest);
authService.login(loginRequest.getEmail(), loginResponse.getRefreshToken());
return ResponseEntity.ok(SuccessResponse.builder()
.result(true)
.status(HttpStatus.OK.value())
.message("로그인 성공")
.data(tokens)
.data(loginResponse)
.build());
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.capstone.bszip.Member.domain.Member;
import com.capstone.bszip.Member.domain.MemberJoinType;
import com.capstone.bszip.Member.repository.MemberRepository;
import com.capstone.bszip.Member.service.dto.LoginResponse;
import com.capstone.bszip.auth.dto.TokenResponse;
import com.capstone.bszip.auth.security.JwtUtil;
import com.capstone.bszip.Member.service.dto.LoginRequest;
Expand Down Expand Up @@ -82,18 +83,19 @@ public void registerMemberNickname(String email,String nickname){
temporaryStorage.remove(email);
}
@Transactional
public TokenResponse loginUser(LoginRequest loginRequest){
public LoginResponse loginUser(LoginRequest loginRequest){
Member member = memberRepository.findByEmail(loginRequest.getEmail())
.orElseThrow(() -> new RuntimeException("존재하지 않는 이메일입니다."));
if(member.getMemberJoinType() != DEFAULT){
throw new RuntimeException("카카오로 로그인해주세요");
}
if(passwordEncoder.matches(loginRequest.getPassword(), member.getPassword())) {
String email = member.getEmail();
String nickname = member.getNickname();
//토큰 생성
String accessToken = JwtUtil.createAccessToken(email);
String refreshToken = JwtUtil.createRefreshToken(email);
return new TokenResponse(accessToken, refreshToken);
return new LoginResponse(nickname,accessToken, refreshToken);
}
else {
throw new RuntimeException("일치하지 않는 비밀번호입니다.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.capstone.bszip.Member.service.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class LoginResponse {
private String nickname;
private String accessToken;
private String refreshToken;
}