Skip to content

Commit

Permalink
Merge pull request #231 from boostcampwm-2024/feature/api/news-#181
Browse files Browse the repository at this point in the history
[BE] 10.03 뉴스 조회 API 구현 #181
  • Loading branch information
uuuo3o authored Nov 29, 2024
2 parents a68295f + 93848c0 commit 9b60f8a
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 17 deletions.
23 changes: 17 additions & 6 deletions BE/src/news/news.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,17 @@ export class NewsService {
@Cron('*/1 8-16 * * 1-5')
async cronNewsData() {
await this.newsRepository.delete({ query: In(['증권', '주식']) });
await this.getNewsDataByQuery('주식');
await this.getNewsDataByQuery('증권');
const stockNews = await this.getNewsDataByQuery('주식');
const securityNews = await this.getNewsDataByQuery('증권');

const allNews = [...stockNews, ...securityNews];
const uniqueNews = allNews.filter(
(news, index) =>
allNews.findIndex((i) => i.originallink === news.originallink) ===
index,
);

await this.newsRepository.save(uniqueNews);
await this.newsRepository.update(
{},
{
Expand All @@ -67,13 +75,16 @@ export class NewsService {

const response =
await this.naverApiDomainService.requestApi<NewsApiResponse>(queryParams);
const formattedData = this.formatNewsData(value, response.items);

return this.newsRepository.save(formattedData);
return this.newsRepository.save(this.formatNewsData(value, response.items));
}

private formatNewsData(query: string, items: NewsDataOutputDto[]) {
return items.slice(0, 10).map((item) => {
const uniqueItems = items.filter(
(item, index) =>
items.findIndex((i) => i.originallink === item.originallink) === index,
);

return uniqueItems.slice(0, 10).map((item) => {
const result = new NewsItemDataDto();

result.title = this.htmlEncode(item.title);
Expand Down
2 changes: 1 addition & 1 deletion BE/src/ranking/ranking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class RankingService {
};
}

@Cron('0 16 * * 1-5')
@Cron('*/1 8-16 * * 1-5')
async updateRanking() {
const [profitRateRanking, assetRanking] = await Promise.all([
this.calculateRanking(SortType.PROFIT_RATE),
Expand Down
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,48 @@
- 친구들과 함께 성장하는 재미를 느껴보세요.
- 경쟁하고 정보도 나누며 더 즐겁게 배워보세요.

---
## ♥️ 사이트 이용방법
[사이트](https://juga.kro.kr/)에 접속하신 후에,
ID: **jindding** / PW: **1234** 를 입력해주시면 매수 등 로그인 후 사용가능한 서비스를 이용하실 수 있습니다.
### [🚀 시작하기](https://juga.kro.kr/)

현재 카카오로 로그인하기 기능은 심사 대기 중으로 사용이 불가능합니다.
> 위의 시작하기를 누르면 사이트로 이동됩니다.
>
### 테스트 계정

- ID: **jindding**
- Password: **1234**

### 주의사항

- 카카오 로그인 기능은 현재 심사 중으로 사용이 불가능합니다.
- 실제 금전적 거래는 이루어지지 않는 모의투자 서비스입니다.

## ⭐️ 프로젝트 기능 소개

### 주식 차트
![화면 기록 2024-11-28 오후 6 40 30](https://github.com/user-attachments/assets/6d36b0d9-2db2-4018-a7f3-2c12fb586fd0)

- 일, 별, 월, 년 단위로 주식 차트를 확인할 수 있습니다.
- 이동평균선 정보를 활용해 해당 주식의 추이를 더 자세히 확인할 수 있습니다.
- 라이브러리를 사용하지 않고 canvas를 활용해 직접 구현했습니다.

### 로그인
![image (14)](https://github.com/user-attachments/assets/9968ef08-cbf8-41fd-bfdc-8ca25dd8d80c)

- 로그인 모달창에서 로그인을 할 수 있습니다.
- 카카오 oAuth 로그인으로 간편하게 로그인할 수 있습니다.


### 랭킹
![image (15)](https://github.com/user-attachments/assets/251821a9-63d9-4f23-9178-2f8f3d8c608d)

- 하루 단위로 랭킹이 갱신됩니다.
- 수익률순, 자산순을 기준으로 랭킹을 확인할 수 있습니다.
- 자신의 오늘 랭킹을 확인할 수 있습니다.

## 🗂️ 기술 스택
<div align="center">
<img width="800" alt="기술 스택" src="https://github.com/user-attachments/assets/d58700ea-8bfe-459f-8b67-d0864bf76693">
</div>

## 🏛️ 소프트웨어 아키텍처
![아키텍처 2 0](https://github.com/user-attachments/assets/e0c33dfc-7495-48bf-ba4a-5fd911f66f9a)
<img width="2336" alt="소프트웨어 아키텍처 3 0" src="https://github.com/user-attachments/assets/3e4d5e3c-3fc5-44a5-8a8e-77bd704e22f2">


## 🧑🏻 팀원 소개
| 🖥️ Web FE | ⚙️ Web BE | ⚙️ Web BE | 🖥️ Web FE | ⚙️ Web BE |
Expand Down
Binary file not shown.
157 changes: 157 additions & 0 deletions scripts/stress-test.script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from locust import HttpUser, task, between, events
import random
from collections import defaultdict
import logging
from typing import Dict, Set
from datetime import datetime

# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 글로벌 통계 저장소
class Stats:
ip_server_mapping: Dict[str, Set[str]] = defaultdict(set)
request_counts: Dict[str, int] = defaultdict(int)

@classmethod
def get_summary(cls):
summary = []
for ip, servers in cls.ip_server_mapping.items():
summary.append({
'ip': ip,
'servers': list(servers),
'request_count': cls.request_counts[ip],
'is_sticky': len(servers) == 1
})
return summary
class IPHashTestUser(HttpUser):
host = "https://juga.kro.kr"
wait_time = between(1, 3)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ip = f"{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}"

def on_start(self):
headers = {
"X-Forwarded-For": self.ip ,
"Content-Type": "application/json"
}

with self.client.post(
"/api/auth/login",
json={"email": "jindding", "password": "1234"},
headers=headers,
name=f"Login Test ({self.ip})"
) as response:
if response.status_code == 200:
logger.info(f"Login success for IP {self.ip}")
else:
logger.warning(f"Login failed for IP {self.ip}")

@task(3)
def buy_stock(self):
headers = {
"X-Forwarded-For": self.ip ,
"Content-Type": "application/json"
}

with self.client.post(
"/api/stocks/order/buy",
headers = headers,
json = {
"stock_code": "005930",
"price": 55400,
"amount": 1
},
catch_response=True,
name=f"API Test ({self.ip})"
) as response:
if response.status_code == 201:
logger.info(f"Buy success for IP {self.ip}")
response.success()
else:
logger.warning(f"Buy failed for IP {self.ip}")
response.failure(f"Status code: {response.status_code}")


@task(1)
def sell_stock(self):
headers = {
"X-Forwarded-For": self.ip ,
"Content-Type": "application/json"
}

with self.client.post(
"/api/stocks/order/sell",
headers = headers,
json = {
"stock_code": "005930",
"price": 55400,
"amount": 1
},
catch_response=True,
name=f"API Test ({self.ip})"
) as response:
if response.status_code == 200:
logger.info(f"Sell success for IP {self.ip}")
response.success()
else:
logger.warning(f"Sell failed for IP {self.ip}")
response.failure(f"Status code: {response.status_code}")



@task(1)
def test_api_endpoint(self):
headers = {
"X-Forwarded-For": self.ip # X-Real-IP 제거
}

try:
with self.client.get(
"/api/ranking",
headers=headers,
catch_response=True,
name=f"API Test ({self.ip})"
) as response:
# 통계 업데이트
server_id = response.headers.get('X-Served-By', 'unknown')
Stats.ip_server_mapping[self.ip].add(server_id)
Stats.request_counts[self.ip] += 1

# 응답 로깅
if response.status_code == 200:
logger.debug(f"Success - IP: {self.ip}, Server: {server_id}")
response.success()
else:
logger.warning(f"Failed - IP: {self.ip}, Status: {response.status_code}")
response.failure(f"Status code: {response.status_code}")

except Exception as e:
logger.error(f"Request failed for IP {self.ip}: {str(e)}")

@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
"""테스트 종료 시 상세한 통계 출력"""
logger.info("\n=== Load Balancing Test Results ===")
logger.info(f"Test completed at: {datetime.now()}")

summary = Stats.get_summary()

# 통계 출력
sticky_count = sum(1 for item in summary if item['is_sticky'])
total_ips = len(summary)

logger.info(f"\nTotal unique IPs tested: {total_ips}")
logger.info(f"IPs with sticky sessions: {sticky_count}")
logger.info(f"Sticky session percentage: {(sticky_count/total_ips)*100:.2f}%\n")

# 상세 결과
logger.info("Detailed Results:")
for item in summary:
logger.info(f"IP: {item['ip']}")
logger.info(f" - Servers: {', '.join(item['servers'])}")
logger.info(f" - Requests: {item['request_count']}")
logger.info(f" - Sticky: {'Yes' if item['is_sticky'] else 'No'}\n")

0 comments on commit 9b60f8a

Please sign in to comment.