Skip to content

Commit

Permalink
Merge pull request #40 from boostcampwm-2024/task-#37-news-write-read
Browse files Browse the repository at this point in the history
Task #37 종목별 뉴스 쓰기/조회구현, 프런트 구현
  • Loading branch information
tuchongkim authored Feb 5, 2025
2 parents d8ea485 + f2501c6 commit dccba25
Show file tree
Hide file tree
Showing 12 changed files with 581 additions and 1 deletion.
170 changes: 170 additions & 0 deletions packages/backend/docs/serviceExplain.md
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.
2 changes: 2 additions & 0 deletions packages/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@/configs/typeormConfig';
import { StockModule } from '@/stock/stock.module';
import { UserModule } from '@/user/user.module';
import { StockNewsModule } from '@/news/stockNews.module';

@Module({
imports: [
Expand All @@ -31,6 +32,7 @@ import { UserModule } from '@/user/user.module';
AuthModule,
ChatModule,
SessionModule,
StockNewsModule,
],
controllers: [],
providers: [],
Expand Down
53 changes: 53 additions & 0 deletions packages/backend/src/news/domain/stockNews.entity.ts
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());
}
}
49 changes: 49 additions & 0 deletions packages/backend/src/news/dto/stockNews.dto.ts
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;
}
34 changes: 34 additions & 0 deletions packages/backend/src/news/stockNews.controller.ts
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;
}
}
14 changes: 14 additions & 0 deletions packages/backend/src/news/stockNews.module.ts
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 {}
Loading

0 comments on commit dccba25

Please sign in to comment.