Skip to content

Commit f59eca7

Browse files
authored
Merge pull request #172 from CleanEngine/feat/chart-api
feat: 차트 API 페이징 처리
2 parents bf6857c + 609a495 commit f59eca7

File tree

5 files changed

+200
-6
lines changed

5 files changed

+200
-6
lines changed
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
package com.cleanengine.coin.chart.controller;
22

33
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
4-
import com.cleanengine.coin.chart.service.minute.MinuteOhlcDataService;
4+
import com.cleanengine.coin.chart.service.minute.PagingMinuteOhlcDataService;
55
import lombok.RequiredArgsConstructor;
66
import org.springframework.http.ResponseEntity;
77
import org.springframework.web.bind.annotation.*;
88

9+
import java.time.LocalDateTime;
910
import java.util.List;
1011

1112
@RestController
1213
@RequestMapping("/api/minute-ohlc")
1314
@RequiredArgsConstructor
1415
public class MinuteOhlcDataController {
1516

16-
private final MinuteOhlcDataService service;
17+
private final PagingMinuteOhlcDataService service;
1718

1819
/**
19-
* GET /api/minute-ohlc?ticker=BTC
20-
* DB에 있는 과거 거래를 1분 단위로 묶어 OHLC+volume을 계산한 리스트 반환
20+
* GET /api/minute-ohlc?ticker=BTC&count=100&interval=1&from=2025-06-19T10:30
21+
* DB에 있는 과거 거래를 interval 단위로 묶어 OHLC+volume을 계산한 리스트 반환
2122
*/
2223
@GetMapping
2324
public ResponseEntity<List<RealTimeOhlcDto>> getMinuteOhlc(
24-
@RequestParam("ticker") String ticker
25+
@RequestParam("ticker") String ticker,
26+
@RequestParam(value = "count", defaultValue = "100") int count,
27+
@RequestParam(value = "interval", defaultValue = "1") int interval,
28+
@RequestParam(value = "from", required = false) LocalDateTime from
2529
) {
26-
List<RealTimeOhlcDto> data = service.getMinuteOhlcData(ticker);
30+
if (from == null) {
31+
from = LocalDateTime.now();
32+
}
33+
34+
List<RealTimeOhlcDto> data = service.getMinuteOhlcData(ticker, count, interval, from.minusMinutes(1));
2735
return ResponseEntity.ok(data);
2836
}
37+
2938
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.cleanengine.coin.chart.service.minute;
2+
3+
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
4+
5+
import java.time.LocalDateTime;
6+
import java.util.List;
7+
8+
public interface PagingMinuteOhlcDataService {
9+
10+
List<RealTimeOhlcDto> getMinuteOhlcData(String ticker, int count, int interval, LocalDateTime from);
11+
12+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.cleanengine.coin.chart.service.minute;
2+
3+
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
4+
import jakarta.persistence.EntityManager;
5+
import jakarta.persistence.Query;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Service;
8+
9+
import java.time.LocalDateTime;
10+
import java.time.format.DateTimeFormatter;
11+
import java.util.Collections;
12+
import java.util.Comparator;
13+
import java.util.List;
14+
import java.util.stream.Collectors;
15+
16+
@RequiredArgsConstructor
17+
@Service
18+
public class PagingMinuteOhlcDataServiceImpl implements PagingMinuteOhlcDataService {
19+
20+
private final EntityManager em;
21+
22+
@Override
23+
public List<RealTimeOhlcDto> getMinuteOhlcData(String ticker, int count, int interval, LocalDateTime from) {
24+
validateTicker(ticker);
25+
26+
String timeSlotQuery = """
27+
SELECT DISTINCT DATE_FORMAT(
28+
DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE),
29+
'%Y-%m-%d %H:%i:00') AS time_slot
30+
FROM trade
31+
WHERE ticker = :ticker AND trade_time <= :from
32+
GROUP BY DATE_FORMAT(
33+
DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE),
34+
'%Y-%m-%d %H:%i:00')
35+
ORDER BY time_slot DESC
36+
LIMIT :count
37+
""";
38+
39+
Query nativeQuery = em.createNativeQuery(timeSlotQuery);
40+
nativeQuery.setParameter("ticker", ticker);
41+
nativeQuery.setParameter("from", from);
42+
nativeQuery.setParameter("interval", interval);
43+
nativeQuery.setParameter("count", count);
44+
45+
@SuppressWarnings("unchecked")
46+
List<String> timeSlotResults = nativeQuery.getResultList();
47+
List<LocalDateTime> timeSlots = timeSlotResults.stream()
48+
.map(str -> LocalDateTime.parse(str, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
49+
.sorted(Comparator.naturalOrder())
50+
.toList();
51+
52+
if (timeSlots.isEmpty()) {
53+
return Collections.emptyList();
54+
}
55+
56+
String ohlcQuery = """
57+
SELECT
58+
:ticker AS ticker,
59+
t.time_slot AS timestamp,
60+
MAX(CASE WHEN rn_open = 1 THEN price END) AS open,
61+
MAX(price) AS high,
62+
MIN(price) AS low,
63+
MAX(CASE WHEN rn_close = 1 THEN price END) AS close,
64+
SUM(size) AS volume
65+
FROM (
66+
SELECT
67+
trade_time,
68+
price,
69+
size,
70+
DATE_FORMAT(
71+
DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE),
72+
'%Y-%m-%d %H:%i:00') AS time_slot,
73+
ROW_NUMBER() OVER (PARTITION BY DATE_FORMAT(
74+
DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE),
75+
'%Y-%m-%d %H:%i:00') ORDER BY trade_time) AS rn_open,
76+
ROW_NUMBER() OVER (PARTITION BY DATE_FORMAT(
77+
DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE),
78+
'%Y-%m-%d %H:%i:00') ORDER BY trade_time DESC) AS rn_close
79+
FROM trade
80+
WHERE ticker = :ticker
81+
AND DATE_FORMAT(
82+
DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE),
83+
'%Y-%m-%d %H:%i:00') IN (:timeSlots)
84+
) t
85+
GROUP BY t.time_slot
86+
ORDER BY t.time_slot
87+
""";
88+
89+
Query ohlcNativeQuery = em.createNativeQuery(ohlcQuery);
90+
ohlcNativeQuery.setParameter("ticker", ticker);
91+
ohlcNativeQuery.setParameter("interval", interval);
92+
ohlcNativeQuery.setParameter("timeSlots", timeSlotResults);
93+
94+
@SuppressWarnings("unchecked")
95+
List<Object[]> results = ohlcNativeQuery.getResultList();
96+
return results.stream()
97+
.map(row -> new RealTimeOhlcDto(
98+
(String) row[0], // ticker
99+
LocalDateTime.parse((String) row[1], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
100+
((Number) row[2]).doubleValue(), // open
101+
((Number) row[3]).doubleValue(), // high
102+
((Number) row[4]).doubleValue(), // low
103+
((Number) row[5]).doubleValue(), // close
104+
((Number) row[6]).doubleValue() // volume
105+
))
106+
.collect(Collectors.toList());
107+
}
108+
109+
static void validateTicker(String ticker) {
110+
if (ticker == null || ticker.trim().isEmpty()) {
111+
throw new IllegalArgumentException("티커는 비어있을 수 없습니다");
112+
}
113+
}
114+
115+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.cleanengine.coin.chart.service.minute;
2+
3+
import com.cleanengine.coin.base.MariaDBAdapterTest;
4+
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
import org.springframework.test.context.jdbc.Sql;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
import java.time.LocalDateTime;
13+
import java.util.List;
14+
15+
import static org.junit.jupiter.api.Assertions.*;
16+
17+
// 테스트 실행 전 MariaDBAdapterTest 클래스의 @DataJpaTest, @Disabled 어노테이션 주석처리 필요
18+
@DisplayName("차트 페이징 통합테스트")
19+
@Transactional
20+
@SpringBootTest
21+
public class PagingMinuteOhlcDataServiceImplTest extends MariaDBAdapterTest {
22+
23+
@Autowired
24+
private PagingMinuteOhlcDataService pagingMinuteOhlcDataService;
25+
26+
@DisplayName("페이징을 통해 차트 OHLC를 정상적으로 가져온다.")
27+
@Test
28+
@Sql("classpath:db/chart/paging_minute_ohlc_data.sql")
29+
public void getMinuteOhlcData() {
30+
// given
31+
String ticker = "TRUMP";
32+
int count = 1;
33+
int interval = 1;
34+
LocalDateTime from = LocalDateTime.of(2025, 6, 20, 12, 3, 0);
35+
36+
// when
37+
List<RealTimeOhlcDto> ohlcData = pagingMinuteOhlcDataService.getMinuteOhlcData(ticker, count, interval, from.minusMinutes(1));
38+
39+
// then
40+
assertNotNull(ohlcData);
41+
assertEquals(1, ohlcData.size());
42+
43+
RealTimeOhlcDto resultDto = ohlcData.getFirst();
44+
assertEquals("TRUMP", resultDto.getTicker());
45+
assertEquals(LocalDateTime.of(2025, 6, 20, 12, 2, 0), resultDto.getTimestamp());
46+
assertEquals(109500.0, resultDto.getOpen());
47+
assertEquals(110500.0, resultDto.getHigh());
48+
assertEquals(109300.0, resultDto.getLow());
49+
assertEquals(110000.0, resultDto.getClose());
50+
assertEquals(12.541453, resultDto.getVolume(), 0.000001);
51+
}
52+
53+
}

src/test/resources/db/chart/paging_minute_ohlc_data.sql

Lines changed: 5 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)