Skip to content

Commit 2f34745

Browse files
committed
feat: 줄서기 기능 개선
1 parent a48e293 commit 2f34745

4 files changed

Lines changed: 127 additions & 184 deletions

File tree

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package com.waitit.capstone.domain.manager;
22

3-
import java.util.List;
4-
import java.util.Optional;
3+
import org.springframework.data.domain.Pageable;
54
import org.springframework.data.jpa.repository.JpaRepository;
65
import org.springframework.stereotype.Repository;
76

7+
import java.util.List;
8+
import java.util.Optional;
9+
810
@Repository
911
public interface HostRepository extends JpaRepository<Host,Long> {
1012
Optional<Host> findHostById(Long id);
1113
List<Host> findAllByIsActive(boolean isActive);
14+
15+
/**
16+
* [추가] 가장 최근에 생성된 Host를 count만큼 조회합니다.
17+
*/
18+
List<Host> findTopByOrderByIdDesc(Pageable pageable);
1219
}

src/main/java/com/waitit/capstone/domain/manager/service/HostService.java

Lines changed: 34 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -10,92 +10,57 @@
1010
import com.waitit.capstone.domain.manager.dto.SessionListDto;
1111
import com.waitit.capstone.domain.manager.dto.WaitingListDto;
1212
import com.waitit.capstone.domain.queue.dto.QueueDto;
13+
import com.waitit.capstone.domain.queue.service.QueueService; // QueueService 임포트
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.data.domain.PageRequest;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
18+
import org.springframework.web.multipart.MultipartFile;
19+
1320
import java.io.IOException;
1421
import java.time.LocalDateTime;
15-
import java.util.ArrayList;
16-
import java.util.Collection;
1722
import java.util.List;
1823
import java.util.Set;
1924
import java.util.stream.Collectors;
20-
import lombok.AllArgsConstructor;
21-
import org.redisson.api.RBatch;
22-
import org.redisson.api.RScoredSortedSet;
23-
import org.redisson.api.RSet;
24-
import org.redisson.api.RedissonClient;
25-
import org.redisson.codec.JsonJacksonCodec;
26-
import org.springframework.data.redis.core.StringRedisTemplate;
27-
import org.springframework.stereotype.Service;
28-
import org.springframework.transaction.annotation.Transactional;
29-
import org.springframework.web.multipart.MultipartFile;
3025

