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
5 changes: 3 additions & 2 deletions src/main/java/avengers/lion/item/service/ItemService.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ public void exchangeItem (Long memberId, Long itemId, ExchangeItemRequest exchan
.orElseThrow(()-> new BusinessException(ExceptionType.ITEM_NOT_FOUND));
Member member = memberRepository.findById(memberId)
.orElseThrow(()-> new BusinessException(ExceptionType.MEMBER_NOT_FOUND));
// 총 비용 = 아이템 가격 * 개수
int price = Math.multiplyExact(item.getPrice(), count);
// 구매하고자 하는 수량보다 재고가 적다 -> 에러 발생
// 구매하고자 하는 수량보다 재고가 적을 때
if(item.getStockCount()<count){
throw new BusinessException(ExceptionType.STOCK_NOT_AVAILABLE);
}
member.buyItemByPoint((long) price);

walletService.addPointTransaction(price, member, item.getItemCategory());
item.buyItem(count);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public class MemberController implements MemberApi {
public ResponseEntity<ResponseBody<MyProfileResponse>> getMyProfile(@AuthenticationPrincipal Long memberId) {
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(memberService.getMyProfile(memberId)));
}

/*
마이페이지 프로필 업데이트
*/
@PutMapping("/my-profile")
@PreAuthorize("hasAuthority('ROLE_USER')")
public ResponseEntity<ResponseBody<Void>> updateMyProfile(@AuthenticationPrincipal Long memberId, @RequestBody UpdateNicknameRequest updateNicknameRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
boolean existsByEmail(String email);

boolean existsByNicknameAndIdNot(String nickname, Long id);
}
5 changes: 5 additions & 0 deletions src/main/java/avengers/lion/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,14 @@ public Long getMyPoint(Long memberId){
return member.getPoint();
}


/*
프로필 업데이트
*/
@Transactional
public void updateNickname(Long memberId, UpdateNicknameRequest request){
String nickname = request.nickname();
// 닉네임이 이미 존재하는지 조회
if(memberRepository.existsByNicknameAndIdNot(nickname, memberId))
throw new BusinessException(ExceptionType.NICKNAME_ALREADY_EXISTS);
Member member = memberRepository.findById(memberId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ public ResponseEntity<ResponseBody<VerifyImageResponse>> startVerification(
@PathVariable Long missionId,
@Valid @RequestBody VerifyImageRequest request,
@AuthenticationPrincipal Long memberId) {

VerifyImageResponse response = missionVerifyService.startVerification(missionId, request, memberId);
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(response));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public List<Review> findAllReviewByMissionId(Long missionId, Long cursorId, int
BooleanBuilder where = new BooleanBuilder()
.and(r.completedMission.mission.id.eq(missionId));
if (cursorId != null) {
where.and(sortType.equals(SortType.ASC) ? r.id.lt(cursorId) : r.id.gt(cursorId));
where.and(sortType.equals(SortType.ASC) ? r.id.gt(cursorId) : r.id.lt(cursorId));
}
OrderSpecifier<?> orderSpecifier = sortType.equals(SortType.ASC) ? r.id.asc() : r.id.desc();

Expand Down
21 changes: 2 additions & 19 deletions src/main/java/avengers/lion/review/service/ReviewService.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,24 +103,6 @@ public PageResult<WrittenReviewResponse> getWrittenPage(Long memberId,
return new PageResult<>(data, hasNext, null, nextId);
}

/** 작성한 리뷰 상세조회 (ASC/DESC) */
public PageResult<WrittenReviewResponse> getWrittenDetailPage(Long memberId,
Long cursorId,
int limit,
SortType sort) {

List<Review> rows = reviewRepository.findWrittenPage(memberId, cursorId, limit+1, sort);

boolean hasNext = rows.size() > limit;
if (hasNext) rows = rows.subList(0, limit);

List<WrittenReviewResponse> data = WrittenReviewResponse.of(rows);
Long nextId = null;
if (!rows.isEmpty()) { Review last = rows.getLast(); nextId = last.getId(); }

return new PageResult<>(data, hasNext, null, nextId);
}

/** 작성 가능한 리뷰 작성 */
@Transactional
public void writeUnWrittenReview(Long memberId, WriteReviewRequest req) {
Expand All @@ -139,9 +121,10 @@ public void writeUnWrittenReview(Long memberId, WriteReviewRequest req) {
.member(cm.getMember())
.completedMission(cm)
.build();
member.addPointByReview();
walletService.addPointTransactionForReview(member);
reviewRepository.save(review);
cm.updateReviewStatus(ReviewStatus.ACTIVE);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public class WalletController implements WalletApi {

private final WalletService walletService;


/*
내 포인트 조회
*/
Expand All @@ -32,6 +31,9 @@ public ResponseEntity<ResponseBody<MyPointResponse>> getMyPoint(@AuthenticationP
return ResponseEntity.ok(ResponseUtil.createSuccessResponse(walletService.getMyPoint(memberId)));
}

/*
내 지갑 조회
*/
@GetMapping("/my-wallet")
@PreAuthorize("hasAuthority('ROLE_USER')")
public ResponseEntity<ResponseBody<MyWalletResponse>> getMyWallet(@AuthenticationPrincipal Long memberId){
Expand Down
57 changes: 24 additions & 33 deletions src/main/java/avengers/lion/wallet/service/WalletService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.function.Function;


@Service
@RequiredArgsConstructor
public class WalletService {

private static final int REVIEW_REWARD_POINTS = 100;

private final PointTransactionRepository pointTransactionRepository;
private final OrderService orderService;
private final MemberService memberService;
Expand All @@ -42,56 +45,38 @@ public MyWalletResponse getMyWallet(Long memberId){
Long consumedItemCount = orderService.myConsumedItemCount(memberId);
List<GiftCardResponse> giftCards = getMyGiftCards(memberId);
List<ParentItemResponse> partnerCards = getMyPartnerCards(memberId);
List<ConsumedItemResponse> usedItems = getMyUsedItems(memberId);
List<ConsumedItemResponse> usedItems = getMyConsumedItems(memberId);
return new MyWalletResponse(giftCardCount, partnerCardCount, consumedItemCount, giftCards, partnerCards, usedItems);
}

/*
내 상품권 조회
*/
public List<GiftCardResponse> getMyGiftCards(Long memberId){
List<Orders> orders = orderService.getGiftCards(memberId);
return orders.stream()
.flatMap(order -> order.getOrderItems().stream())
.map(GiftCardResponse::of)
.toList();
return convertOrdersToResponse(orders, GiftCardResponse::of);

}
/*
내 제휴상품 조회
*/

public List<ParentItemResponse> getMyPartnerCards(Long memberId){
List<Orders> orders = orderService.getPartnerCards(memberId);
return orders.stream()
.flatMap(order -> order.getOrderItems().stream())
.map(ParentItemResponse::of)
.toList();
return convertOrdersToResponse(orders, ParentItemResponse::of);
}
/*
사용한 상품 조회
*/
public List<ConsumedItemResponse> getMyUsedItems(Long memberId){

public List<ConsumedItemResponse> getMyConsumedItems(Long memberId){
List<Orders> orders = orderService.getConsumedItem(memberId);
return orders.stream()
.flatMap(order -> order.getOrderItems().stream())
.map(ConsumedItemResponse::of)
.toList();
return convertOrdersToResponse(orders, ConsumedItemResponse::of);
}

/*
리워드 내역 조회
*/

public List<RewardHistoryResponse> getRewardHistory(Long memberId){
List<PointTransaction> pointTransactions = pointTransactionRepository.findAllByMemberId(memberId);
return pointTransactions.stream()
.map(RewardHistoryResponse::of)
.toList();
}

/*
리워드 거래 내역 추가 (아이템 구매용)
*/

@Transactional
public void addPointTransaction(long price, Member member, ItemCategory itemCategory){
member.buyItemByPoint(price);
if(price<=0)
throw new BusinessException(ExceptionType.PRICE_IS_POSITIVE);

Expand All @@ -110,19 +95,18 @@ public void addPointTransaction(long price, Member member, ItemCategory itemCate
/*
포인트 적립 거래 내역 추가 (리뷰 작성용)
*/
@Transactional
public void addPointTransactionForReview(Member member) {
member.addPointByReview();
PointTransaction pointTransaction = PointTransaction.builder()
.changeAmount(100) // 리뷰 적립 포인트
.changeAmount(REVIEW_REWARD_POINTS)
.balanceAfter(Math.toIntExact(member.getPoint()))
.pointType(PointType.REVIEW)
.member(member)
.build();
pointTransactionRepository.save(pointTransaction);
}

/*
상품 사용 기능
*/
@Transactional
public void useItem(Long memberId, Long orderItemId){
OrderItem orderItem = orderService.getOrderItem(memberId, orderItemId);
Expand All @@ -131,4 +115,11 @@ public void useItem(Long memberId, Long orderItemId){
}
orderItem.useItem();
}

private <T> List<T> convertOrdersToResponse(List<Orders> orders, Function<OrderItem, T> mapper){
return orders.stream()
.flatMap(order -> order.getOrderItems().stream())
.map(mapper)
.toList();
}
}
71 changes: 43 additions & 28 deletions src/main/java/avengers/lion/weather/service/WeatherService.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,70 +45,85 @@ public class WeatherService {

private static final ZoneId KST = ZoneId.of("Asia/Seoul");
private static final DateTimeFormatter HOUR_FMT = DateTimeFormatter.ofPattern("a h시", Locale.KOREAN);
private static final String CACHE_KEY_PREFIX = "weather:gumi:basic:";
private static final String API_EXCLUDE_PARAMS = "minutely,alerts";
private static final int SKIP_CURRENT_HOUR = 1;
private static final int HOURLY_FORECAST_COUNT = 6;
private static final int CACHE_BUFFER_SECONDS = 5;

/*
현재 시간을 기반으로 Redis 키 생성
*/
private String cacheKey() {
ZonedDateTime hour = ZonedDateTime.now(KST).truncatedTo(ChronoUnit.HOURS);
return "weather:gumi:basic:" +hour.toEpochSecond();
return CACHE_KEY_PREFIX + hour.toEpochSecond();
}

private Duration ttlUntilNextHour() {
ZonedDateTime now = ZonedDateTime.now(KST);
return Duration.between(now, now.truncatedTo(ChronoUnit.HOURS).plusHours(1)).plusSeconds(5);
return Duration.between(now, now.truncatedTo(ChronoUnit.HOURS).plusHours(1)).plusSeconds(CACHE_BUFFER_SECONDS);
}

public WeatherBasic oneCall() {
String key = cacheKey();
// 캐시된 날씨 데이터가 있으면 바로 가져오기
Object hit = redis.opsForValue().get(key);
if(hit instanceof WeatherBasic cached) {
if (hit instanceof WeatherBasic cached) {
return cached;
}
OpenWeatherDto dto = requestOpenWeather();
// OpenWeatherApi를 호출하여 날씨 데이터 가져오기
OpenWeatherDto dto = requestOpenWeather();
OpenWeatherDto.Current current = dto.current();
List<OpenWeatherDto.Hourly> hourly = dto.hourly();
List<OpenWeatherDto.Daily> daily = dto.daily();

// 현재
WeatherBasic.Now now = new WeatherBasic.Now(
current.temp(),
current.weather().getFirst().icon(),
daily.getFirst().temp().max(),
daily.getFirst().temp().min());

WeatherBasic.Now now = convertToCurrentWeather(current, daily);
// 6시간
List<WeatherBasic.Hour> hourList = hourly.stream()
.skip(1)
.limit(6)
.map(hour -> {
ZonedDateTime kstTime = Instant.ofEpochSecond(hour.dt())
.atZone(ZoneId.of("Asia/Seoul"));

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("a h시", Locale.KOREAN);
String formattedTime = kstTime.format(formatter);
return new WeatherBasic.Hour(formattedTime, hour.temp(), hour.weather().getFirst().icon());
})
.toList();

// 오늘 제외
List<WeatherBasic.Hour> hourList = convertToHourWeather(hourly);
WeatherBasic view = new WeatherBasic(now, hourList);
redis.opsForValue().set(key, view, ttlUntilNextHour());
return view;
}

public OpenWeatherDto requestOpenWeather() {
URI uri = UriComponentsBuilder.fromUriString(baseUrl + "/data/3.0/onecall")
.queryParam("lat", lat)
.queryParam("lon", lon)
.queryParam("appid", apiKey)
.queryParam("lang", lang)
.queryParam("units", units)
.queryParam("exclude", "minutely,alerts")
.queryParam("exclude", API_EXCLUDE_PARAMS)
.build()
.toUri();
log.info("onecall uri = {}", uri);
try{
try {
return rest.getForObject(uri, OpenWeatherDto.class);
} catch(HttpClientErrorException | HttpServerErrorException | UnknownContentTypeException | ResourceAccessException e) {
} catch (HttpClientErrorException | HttpServerErrorException | UnknownContentTypeException |
ResourceAccessException e) {
log.error("OpenWeather API 호출 실패: {}", e.getMessage());
throw new BusinessException(ExceptionType.WEATHER_API_ERROR);
}
}
public WeatherBasic.Now convertToCurrentWeather(OpenWeatherDto.Current current, List<OpenWeatherDto.Daily> daily) {
return new WeatherBasic.Now(
current.temp(),
current.weather().getFirst().icon(),
daily.getFirst().temp().max(),
daily.getFirst().temp().min());
}

public List<WeatherBasic.Hour> convertToHourWeather(List<OpenWeatherDto.Hourly> hourly){
return hourly.stream()
.skip(SKIP_CURRENT_HOUR)
.limit(HOURLY_FORECAST_COUNT)
.map(hour -> {
ZonedDateTime kstTime = Instant.ofEpochSecond(hour.dt())
.atZone(ZoneId.of("Asia/Seoul"));

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("a h시", Locale.KOREAN);
String formattedTime = kstTime.format(formatter);
return new WeatherBasic.Hour(formattedTime, hour.temp(), hour.weather().getFirst().icon());
})
.toList();
}
}