From 04239ae004c56f648cd50f405f1027fff5d2fabd Mon Sep 17 00:00:00 2001 From: jindding Date: Thu, 28 Nov 2024 16:03:38 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20feat:=20locust=20=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EB=B6=80=ED=95=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stress-test.script.cpython-311.pyc | Bin 0 -> 10029 bytes scripts/stress-test.script.py | 157 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 scripts/__pycache__/stress-test.script.cpython-311.pyc create mode 100644 scripts/stress-test.script.py diff --git a/scripts/__pycache__/stress-test.script.cpython-311.pyc b/scripts/__pycache__/stress-test.script.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02e72fddc6ae4ab73d6cfb3a2064e53678e2a8a2 GIT binary patch literal 10029 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 literal 0 HcmV?d00001 diff --git a/scripts/stress-test.script.py b/scripts/stress-test.script.py new file mode 100644 index 00000000..cfd73e45 --- /dev/null +++ b/scripts/stress-test.script.py @@ -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") \ No newline at end of file From d64715d17108c227f2f6f404e22d8227b76e55ed Mon Sep 17 00:00:00 2001 From: dannysir <48199716+dannysir@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:51:55 +0900 Subject: [PATCH 2/8] Update README.md --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e63d72f..9739d6c4 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,20 @@ - 친구들과 함께 성장하는 재미를 느껴보세요. - 경쟁하고 정보도 나누며 더 즐겁게 배워보세요. ---- -## ♥️ 사이트 이용방법 -[사이트](https://juga.kro.kr/)에 접속하신 후에, -ID: **jindding** / PW: **1234** 를 입력해주시면 매수 등 로그인 후 사용가능한 서비스를 이용하실 수 있습니다. +### [🚀 시작하기](https://juga.kro.kr/) + +> 위의 시작하기를 누르면 사이트로 이동됩니다. +> + +### 테스트 계정 + +- ID: **jindding** +- Password: **1234** + +### 주의사항 -현재 카카오로 로그인하기 기능은 심사 대기 중으로 사용이 불가능합니다. +- 카카오 로그인 기능은 현재 심사 중으로 사용이 불가능합니다. +- 실제 금전적 거래는 이루어지지 않는 모의투자 서비스입니다. ## 🗂️ 기술 스택
From c5b90b72ff4e9ef10f0a0680c60971e3e4a6ef8e Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 28 Nov 2024 18:11:44 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20=EC=8B=9C?= =?UTF-8?q?=EC=97=B0=EC=97=90=EC=84=9C=20=EB=9E=AD=ED=82=B9=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=EB=A5=BC=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=20=EC=9C=84=ED=95=B4=20cron=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=20=EC=8B=9C=EA=B0=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index b7559f8f..aa13a8b7 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -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), From 5ed762de79f1b6a4ee8ad59b8889ccd28c97d3a0 Mon Sep 17 00:00:00 2001 From: Dongwoo Ko <68095803+dongree@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:53:27 +0900 Subject: [PATCH 4/8] Update README.md --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 9739d6c4..43c79dca 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,29 @@ - 카카오 로그인 기능은 현재 심사 중으로 사용이 불가능합니다. - 실제 금전적 거래는 이루어지지 않는 모의투자 서비스입니다. +## ⭐️ 프로젝트 기능 소개 + +### 주식 차트 +![화면 기록 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) + +- 하루 단위로 랭킹이 갱신됩니다. +- 수익률순, 자산순을 기준으로 랭킹을 확인할 수 있습니다. +- 자신의 오늘 랭킹을 확인할 수 있습니다. + + ## 🗂️ 기술 스택
기술 스택 From 37f06c0b773dac591d8414e48963641e136e8d42 Mon Sep 17 00:00:00 2001 From: Dongwoo Ko <68095803+dongree@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:54:32 +0900 Subject: [PATCH 5/8] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 43c79dca..a38eede5 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ ### 로그인 ![image (14)](https://github.com/user-attachments/assets/9968ef08-cbf8-41fd-bfdc-8ca25dd8d80c) +- 로그인 모달창에서 로그인을 할 수 있습니다. - 카카오 oAuth 로그인으로 간편하게 로그인할 수 있습니다. From d3bfca15d5bbcaee8dce354d4a58f4bab712e272 Mon Sep 17 00:00:00 2001 From: JIN <80706216+uuuo3o@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:59:16 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=EC=86=8C=ED=94=84?= =?UTF-8?q?=ED=8A=B8=EC=9B=A8=EC=96=B4=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a38eede5..0017653b 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@
## 🏛️ 소프트웨어 아키텍처 -![아키텍처 2 0](https://github.com/user-attachments/assets/e0c33dfc-7495-48bf-ba4a-5fd911f66f9a) +소프트웨어 아키텍처 3 0 + ## 🧑🏻 팀원 소개 | 🖥️ Web FE | ⚙️ Web BE | ⚙️ Web BE | 🖥️ Web FE | ⚙️ Web BE | From 332aa16214ddd57cbcf0b0b68cb81374d258c23e Mon Sep 17 00:00:00 2001 From: dannysir <48199716+dannysir@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:21:21 +0900 Subject: [PATCH 7/8] Update README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 0017653b..b55a23c6 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,6 @@ - 자신의 오늘 랭킹을 확인할 수 있습니다. -## 🗂️ 기술 스택 -
- 기술 스택 -
- ## 🏛️ 소프트웨어 아키텍처 소프트웨어 아키텍처 3 0 From 93848c0758576b17578b4a2062fa915016c674bb Mon Sep 17 00:00:00 2001 From: JIN Date: Fri, 29 Nov 2024 10:30:01 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=9C=20=EB=A7=81=ED=81=AC=EB=A5=BC=20=EB=84=A3=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EA=B2=8C=20=EC=A4=91=EB=B3=B5=EC=9D=84=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80#181?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/news/news.service.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/BE/src/news/news.service.ts b/BE/src/news/news.service.ts index 4d05db80..6fff8b84 100644 --- a/BE/src/news/news.service.ts +++ b/BE/src/news/news.service.ts @@ -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( {}, { @@ -67,13 +75,16 @@ export class NewsService { const response = await this.naverApiDomainService.requestApi(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);