31-
@AllArgsConstructor
26+
@RequiredArgsConstructor // @AllArgsConstructor -> @RequiredArgsConstructor 변경
3227
@Service
3328
public class HostService {
3429
private final HostRepository hostRepository;
35-
private final StringRedisTemplate redisTemplate;
3630
private final HostMapper hostMapper;
3731
private final ImageService imageService;
38-
private final RedissonClient redissonClient;
39-
private static final String ACTIVE_HOSTS_KEY = "active:hosts";
40-
private static final String SORTED_HOSTS_KEY = "sorted:hosts";
41-
42-
private String getWaitListKey(Long hostId) {
43-
return "waitList:" + hostId;
44-
}
32+
private final QueueService queueService; // Redisson 대신 QueueService 주입
4533

46-
//호스트 정보 저장
4734
@Transactional
4835
public void saveHost(HostRequest request, List<MultipartFile> hostImages) throws IOException {
49-
5036
Host host = hostMapper.toEntity(request);
5137
Host saved = hostRepository.save(host);
5238

5339
if (hostImages != null) {
5440
for (MultipartFile file : hostImages) {
55-
// imageService 에서 HostImage 엔티티 생성 & 파일 저장, DB에는 아직 영속화 안 됨
5641
HostImage img = imageService.uploadHost(saved.getId(), file);
57-
// Host.addImage 으로 images 리스트에 추가(양방향 세팅)
5842
saved.addImage(img);
5943
}
60-
// cascade = ALL 이므로 save 한번 더 해 주면 이미지도 같이 저장
6144
hostRepository.save(saved);
6245
}
63-
64-
// 1. 활성 호스트 Set에 호스트 ID 추가
65-
RSet<Long> activeHosts = redissonClient.getSet(ACTIVE_HOSTS_KEY);
66-
activeHosts.add(host.getId());
67-
68-
// 2. 최신 정렬용 ZSet에 추가 (Score: 현재 시간)
69-
RScoredSortedSet<Long> sortedHosts = redissonClient.getScoredSortedSet(SORTED_HOSTS_KEY);
70-
sortedHosts.add(System.currentTimeMillis(), host.getId());
46+
// Redis 관련 로직 모두 삭제
7147
}
7248

73-
// 호스트 세션 비활성화
7449
@Transactional
7550
public void deactivateHost(Long hostId) {
76-
// RBatch를 사용해 여러 명령을 원자적으로 실행
77-
RBatch batch = redissonClient.createBatch();
78-
79-
// 1. 활성 호스트 Set에서 호스트 ID 제거
80-
batch.getSet(ACTIVE_HOSTS_KEY).removeAsync(hostId);
81-
// 2. 정렬용 Set에서도 호스트 ID 제거
82-
batch.getScoredSortedSet(SORTED_HOSTS_KEY).removeAsync(hostId);
83-
// 3. 해당 호스트의 대기열 데이터도 삭제
84-
batch.getScoredSortedSet(getWaitListKey(hostId)).deleteAsync();
85-
86-
batch.execute();
51+
// 인메모리 대기열에서 해당 가게의 큐를 제거
52+
queueService.deactivateQueue(hostId);
8753
}
8854

89-
//요청받은 아이디로 db에 호스트 조회
9055
public HostResponse getHost(Long id) {
9156
Host host = hostRepository.findHostById(id)
9257
.orElseThrow(() -> new IllegalArgumentException("Host not found with id: " + id));
9358
return hostMapper.hostToDto(host);
9459
}
9560

9661
public List<SessionListDto> getAllSessions() {
97-
RSet<Long> activeHosts = redissonClient.getSet(ACTIVE_HOSTS_KEY);
98-
Set<Long> activeIds = activeHosts.readAll();
62+
// 활성화된 가게 ID 목록을 QueueService에서 가져옴
63+
Set<Long> activeIds = queueService.getActiveHostIds();
9964

10065
if (activeIds.isEmpty()) {
10166
return List.of();
@@ -104,10 +69,14 @@ public List<SessionListDto> getAllSessions() {
10469
List<Host> hosts = hostRepository.findAllById(activeIds);
10570

10671
return hosts.stream().map(host -> {
107-
String imgUrl = host.getImages().stream().findFirst().map(HostImage::getImagePath).orElse(null);
72+
String imgUrl = host.getImages().stream()
73+
.filter(HostImage::isRepresentative)
74+
.findFirst()
75+
.map(HostImage::getImagePath)
76+
.orElse(host.getImages().isEmpty() ? null : host.getImages().get(0).getImagePath());
10877

109-
// 대기열 크기를 RScoredSortedSet의 size()로 조회
110-
int waiting = redissonClient.getScoredSortedSet(getWaitListKey(host.getId())).size();
78+
// 대기열 크기를 QueueService에서 조회
79+
int waiting = queueService.getQueueByHostId(host.getId()).size();
11180

11281
return SessionListDto.builder()
11382
.hostId(host.getId())
@@ -119,50 +88,30 @@ public List<SessionListDto> getAllSessions() {
11988
}).collect(Collectors.toList());
12089
}
12190

122-
123-
// 예상 시간 계산 메소드
12491
private String calculateEstimatedTime(LocalDateTime startTime, LocalDateTime endTime) {
92+
// 이 부분은 비즈니스 로직에 따라 구현 필요
12593
return null;
12694
}
12795

128-
//웨이팅 리스트 조회
12996
public List<WaitingListDto> getQueueListByHostId(Long hostId) {
130-
String key = getWaitListKey(hostId);
131-
132-
// Codec을 사용하여 QueueDto 객체로 직접 작업
133-
RScoredSortedSet<QueueDto> queue = redissonClient.getScoredSortedSet(key, new JsonJacksonCodec(
134-
QueueDto.class.getClassLoader()));
135-
136-
// 모든 대기열 멤버(QueueDto 객체)를 Score 순서대로 가져옴
137-
Collection<QueueDto> dtoList = queue.readAll();
138-
139-
return hostMapper.queueToWaiting(new ArrayList<>(dtoList));
97+
// 대기열 목록을 QueueService에서 조회
98+
List<QueueDto> dtoList = queueService.getQueueByHostId(hostId);
99+
return hostMapper.queueToWaiting(dtoList);
140100
}
141101

142-
//트렌드 호스트
143102
public List<SessionListDto> findTrendHost(int count) {
144-
RScoredSortedSet<Long> sortedHosts = redissonClient.getScoredSortedSet(SORTED_HOSTS_KEY);
145-
146-
// 1. 결과를 담을 비어있는 List를 먼저 생성합니다.
147-
List<Long> latestHostIds = new ArrayList<>();
148-
149-
// 2. revRangeTo 메소드를 호출하여 위에서 만든 List에 결과를 채워넣습니다.
150-
sortedHosts.revRangeTo(latestHostIds.toString(), 0, count - 1);
151-
152-
if (latestHostIds.isEmpty()) {
153-
return List.of();
154-
}
103+
// Redis 대신 DB에서 최신순으로 가게를 조회하는 로직으로 변경
104+
List<Host> latestHosts = hostRepository.findTopByOrderByIdDesc(PageRequest.of(0, count));
155105

156-
List<Host> hosts = hostRepository.findAllById(latestHostIds);
106+
return latestHosts.stream().map(host -> {
107+
String imgUrl = host.getImages().stream()
108+
.filter(HostImage::isRepresentative)
109+
.findFirst()
110+
.map(HostImage::getImagePath)
111+
.orElse(host.getImages().isEmpty() ? null : host.getImages().get(0).getImagePath());
157112

158-
// Redis에서 조회한 순서대로 DB 조회 결과를 정렬
159-
List<Host> sorted = latestHostIds.stream()
160-
.flatMap(id -> hosts.stream().filter(h -> h.getId().equals(id)))
161-
.toList();
113+
int waiting = queueService.getQueueByHostId(host.getId()).size();
162114

163-
return sorted.stream().map(host -> {
164-
String imgUrl = host.getImages().stream().findFirst().map(HostImage::getImagePath).orElse(null);
165-
int waiting = redissonClient.getScoredSortedSet(getWaitListKey(host.getId())).size();
166115
return SessionListDto.builder()
167116
.hostId(host.getId())
168117
.hostName(host.getHostName())

src/main/java/com/waitit/capstone/domain/queue/controller/QueueController.java

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,10 @@
99
import io.swagger.v3.oas.annotations.Operation;
1010
import io.swagger.v3.oas.annotations.tags.Tag;
1111
import jakarta.servlet.http.HttpServletRequest;
12-
import java.util.concurrent.Executors;
13-
import java.util.concurrent.ScheduledExecutorService;
14-
import java.util.concurrent.TimeUnit;
1512
import lombok.RequiredArgsConstructor;
1613
import org.springframework.http.HttpStatus;
1714
import org.springframework.http.ResponseEntity;
18-
import org.springframework.web.bind.annotation.DeleteMapping;
19-
import org.springframework.web.bind.annotation.GetMapping;
20-
import org.springframework.web.bind.annotation.PathVariable;
21-
import org.springframework.web.bind.annotation.PostMapping;
22-
import org.springframework.web.bind.annotation.RequestBody;
23-
import org.springframework.web.bind.annotation.RequestMapping;
24-
import org.springframework.web.bind.annotation.RequestParam;
25-
import org.springframework.web.bind.annotation.RestController;
26-
import org.springframework.web.context.request.async.DeferredResult;
15+
import org.springframework.web.bind.annotation.*;
2716

2817
@RestController
2918
@RequestMapping("/queue")
@@ -33,53 +22,33 @@ public class QueueController {
3322
private final QueueService queueService;
3423
private final QueueMapper queueMapper;
3524

36-
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
37-
// 최대 대기 시간 (ms)
38-
private static final long TIMEOUT = 30_000L; // 30초
39-
private static final long POLL_INTERVAL = 1_000L; // 1초
25+
// 롱폴링 관련 코드 모두 삭제
4026

4127
@Operation(summary = "대기열 등록", description = "특정 가게의 대기열에 사용자를 등록합니다.")
4228
@PostMapping("/{id}")
4329
public ResponseEntity<?> registerQueue(@PathVariable Long id, @RequestBody QueueRequest queueRequest, HttpServletRequest request){
44-
4530
String token = request.getHeader("access");
4631
QueueDto dto = queueMapper.requestToDto(queueRequest);
47-
int index = 0;
48-
index = queueService.registerQueue(id,dto);
49-
32+
int index = queueService.registerQueue(id,dto);
5033

51-
QueResponseDto responseDto = new QueResponseDto("대기열 등록 완료",index);
34+
QueResponseDto responseDto = new QueResponseDto("대기열 등록 완료", index);
5235
return ResponseEntity.status(HttpStatus.OK).body(responseDto);
5336
}
5437

55-
@Operation(summary = "내 대기 순번 확인 (Long Polling)", description = "자신의 대기 순번을 확인합니다. 순번 변경이 있을 때까지 연결을 유지합니다.")
38+
/**
39+
* [수정] 롱폴링을 제거하고, 현재 대기 순번을 즉시 반환하도록 변경
40+
*/
41+
@Operation(summary = "내 대기 순번 즉시 확인", description = "자신의 현재 대기 순번을 즉시 확인합니다.")
5642
@GetMapping("/{id}/position/")
57-
public DeferredResult<ResponseEntity<QueResponseDto>> getMyPosition(
43+
public ResponseEntity<QueResponseDto> getMyPosition(
5844
@PathVariable Long id,
59-
@RequestBody QueueRequest queueRequest,
60-
@RequestParam(name = "lastPos", required = false) Integer lastPos) {
45+
@RequestBody QueueRequest queueRequest) {
46+
6147
QueueDto dto = queueMapper.requestToDto(queueRequest);
62-
int initialPos = queueService.getMyPosition(id, dto);
63-
int clientLast = (lastPos != null ? lastPos : initialPos);
64-
65-
DeferredResult<ResponseEntity<QueResponseDto>> result = new DeferredResult<>(TIMEOUT);
48+
int currentPosition = queueService.getMyPosition(id, dto);
6649

67-
result.onTimeout(() -> {
68-
QueResponseDto body = new QueResponseDto("변동 없음(타임아웃)", clientLast);
69-
result.setResult(ResponseEntity.ok(body));
70-
});
71-
72-
scheduler.scheduleAtFixedRate(() -> {
73-
if (result.isSetOrExpired()) return;
74-
75-
int current = queueService.getMyPosition(id, dto);
76-
if (current != clientLast) {
77-
QueResponseDto body = new QueResponseDto("순번 변경!", current);
78-
result.setResult(ResponseEntity.ok(body));
79-
}
80-
}, 0, POLL_INTERVAL, TimeUnit.MILLISECONDS);
81-
82-
return result;
50+
QueResponseDto responseDto = new QueResponseDto("현재 대기 순번입니다.", currentPosition);
51+
return ResponseEntity.ok(responseDto);
8352
}
8453

8554
@Operation(summary = "대기열 등록 취소", description = "대기열 등록을 취소합니다.")
@@ -111,5 +80,4 @@ public ResponseEntity<?> admitFromPostpone(
11180
queueService.deletePostpone(id, dto);
11281
return ResponseEntity.status(HttpStatus.OK).body("미루기 큐에서 입장 처리되었습니다.");
11382
}
114-
11583
}

0 commit comments

Comments
 (0)