forked from boostcampwm-2024/web17-juchumjuchum
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #40 from boostcampwm-2024/task-#37-news-write-read
Task #37 종목별 뉴스 쓰기/조회구현, 프런트 구현
- Loading branch information
Showing
12 changed files
with
581 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
# 서비스 설명서 | ||
|
||
## 1. 서비스 구조 | ||
|
||
### 1.1 StockService | ||
|
||
- **역할**: 주식 기본 정보 관리 | ||
- **주요 기능**: | ||
- 주식 검색 | ||
- 인기 주식 순위 관리 | ||
- 사용자 관심 주식 관리 | ||
- 조회수 관리 | ||
- **관련 테이블**: stock, user_stock | ||
|
||
### 1.2 StockDetailService | ||
|
||
- **역할**: 주식 상세 정보 관리 | ||
- **주요 기능**: | ||
- 시가총액, EPS, PER 정보 제공 | ||
- 52주 최고가/최저가 정보 제공 | ||
- **관련 테이블**: stock_detail | ||
|
||
### 1.3 KoreaStockInfoService | ||
|
||
- **역할**: 주식 마스터 데이터 수집 | ||
- **실행 시간**: 매일 오전 9시, 자정 (월-금) | ||
- **주요 기능**: | ||
- KOSDAQ/KOSPI 마스터 데이터 다운로드 | ||
- 주식 기본 정보 업데이트 | ||
- **관련 테이블**: stock | ||
|
||
### 1.4 OpenapiLiveDataService | ||
|
||
- **역할**: 실시간 주가 데이터 수집 | ||
- **실행 주기**: 1분 | ||
- **주요 기능**: | ||
- 실시간 가격 정보 수집 | ||
- 거래량 데이터 수집 | ||
- 등락률 계산 | ||
- **관련 테이블**: stock_live_data | ||
|
||
## 2. 크론 작업 | ||
|
||
### 2.1 데이터 수집 | ||
|
||
| 작업 | 실행 시간 | 담당 서비스 | | ||
| ------------------ | -------------------- | --------------------------- | | ||
| 마스터 데이터 수집 | 09:00, 00:00 (월-금) | KoreaStockInfoService | | ||
| 실시간 데이터 수집 | 매 1분 (09:00-15:30) | OpenapiLiveDataService | | ||
| 일일 통계 집계 | 15:40 (장 마감 후) | StockDataAggregationService | | ||
|
||
### 2.2 데이터 정리 | ||
|
||
| 작업 | 실행 시간 | 설명 | | ||
| ------------------ | ---------- | -------------------------- | | ||
| 실시간 데이터 정리 | 00:00 매일 | 24시간 이상 된 데이터 삭제 | | ||
| 캐시 정리 | 매시 정각 | 만료된 캐시 삭제 | | ||
| 임시 파일 정리 | 03:00 매일 | 다운로드된 임시 파일 삭제 | | ||
|
||
## 3. API 상세 | ||
|
||
### 3.1 주식 정보 조회 | ||
|
||
GET /stock | ||
|
||
- 설명: 주식명으로 검색 | ||
- 권한: 없음 | ||
- 캐시: 1시간 | ||
- 응답시간: < 100ms | ||
|
||
GET /stock/top | ||
|
||
- 설명: 인기/상승/하락 주식 조회 | ||
- 권한: 없음 | ||
- 캐시: 5분 | ||
- 응답시간: < 200ms | ||
|
||
GET /stock/:stockId/detail | ||
|
||
- 설명: 주식 상세 정보 조회 | ||
- 권한: 없음 | ||
- 캐시: 60초 | ||
- 응답시간: < 150ms | ||
|
||
### 3.2 사용자 기능 | ||
|
||
POST /stock/user | ||
|
||
- 설명: 관심 주식 추가 | ||
- 권한: 로그인 필요 | ||
- 제한: 최대 100개 | ||
|
||
DELETE /stock/user | ||
|
||
- 설명: 관심 주식 제거 | ||
- 권한: 로그인 필요 | ||
- 검증: 소유권 확인 | ||
|
||
## 4. 데이터베이스 상세 | ||
|
||
### 4.1 테이블 구조 | ||
|
||
stock | ||
|
||
- stock_id (PK): 종목코드 | ||
- name: 종목명 | ||
- views: 조회수 | ||
- is_trading: 거래가능여부 | ||
- group_code: 그룹코드 | ||
|
||
stock_detail | ||
|
||
- id (PK): Auto Increment | ||
- stock_id (FK): 종목코드 | ||
- market_cap: 시가총액 | ||
- eps: EPS | ||
- per: PER | ||
- high_52w: 52주 최고가 | ||
- low_52w: 52주 최저가 | ||
- updated_at: 갱신일시 | ||
|
||
### 4.2 인덱스 전략 | ||
|
||
- **조회 성능 최적화**: | ||
- stock_views_idx: (views DESC, name ASC) | ||
- stock_detail_latest_idx: (stock_id, updated_at DESC) | ||
- **검색 성능 최적화**: | ||
- stock_name_idx: FULLTEXT(name) | ||
|
||
## 5. 캐싱 상세 | ||
|
||
### 5.1 API 캐시 | ||
|
||
| 엔드포인트 | 캐시 시간 | 키 패턴 | | ||
| ----------------- | --------- | ---------------------------- | | ||
| /stock/top | 5분 | `stock:top:{sortBy}:{limit}` | | ||
| /stock/:id/detail | 60초 | `stock:detail:{stockId}` | | ||
| /stock/index | 30초 | `stock:index` | | ||
|
||
### 5.2 데이터베이스 캐시 | ||
|
||
| 쿼리 종류 | 캐시 시간 | 갱신 조건 | | ||
| ------------- | --------- | ---------- | | ||
| 인기 검색어 | 1시간 | 수동 갱신 | | ||
| 차트 데이터 | 1일 | 장 마감 후 | | ||
| 종목 상세정보 | 60초 | 자동 갱신 | | ||
|
||
## 6. 에러 처리 | ||
|
||
### 6.1 에러 코드 | ||
|
||
| 코드 | 설명 | 대응 방안 | | ||
| ---- | -------------- | -------------------------- | | ||
| 400 | 잘못된 요청 | 요청 파라미터 검증 | | ||
| 401 | 인증 필요 | 로그인 페이지로 리다이렉트 | | ||
| 404 | 리소스 없음 | 사용자에게 메시지 표시 | | ||
| 429 | 요청 횟수 초과 | 잠시 후 재시도 안내 | | ||
|
||
### 6.2 로깅 정책 | ||
|
||
- **에러 레벨**: | ||
- ERROR: 서버 오류, 외부 API 실패 | ||
- WARN: 비즈니스 규칙 위반 | ||
- INFO: API 호출, 크론 작업 실행 | ||
- **로그 포맷**: | ||
- 시간 | ||
- 요청 ID | ||
- 사용자 ID | ||
- 에러 메시지 | ||
- 스택 트레이스 (ERROR 레벨만) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
Column, | ||
CreateDateColumn, | ||
Entity, | ||
Index, | ||
JoinColumn, | ||
ManyToOne, | ||
PrimaryGeneratedColumn, | ||
UpdateDateColumn, | ||
} from 'typeorm'; | ||
import { Stock } from '@/stock/domain/stock.entity'; | ||
|
||
@Entity('stock_news') | ||
export class StockNews { | ||
@PrimaryGeneratedColumn() | ||
id: number; | ||
|
||
@Index() | ||
@Column({ name: 'stock_id' }) | ||
stockId: string; | ||
|
||
@Column({ name: 'stock_name', length: 100 }) | ||
stockName: string; | ||
|
||
@Column({ type: 'text' }) | ||
link: string; | ||
|
||
@Column({ type: 'varchar', length: 255 }) | ||
title: string; | ||
|
||
@Column({ type: 'text' }) | ||
summary: string; | ||
|
||
@Column({ name: 'positive_content', type: 'text' }) | ||
positiveContent: string; | ||
|
||
@Column({ name: 'negative_content', type: 'text' }) | ||
negativeContent: string; | ||
|
||
@CreateDateColumn({ name: 'created_at' }) | ||
createdAt: Date; | ||
|
||
@UpdateDateColumn({ name: 'updated_at' }) | ||
updatedAt: Date; | ||
|
||
@ManyToOne(() => Stock, (stock) => stock.news) | ||
@JoinColumn({ name: 'stock_id' }) | ||
stock: Stock; | ||
|
||
getLinks(): string[] { | ||
return this.link.split(',').map(link => link.trim()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { IsString, MaxLength } from 'class-validator'; | ||
import { StockNews } from '@/news/domain/stockNews.entity'; | ||
|
||
export class CreateStockNewsDto { | ||
@IsString() | ||
stock_id: string; | ||
|
||
@IsString() | ||
stock_name: string; | ||
|
||
@IsString() | ||
link: string; | ||
|
||
@IsString() | ||
@MaxLength(255) | ||
title: string; | ||
|
||
@IsString() | ||
@MaxLength(10000) | ||
summary: string; | ||
|
||
@IsString() | ||
positive_content: string; | ||
|
||
@IsString() | ||
negative_content: string; | ||
} | ||
|
||
export class StockNewsResponse { | ||
constructor(stockNews: StockNews) { | ||
this.stockId = stockNews.stockId; | ||
this.stockName = stockNews.stockName; | ||
this.link = stockNews.link; | ||
this.title = stockNews.title; | ||
this.summary = stockNews.summary; | ||
this.positiveContent = stockNews.positiveContent; | ||
this.negativeContent = stockNews.negativeContent; | ||
this.createdAt = stockNews.createdAt; | ||
} | ||
|
||
stockId: string; | ||
stockName: string; | ||
link: string; | ||
title: string; | ||
summary: string; | ||
positiveContent: string; | ||
negativeContent: string; | ||
createdAt: Date; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { Controller, Post, Body, Get, Param } from '@nestjs/common'; | ||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; | ||
import { StockNewsService } from '@/news/stockNews.service'; | ||
import { CreateStockNewsDto, StockNewsResponse } from '@/news/dto/stockNews.dto'; | ||
|
||
@ApiTags('Stock News') | ||
@Controller('stock/news') | ||
export class StockNewsController { | ||
constructor(private readonly stockNewsService: StockNewsService) {} | ||
|
||
@Post() | ||
@ApiOperation({ summary: '주식 뉴스 정보 저장' }) | ||
@ApiResponse({ status: 201, type: StockNewsResponse }) | ||
async create(@Body() createStockNewsDto: CreateStockNewsDto) { | ||
const stockNews = await this.stockNewsService.create(createStockNewsDto); | ||
return new StockNewsResponse(stockNews); | ||
} | ||
|
||
@Get(':stockId') | ||
@ApiOperation({ summary: '종목별 뉴스 조회' }) | ||
@ApiResponse({ status: 200, type: [StockNewsResponse] }) | ||
async findByStockId(@Param('stockId') stockId: string) { | ||
const newsList = await this.stockNewsService.findByStockId(stockId); | ||
return newsList.map(news => new StockNewsResponse(news)); | ||
} | ||
|
||
@Get(':stockId/latest') | ||
@ApiOperation({ summary: '종목별 최신 뉴스 조회' }) | ||
@ApiResponse({ status: 200, type: StockNewsResponse }) | ||
async findLatestByStockId(@Param('stockId') stockId: string) { | ||
const news = await this.stockNewsService.findLatestByStockId(stockId); | ||
return news ? new StockNewsResponse(news) : null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { StockNews } from '@/news/domain/stockNews.entity'; | ||
import { StockNewsController } from '@/news/stockNews.controller'; | ||
import { Module } from '@nestjs/common'; | ||
import { StockNewsService } from '@/news/stockNews.service'; | ||
|
||
@Module({ | ||
imports: [TypeOrmModule.forFeature([StockNews])], | ||
controllers: [StockNewsController], | ||
providers: [StockNewsService], | ||
exports: [StockNewsService], | ||
}) | ||
|
||
export class StockNewsModule {} |
Oops, something went wrong.