Skip to content

Commit a6735a2

Browse files
sengjun0624YoungjaeRoCopilotjeongmin07262
authored
[DEPLOY] v1.0.2 (#116)
* ✨ feat: Add StoreDetail Api * ✨ feat: Add StoreDetail Voucher Api * ✨ feat: Set TimeZone KR On Boot * ✨ feat: 카카오맵 조회 API 최적화 * fix: HikariCP Pool size error * Update src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/main/java/com/example/Tokkit_server/store/controller/StoreController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: noeyoes <> Co-authored-by: 노영재 <146312456+YoungjaeRo@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: 이정민 <152269806+jeongmin07262@users.noreply.github.com>
1 parent 0e1fa07 commit a6735a2

9 files changed

Lines changed: 213 additions & 6 deletions

File tree

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
package com.example.Tokkit_server;
22

3+
import java.util.TimeZone;
4+
35
import org.springframework.boot.SpringApplication;
46
import org.springframework.boot.autoconfigure.SpringBootApplication;
57
import org.springframework.boot.autoconfigure.domain.EntityScan;
68
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
79
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
10+
import org.springframework.scheduling.annotation.EnableAsync;
811
import org.springframework.scheduling.annotation.EnableScheduling;
912

1013
@SpringBootApplication(scanBasePackages = "com.example")
14+
@EnableAsync
1115
@EnableJpaAuditing
1216
@EnableScheduling
1317
@EnableJpaRepositories(basePackages = "com.example")
1418
@EntityScan(basePackages = "com.example")
1519
public class TokkitServerApplication {
1620

1721
public static void main(String[] args) {
22+
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
1823
SpringApplication.run(TokkitServerApplication.class, args);
1924
}
20-
2125
}

src/main/java/com/example/Tokkit_server/notification/service/NotificationServiceImpl.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import com.example.Tokkit_server.user.utils.SseEmitters;
1818
import lombok.RequiredArgsConstructor;
1919
import lombok.extern.slf4j.Slf4j;
20+
import org.springframework.scheduling.annotation.Async;
2021
import org.springframework.stereotype.Service;
22+
import org.springframework.transaction.annotation.Propagation;
2123
import org.springframework.transaction.annotation.Transactional;
2224
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
2325

@@ -76,7 +78,6 @@ public void sendNotification(User user, NotificationTemplate template, Object...
7678
notificationRepository.save(notification);
7779
}
7880

79-
@Transactional
8081
public SseEmitter subscribe(Long userId) {
8182
SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT);
8283
sseEmitters.add(userId, emitter);
@@ -91,9 +92,10 @@ public SseEmitter subscribe(Long userId) {
9192
sseEmitters.remove(userId);
9293
}
9394

94-
userRepository.findById(userId).ifPresent(this::sendUnsentNotifications);
95+
// 트랜잭션 점유 방지를 위해 비동기로 분리된 메서드 호출
96+
userRepository.findById(userId).ifPresent(this::sendUnsentNotificationsAsync);
9597

96-
// 연결 유지용 ping
98+
// Ping 유지
9799
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
98100
scheduler.scheduleAtFixedRate(() -> {
99101
try {
@@ -123,6 +125,12 @@ public SseEmitter subscribe(Long userId) {
123125
return emitter;
124126
}
125127

128+
@Async
129+
@Transactional(propagation = Propagation.REQUIRES_NEW)
130+
public void sendUnsentNotificationsAsync(User user) {
131+
sendUnsentNotifications(user);
132+
}
133+
126134
@Transactional
127135
public void deleteNotification(Long notificationId, User user) {
128136
Notification notification = notificationRepository.findByIdAndUser(notificationId, user)

src/main/java/com/example/Tokkit_server/store/controller/StoreController.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22

33
import com.example.Tokkit_server.global.apiPayload.ApiResponse;
44
import com.example.Tokkit_server.store.dto.response.KakaoMapSearchResponse;
5+
import com.example.Tokkit_server.store.dto.response.StoreBasicInfoResponseDto;
56
import com.example.Tokkit_server.store.dto.response.StoreInfoResponse;
67
import com.example.Tokkit_server.store.dto.response.StoreSimpleResponse;
8+
import com.example.Tokkit_server.store.dto.response.VoucherPageResponseDto;
79
import com.example.Tokkit_server.store.service.StoreService;
810
import com.example.Tokkit_server.store.service.command.StoreCommandService;
11+
import com.example.Tokkit_server.store.service.query.StoreQueryService;
912
import com.example.Tokkit_server.user.auth.CustomUserDetails;
1013
import io.swagger.v3.oas.annotations.Operation;
1114
import io.swagger.v3.oas.annotations.tags.Tag;
1215
import lombok.RequiredArgsConstructor;
16+
17+
import org.springframework.data.domain.Pageable;
18+
import org.springframework.data.web.PageableDefault;
1319
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1420
import org.springframework.web.bind.annotation.GetMapping;
21+
import org.springframework.web.bind.annotation.PathVariable;
1522
import org.springframework.web.bind.annotation.RequestMapping;
1623
import org.springframework.web.bind.annotation.RequestParam;
1724
import org.springframework.web.bind.annotation.RestController;
@@ -27,6 +34,7 @@ public class StoreController {
2734

2835
private final StoreCommandService storeCommandService;
2936
private final StoreService storeService;
37+
private final StoreQueryService storeQueryService;
3038

3139
@GetMapping("/nearby")
3240
@Operation(
@@ -69,4 +77,17 @@ public ApiResponse<StoreSimpleResponse> getStoreSimpleInfo(@RequestParam Long st
6977
StoreSimpleResponse response = storeService.getSimpleStoreInfo(storeId);
7078
return ApiResponse.onSuccess(response);
7179
}
80+
@GetMapping("/{storeId}")
81+
public ApiResponse<StoreBasicInfoResponseDto> getStoreInfo(@PathVariable Long storeId) {
82+
return ApiResponse.onSuccess(storeQueryService.getStoreInfo(storeId));
83+
}
84+
85+
@GetMapping("/{storeId}/vouchers")
86+
public ApiResponse<VoucherPageResponseDto> getAvailabㄴleVouchers(
87+
@PathVariable Long storeId,
88+
@AuthenticationPrincipal CustomUserDetails userDetails,
89+
@PageableDefault(size = 5) Pageable pageable
90+
) {
91+
return ApiResponse.onSuccess(storeQueryService.getAvailableVouchers(storeId, userDetails.getId(), pageable));
92+
}
7293
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.example.Tokkit_server.store.dto.response;
2+
3+
import com.example.Tokkit_server.store.entity.Store;
4+
5+
public record StoreBasicInfoResponseDto(
6+
Long storeId,
7+
String storeName,
8+
String category,
9+
String address,
10+
String postalCode
11+
) {
12+
public static StoreBasicInfoResponseDto from(Store store) {
13+
return new StoreBasicInfoResponseDto(
14+
store.getId(),
15+
store.getStoreName(),
16+
store.getStoreCategory().name(),
17+
store.getRoadAddress(),
18+
store.getNewZipcode()
19+
);
20+
}
21+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.example.Tokkit_server.store.dto.response;
2+
3+
import com.example.Tokkit_server.voucher.entity.Voucher;
4+
import com.example.Tokkit_server.voucher_ownership.entity.VoucherOwnership;
5+
6+
import java.time.LocalDate;
7+
import java.util.List;
8+
9+
import org.springframework.data.domain.Page;
10+
11+
public record VoucherPageResponseDto(
12+
List<VoucherInfoDto> content,
13+
int currentPage,
14+
int pageSize,
15+
int totalPages,
16+
long totalElements,
17+
boolean hasNext
18+
) {
19+
public static VoucherPageResponseDto from(Page<VoucherOwnership> page) {
20+
List<VoucherInfoDto> content = page.getContent().stream()
21+
.map(VoucherInfoDto::from)
22+
.toList();
23+
24+
return new VoucherPageResponseDto(
25+
content,
26+
page.getNumber(),
27+
page.getSize(),
28+
page.getTotalPages(),
29+
page.getTotalElements(),
30+
page.hasNext()
31+
);
32+
}
33+
34+
public record VoucherInfoDto(
35+
Long voucherId,
36+
String name,
37+
LocalDate validUntil,
38+
Long balance
39+
) {
40+
public static VoucherInfoDto from(VoucherOwnership ownership) {
41+
Voucher v = ownership.getVoucher();
42+
return new VoucherInfoDto(
43+
v.getId(),
44+
v.getName(),
45+
v.getValidDate().toLocalDate(),
46+
ownership.getRemainingAmount()
47+
);
48+
}
49+
}
50+
}

src/main/java/com/example/Tokkit_server/store/repository/StoreRepository.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public interface StoreRepository extends JpaRepository<Store, Long> {
1919
"FROM VoucherStore vs JOIN vs.store s " +
2020
"WHERE vs.voucher.id = :voucherId")
2121
Page<StoreResponse> findByVoucherId(@Param("voucherId") Long voucherId, Pageable pageable);
22-
22+
/*
2323
2424
@Query(value = """
2525
SELECT
@@ -48,7 +48,37 @@ List<Object[]> findNearbyStoresRaw(
4848
@Param("radius") double radius,
4949
@Param("category") String category,
5050
@Param("keyword") String keyword
51-
);
51+
);*/
52+
@Query(value = """
53+
SELECT
54+
s.id AS id,
55+
s.store_name AS storeName,
56+
s.road_address AS roadAddress,
57+
s.new_zipcode AS newZipcode,
58+
s.latitude AS latitude,
59+
s.longitude AS longitude,
60+
s.store_category AS storeCategory,
61+
ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(:lng, :lat)) AS distance
62+
FROM wallet w
63+
JOIN voucher_ownership vo ON w.id = vo.wallet_id
64+
JOIN voucher_store vs ON vo.voucher_id = vs.voucher_id
65+
JOIN store s ON vs.store_id = s.id
66+
WHERE w.user_id = :userId
67+
AND s.latitude BETWEEN :lat - (:radius / 111320) AND :lat + (:radius / 111320)
68+
AND s.longitude BETWEEN :lng - (:radius / (111320 * COS(RADIANS(:lat)))) AND :lng + (:radius / (111320 * COS(RADIANS(:lat))))
69+
AND (:category IS NULL OR s.store_category = :category)
70+
AND (:keyword IS NULL OR s.store_name LIKE CONCAT('%', :keyword, '%') OR s.road_address LIKE CONCAT('%', :keyword, '%'))
71+
HAVING distance <= :radius
72+
ORDER BY distance
73+
""", nativeQuery = true)
74+
List<Object[]> findNearbyStoresRaw(
75+
@Param("userId") Long userId,
76+
@Param("lat") double lat,
77+
@Param("lng") double lng,
78+
@Param("radius") double radius,
79+
@Param("category") String category,
80+
@Param("keyword") String keyword
81+
);
5282

5383

5484
Optional<Store> findByIdAndMerchantId(Long storeId, Long merchantId);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.Tokkit_server.store.service.query;
2+
import com.example.Tokkit_server.store.dto.response.StoreBasicInfoResponseDto;
3+
import com.example.Tokkit_server.store.dto.response.VoucherPageResponseDto;
4+
5+
import org.springframework.data.domain.Pageable;
6+
public interface StoreQueryService {
7+
StoreBasicInfoResponseDto getStoreInfo(Long storeId);
8+
VoucherPageResponseDto getAvailableVouchers(Long storeId, Long userId, Pageable pageable);
9+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.example.Tokkit_server.store.service.query;
2+
3+
import com.example.Tokkit_server.global.apiPayload.code.status.ErrorStatus;
4+
import com.example.Tokkit_server.global.apiPayload.exception.GeneralException;
5+
import com.example.Tokkit_server.store.dto.response.StoreBasicInfoResponseDto;
6+
import com.example.Tokkit_server.store.dto.response.VoucherPageResponseDto;
7+
import com.example.Tokkit_server.store.entity.Store;
8+
import com.example.Tokkit_server.store.repository.StoreRepository;
9+
import com.example.Tokkit_server.user.repository.UserRepository;
10+
import com.example.Tokkit_server.voucher_ownership.entity.VoucherOwnership;
11+
import com.example.Tokkit_server.voucher_ownership.repository.VoucherOwnershipRepository;
12+
13+
import lombok.RequiredArgsConstructor;
14+
15+
import org.springframework.data.domain.Page;
16+
import org.springframework.data.domain.Pageable;
17+
import org.springframework.stereotype.Service;
18+
19+
@Service
20+
@RequiredArgsConstructor
21+
public class StoreQueryServiceImpl implements StoreQueryService {
22+
23+
private final StoreRepository storeRepository;
24+
private final VoucherOwnershipRepository voucherOwnershipRepository;
25+
private final UserRepository userRepository;
26+
27+
@Override
28+
public StoreBasicInfoResponseDto getStoreInfo(Long storeId) {
29+
Store store = storeRepository.findById(storeId)
30+
.orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND));
31+
return StoreBasicInfoResponseDto.from(store);
32+
}
33+
34+
@Override
35+
public VoucherPageResponseDto getAvailableVouchers(Long storeId, Long userId, Pageable pageable) {
36+
37+
getStoreInfo(storeId);
38+
39+
userRepository.findById(userId).orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));
40+
41+
Page<VoucherOwnership> page = voucherOwnershipRepository.findAvailableVouchersByUserAndStore(
42+
userId, storeId, pageable
43+
);
44+
45+
return VoucherPageResponseDto.from(page);
46+
}
47+
}

src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepository.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,21 @@ List<VoucherOwnership> findByStatusAndVoucherValidDateBeforeWithFetchJoin(
4141
@Param("now") LocalDateTime now
4242
);
4343

44+
@Query("""
45+
SELECT vo FROM VoucherOwnership vo
46+
JOIN vo.voucher v
47+
JOIN v.voucherStores vs
48+
WHERE vo.wallet.user.id = :userId
49+
AND vs.store.id = :storeId
50+
AND vo.status = 'AVAILABLE'
51+
AND vo.remainingAmount > 0
52+
AND v.validDate >= CURRENT_TIMESTAMP
53+
ORDER BY vo.remainingAmount DESC,v.validDate ASC
54+
""")
55+
Page<VoucherOwnership> findAvailableVouchersByUserAndStore(
56+
@Param("userId") Long userId,
57+
58+
@Param("storeId") Long storeId,
59+
Pageable pageable
60+
);
4461
}

0 commit comments

Comments
 (0)