금융에 대해 처음 공부하는 청년들을 대상으로 정책∙예적금 추천 및 실제 주가를 기반으로 하는 모의투자 시스템
투자 성향에 따른 AI 포트폴리오 추천 시스템
주가 정보, 기업 투자 분석, 금융지식 질문 등 RAG 기반 AI 챗봇 시스템
GitHub
https://github.com/KE-WhyNot
Swagger API
AUTH https://auth.youth-fi.com/swagger-ui.html
FINANCE https://finance.youth-fi.com/swagger-ui.html
POLICY https://policy.youth-fi.com/docs#/
NOTIFY https://notify.youth-fi.com/swagger-ui.html
프로젝트 소개 | 팀원 구성 | 기술 스택 | 저장소·브랜치 전략·구조 | 개발 기간·작업 관리 | 신경 쓴 부분 | 페이지별 기능 | 주요 API
YouthFi 서비스 홈
YouthFi 서비스 정책
YouthFi 서비스 예•적금
YouthFi 서비스 모의투자
YouthFi 서비스 AI 포트폴리오
YouthFi 서비스 AI 주가•뉴스 분석
YouthFi 서비스 AI 투자 분석
YouthFi는 만 19~35세 청년의 재무현황과 투자성향을 바탕으로 위험/안전자산 비율을 자동 설계하고, 거주 지역과 나이 조건에 맞는 청년정책을 한 번에 매칭해주는 맞춤형 자산관리 플랫폼입니다. 흩어진 금융상품·정책 정보와 개인의 성향·목표를 하나의 사용자 흐름으로 연결해 “지금 내게 맞는 선택”을 제시합니다.
입력: 나이, 거주지역, 소득분위, 투자성향, 투자 가능 금액
출력: 위험/안전자산 권장 비율, 예상 수익률, 리밸런싱 가이드, 정책 매칭 결과
- 개인화 자산배분
- 실시간 데이터 연동
- 리밸런싱
- 청년정책 매칭
- 모의투자
- 간결 입력–명확 결과
- 신뢰·보호 지향 설계
- 회원관리
- 정책/은행상품 소개
- 개인화 포트폴리오
- 모의투자
- 실시간 수익률 랭킹
- 회원관리: 회원가입 · 로그인 · 이메일 인증 · 소셜 로그인 · JWT 관리
- 정책·은행상품: API 수집 및 전처리, 조건 기반 추천과 검색
- 개인화 포트폴리오: 사용자 금융정보 수집 → AI 기반 맞춤 포트폴리오 생성
- 모의투자: 실시간 시세, 매수/매도, 거래 트랜잭션 처리
- 실시간 수익률 랭킹 & 알림: 총수익률 계산, 이벤트 알림
-
데이터 전처리
- 비정형 텍스트에서 이자율·가입조건·한도 등 정보를 추출해 지정 JSON 구조화 (Extraction)
- 핵심 내용을 3문장 이내로 요약 (Summarization)
- 주거·학자금·목돈 등 카테고리 자동 분류 (Classification)
-
의미 기반 검색 & RAG
- 질의 벡터화 → Vector Search → RAG 기반 응답 생성
-
개인화 포트폴리오
- 프롬프트 동적 생성 후 Gemini 호출로 종목/비중 JSON 수신
- 추천 근거 문장을 포함하고 응답 JSON을 검증
-
비기능(성능)
- LLM API 평균 응답 5초 이내(Timeout 7초) 목표
- Debezium CDC: Finance Service 배당·거래·보유주식 테이블 변경 사항을 OCI Streaming(Kafka)으로 전송
- Kafka Consumer: CDC 이벤트 처리 후 개인화 알림 생성 및 수익률 랭킹 TOP10 진입 여부 판단
- 수익률 계산: 요구서 기준 공식 및 예시를 기반으로 포트폴리오 수익률 재계산
- UPDATE 이벤트 처리: 보유 주식 변동 시 재계산을 트리거해 랭킹과 알림을 즉시 갱신
|
박영두 PM 코드리뷰 · 인프라 · RAG |
권도윤 AI Multi-Agent · RAG |
김시혁 백엔드 포트폴리오 · 모의투자 |
김태엽 백엔드 ETL · 검색 · 정책 및 예적금 |
|
김다영 인프라 · 백엔드 알림 · CDC · 인프라 |
정명성 프론트 파트장 UI/UX |
곽다현 프론트 UI/UX |
Auth-Service에서 JWT를 발급·검증하고, NGINX Ingress가 Edge 인증을 수행한 뒤 각 백엔드 서비스가 동작합니다. 아래는 서비스별 대표 엔드포인트와 인증 경로 요약입니다.
| Method | Endpoint | 주요 기능 | 인증 |
|---|---|---|---|
| POST | /api/auth/signup | 이메일 인증 완료 후 회원가입 | Public |
| POST | /api/auth/login | JWT 발급 (access / refresh) | Public |
| DELETE | /api/auth/logout | 토큰 무효화 로그아웃 | JWT |
| DELETE | /api/auth/logout/user | @CurrentUser 기반 로그아웃 | JWT |
| POST | /api/auth/reissue | Refresh Token으로 Access Token 재발급 | JWT |
| GET | /api/auth/profile | 토큰 유효성 검증 및 프로필 조회 | JWT |
| PATCH | /api/auth/profile | 회원 정보 수정 / 비밀번호 변경 | JWT |
| POST | /api/auth/login/{provider} | OAuth2 소셜 로그인 · 회원가입 | Public |
| POST | /api/email/verification/send | 6자리 이메일 인증 코드 발송 | Public |
| POST | /api/email/verification/verify | 인증 코드 검증 및 이메일 인증 완료 | Public |
| Method | Endpoint | 주요 기능 | 인증 |
|---|---|---|---|
| GET | /api/master/region | 지역 필터 목록 | Public |
| GET | /api/master/category | 카테고리 필터 목록 | Public |
| GET | /api/master/education | 학력 필터 목록 | Public |
| GET | /api/master/job-status | 취업 상태 필터 목록 | Public |
| GET | /api/master/major | 전공 요건 필터 | Public |
| GET | /api/master/specialization | 특화 분야 필터 | Public |
| GET | /api/master/keyword | 정책 키워드 목록 | Public |
| GET | /api/policies | 정책 리스트 (조건 검색 · 페이징) | Public |
| GET | /api/policies/{id} | 정책 상세 · 신청 정보 | Public |
| Method | Endpoint | 주요 기능 | 인증 |
|---|---|---|---|
| GET | /api/fin/master/banks | 은행 목록 검색 / 권역 필터 | Public |
| GET | /api/fin/master/presets | 기간 · 상품 유형 등 선택값 프리셋 | Public |
| GET | /api/fin/products | 예 · 적금 상품 검색 · 필터 · 정렬 | Public |
| GET | /api/fin/products/{id} | 상품 상세 및 가입 조건 | Public |
| GET | /api/fin/products/{id}/rates | 기간별 금리 옵션 조회 | Public |
| GET | /api/fin/products/{id}/calculate | 예상 실효금리 · 만기금액 계산 | Public |
| Method | Endpoint | 주요 기능 | 인증 |
|---|---|---|---|
| POST | /api/auth/investment-info | 투자 성향 및 기본 정보 등록 | Edge JWT |
| GET | /api/auth/investment-info | 저장된 투자 정보 조회 | Edge JWT |
| PUT | /api/auth/investment-info | 투자 정보 수정 | Edge JWT |
| POST | /api/portfolios/recommendations | Gemini 기반 맞춤 포트폴리오 요청 | Edge JWT |
| GET | /api/portfolios/expected-return | 포트폴리오별 예상 수익률 계산 | Edge JWT |
| GET | /api/finance/stock/showall | 전체 주식 실시간 시세 조회 | Edge JWT |
| POST | /api/finance/stock/buy | 모의 매수 주문 생성 | Edge JWT |
| POST | /api/finance/stock/sell | 모의 매도 주문 생성 | Edge JWT |
| GET | /api/finance/stock/value | 시드머니 · 잔고 · 총평가금액 조회 | Edge JWT |
| GET | /api/finance/stock/my | 보유 종목 및 평가 조회 | Edge JWT |
| GET | /api/finance/stock/execution | 사용자 전체 거래 내역 | Edge JWT |
| PATCH | /api/finance/stock/interest/add/{stockId} | 관심 종목 등록 | Edge JWT |
| PATCH | /api/finance/stock/interest/cancel/{stockId} | 관심 종목 해제 | Edge JWT |
| GET | /api/finance/stock/interest/show | 관심 종목 목록 조회 | Edge JWT |
| Method | Endpoint | 주요 기능 | 인증 |
|---|---|---|---|
| GET | /api/notifications | 사용자 알림 목록 (페이징) | Edge JWT |
| PATCH | /api/notifications/{notificationId}/read | 단일 알림 읽음 처리 | Edge JWT |
| PATCH | /api/notifications/read-all | 전체 알림 읽음 처리 | Edge JWT |
| POST | /api/internal/notifications/trade | 거래 체결 알림 생성 | Internal |
| POST | /api/internal/notifications/ranking-entered | 수익률 랭킹 진입 알림 생성 | Internal |
| POST | /api/internal/notifications/dividend | 배당금 지급 알림 생성 | Internal |
| Method | Endpoint | 주요 기능 | 인증 |
|---|---|---|---|
| POST | /api/v1/chat | 자연어 질의 기반 정책 · 금융상품 의미 검색, 투자 분석 · 현재 주가 · 실시간 기사 검색 | Edge JWT → GCP SA |
| POST | /api/v1/protfolio/enhanced | 투자 성향 기반 주식 포트폴리오 및 근거 반환 | Edge JWT → GCP SA |
표기: Edge JWT는 NGINX Ingress의 JWT 검증 이후 백엔드 호출, Internal은 서비스 간 호출(예: CDC Consumer), Edge JWT → GCP SA는 Finance-Service가 Cloud Run Gemini 엔드포인트 호출 시 GCP_SA_KEY로 서비스 계정 인증을 수행함을 의미합니다.
main— 배포용 안정 브랜치. 태깅(vX.Y.Z) 후 배포.develop— 통합 개발 브랜치. 기능/버그 픽스 머지 대상.feature/<scope>-<short-desc>— 기능 단위 작업. 완료 시 PR →develop.hotfix/<issue>— 긴급 수정. PR →main및develop양쪽 반영.release/<version>— 릴리즈 준비(버전, 문서, 마이그레이션) 후main병합.
- PR 템플릿 사용: 배경/변경점/테스트/스크린샷/체크리스트 포함
- 리뷰 1명 이상 승인(🚦 최소 1 Approve), CI 통과 필수
- 라벨:
feature,fix,refactor등
feat(auth): add refresh token rotation
fix(api): handle null imageUrl in profile response
refactor(ui): split ReportChart into small components
docs(readme): add tech stack badges
chore(ci): bump node to 20.x in workflow
아래는 portfolio 도메인의 대표 구성요소를 간단히 요약한 예시입니다.
전체 코드는 레포지토리에서 확인하세요.
1) DTO · Request
// CompleteInvestmentProfileRequest.java
public record CompleteInvestmentProfileRequest(
@Schema(description = "투자성향 유형", example = "CONSERVATIVE")
InvestmentProfile.InvestmentProfileType investmentProfile,
@Schema(description = "투자가능 자산", example = "10000000.00")
BigDecimal availableAssets,
@Schema(description = "투자 목표", example = "EDUCATION")
InvestmentProfile.InvestmentGoal investmentGoal,
@Schema(description = "감당가능 손실", example = "TEN_PERCENT")
InvestmentProfile.LossTolerance lossTolerance,
@Schema(description = "금융 이해도", example = "MEDIUM")
InvestmentProfile.FinancialKnowledge financialKnowledge,
@Schema(description = "기대 이익", example = "TWO_HUNDRED_PERCENT")
InvestmentProfile.ExpectedProfit expectedProfit,
@Schema(description = "관심섹터명 목록", example = "[\"전기·전자\", \"건설\", \"IT 서비스\"]")
List<String> interestedSectorNames
) {}
// ChatRequest.java
public record ChatRequest(
String message,
String user_id,
String session_id
) {}
2) DTO · Response
// PortfolioResponse.java
public record PortfolioResponse(
Long portfolioId,
String userId,
List<RecommendedStock> recommendedStocks,
BigDecimal allocationSavings,
BigDecimal highestValue,
BigDecimal lowestValue,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public record RecommendedStock(
String stockId,
String stockName,
BigDecimal allocationPct,
String sectorName,
String reason
) {}
}
// PortfolioRiskAnalysisResponse.java
public record PortfolioRiskAnalysisResponse(
BigDecimal originalInvestment,
BigDecimal highestValue,
BigDecimal lowestValue,
BigDecimal currentValue,
BigDecimal highestReturn,
BigDecimal lowestReturn,
BigDecimal currentReturn,
String riskLevel
) {
public static PortfolioRiskAnalysisResponse getDefault(BigDecimal investment) {
return new PortfolioRiskAnalysisResponse(
investment, investment, investment, investment,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, "MEDIUM"
);
}
}
// ChatResponse.java
public record ChatResponse(
String replyText,
boolean success,
String errorMessage
) {}
3) Entity
// InvestmentProfile.java
@Entity
@Table(name = "investment_profiles")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class InvestmentProfile extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long profileId;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId", nullable = false)
private User user;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private InvestmentProfileType investmentProfile;
@Column(nullable = false, precision = 15, scale = 2)
private BigDecimal availableAssets;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private InvestmentGoal investmentGoal;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LossTolerance lossTolerance;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private FinancialKnowledge financialKnowledge;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ExpectedProfit expectedProfit;
@OneToMany(mappedBy = "investmentProfile", cascade = CascadeType.ALL, orphanRemoval = true)
private List<InvestmentProfileSector> investmentProfileSectors = new ArrayList<>();
public enum InvestmentProfileType {
CONSERVATIVE("안정형"),
CONSERVATIVE_SEEKING("안정추구형"),
RISK_NEUTRAL("위험중립형"),
AGGRESSIVE("적극투자형"),
VERY_AGGRESSIVE("공격투자형");
}
public enum InvestmentGoal {
EDUCATION("학비"),
LIVING_EXPENSES("생활비"),
HOUSE_PURCHASE("주택마련"),
ASSET_GROWTH("자산증식"),
DEBT_REPAYMENT("채무상환");
}
public enum LossTolerance {
NO_LOSS("원금 손실 없음"),
TEN_PERCENT("원금의 10%"),
THIRTY_PERCENT("원금의 30%"),
FIFTY_PERCENT("원금의 50%"),
SEVENTY_PERCENT("원금의 70%"),
FULL_AMOUNT("원금 전액");
}
}
// Portfolio.java
@Entity
@Table(name = "portfolios")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Portfolio extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long portfolioId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId", nullable = false)
private User user;
@Column(nullable = false, length = 100)
private String portfolioName;
@Column(precision = 18, scale = 2)
private BigDecimal highestValue;
@Column(precision = 18, scale = 2)
private BigDecimal lowestValue;
@OneToMany(mappedBy = "portfolio", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PortfolioStock> portfolioStocks = new ArrayList<>();
public void updatePortfolio(String portfolioName, BigDecimal highestValue, BigDecimal lowestValue) {
this.portfolioName = portfolioName;
this.highestValue = highestValue;
this.lowestValue = lowestValue;
}
}
4) Repository
// InvestmentProfileRepository.java
public interface InvestmentProfileRepository extends JpaRepository<InvestmentProfile, Long> {
Optional<InvestmentProfile> findByUserUserId(String userId);
boolean existsByUserUserId(String userId);
}
// PortfolioRepository.java
public interface PortfolioRepository extends JpaRepository<Portfolio, Long> {
List<Portfolio> findByUserUserIdOrderByCreatedAtDesc(String userId);
}
// PortfolioStockRepository.java
public interface PortfolioStockRepository extends JpaRepository<PortfolioStock, Long> {
List<PortfolioStock> findByPortfolioPortfolioId(Long portfolioId);
}
5) Service (요약)
// InvestmentProfileService.java (발췌)
@Service
@RequiredArgsConstructor
public class InvestmentProfileService {
private final InvestmentProfileRepository investmentProfileRepository;
private final UserRepository userRepository;
public InvestmentProfile createInvestmentProfile(String userId,
InvestmentProfileType investmentProfile,
BigDecimal availableAssets,
InvestmentGoal investmentGoal,
LossTolerance lossTolerance,
FinancialKnowledge financialKnowledge,
ExpectedProfit expectedProfit,
List<String> interestedSectorNames) {
User user = userRepository.findById(userId)
.orElseThrow(() -> PortfolioException.userNotFound(userId));
InvestmentProfile profile = InvestmentProfile.builder()
.user(user)
.investmentProfile(investmentProfile)
.availableAssets(availableAssets)
.investmentGoal(investmentGoal)
.lossTolerance(lossTolerance)
.financialKnowledge(financialKnowledge)
.expectedProfit(expectedProfit)
.build();
return investmentProfileRepository.save(profile);
}
public Optional<InvestmentProfile> getInvestmentProfileByUserId(String userId) {
return investmentProfileRepository.findByUserUserId(userId);
}
}
// PortfolioService.java (발췌)
@Service
@RequiredArgsConstructor
public class PortfolioService {
private final PortfolioRepository portfolioRepository;
private final UserRepository userRepository;
public Portfolio createPortfolio(String userId, String portfolioName,
BigDecimal highestValue, BigDecimal lowestValue) {
User user = userRepository.findById(userId)
.orElseThrow(() -> PortfolioException.userNotFound(userId));
Portfolio portfolio = Portfolio.builder()
.user(user)
.portfolioName(portfolioName)
.highestValue(highestValue)
.lowestValue(lowestValue)
.build();
return portfolioRepository.save(portfolio);
}
public List<Portfolio> findPortfoliosByUserId(String userId) {
return portfolioRepository.findByUserUserIdOrderByCreatedAtDesc(userId);
}
}
// PortfolioRiskService.java (발췌)
@Service
@RequiredArgsConstructor
public class PortfolioRiskService {
private final StockCurrentPriceApiClient stockCurrentPriceApiClient;
public PortfolioRiskAnalysisResponse calculatePortfolioRisk(
List<CompleteInvestmentProfileResponse.RecommendedStock> recommendedStocks,
BigDecimal investmentAmount) {
BigDecimal totalHighValue = BigDecimal.ZERO;
BigDecimal totalLowValue = BigDecimal.ZERO;
BigDecimal currentValue = BigDecimal.ZERO;
for (CompleteInvestmentProfileResponse.RecommendedStock stock : recommendedStocks) {
StockPriceInfoResponse priceInfo = getStockPriceInfo(stock.stockId());
BigDecimal stockInvestment = investmentAmount
.multiply(stock.allocationPct())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
BigDecimal highValue = stockInvestment
.multiply(priceInfo.w52HighPrice())
.divide(priceInfo.currentPrice(), 2, RoundingMode.HALF_UP);
BigDecimal lowValue = stockInvestment
.multiply(priceInfo.w52LowPrice())
.divide(priceInfo.currentPrice(), 2, RoundingMode.HALF_UP);
totalHighValue = totalHighValue.add(highValue);
totalLowValue = totalLowValue.add(lowValue);
currentValue = currentValue.add(stockInvestment);
}
BigDecimal highReturn = calculateReturnRate(investmentAmount, totalHighValue);
BigDecimal lowReturn = calculateReturnRate(investmentAmount, totalLowValue);
return new PortfolioRiskAnalysisResponse(
investmentAmount, totalHighValue, totalLowValue, currentValue,
highReturn, lowReturn, BigDecimal.ZERO,
calculateRiskLevel(highReturn, lowReturn)
);
}
private String calculateRiskLevel(BigDecimal highReturn, BigDecimal lowReturn) {
BigDecimal riskRange = highReturn.subtract(lowReturn);
if(riskRange.compareTo(BigDecimal.valueOf(50)) > 0) return "HIGH";
else if (riskRange.compareTo(BigDecimal.valueOf(20)) > 0) return "MEDIUM";
else return "LOW";
}
}
6) UseCase
// PortfolioUseCase.java (발췌)
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PortfolioUseCase {
private final InvestmentProfileService investmentProfileService;
private final PortfolioService portfolioService;
private final PortfolioStockService portfolioStockService;
private final LLMApiClient llmApiClient;
private final PortfolioRiskService portfolioRiskCalculator;
@Transactional
public PortfolioResponse generateAiPortfolioRecommendation(String userId) {
// 1. 투자 프로필 조회
InvestmentProfile investmentProfile = investmentProfileService.getInvestmentProfileByUserId(userId)
.orElseThrow(() -> PortfolioException.investmentProfileNotFound());
// 2. LLM API 호출하여 포트폴리오 추천 받음
InvestmentProfileResponse profileResponse = investmentProfileService.toInvestmentProfileResponse(investmentProfile);
Map<String, Object> aiResponse = llmApiClient.requestPortfolioRecommendation(profileResponse);
CompleteInvestmentProfileResponse llmResponse = convertAiResponseToCompleteInvestmentProfileResponse(aiResponse);
// 3. 위험도 분석 수행
List<CompleteInvestmentProfileResponse.RecommendedStock> safeStocks =
Optional.ofNullable(llmResponse.recommendedStocks()).orElse(Collections.emptyList());
PortfolioRiskAnalysisResponse riskAnalysis = portfolioRiskCalculator.calculatePortfolioRisk(
safeStocks, investmentProfile.getAvailableAssets()
);
// 4. 포트폴리오 엔티티 생성 및 저장
Portfolio portfolio = portfolioService.createPortfolio(
userId, "AI 추천 포트폴리오",
riskAnalysis.highestValue(), riskAnalysis.lowestValue()
);
// 5. 포트폴리오에 주식 추가
for (CompleteInvestmentProfileResponse.RecommendedStock stock : safeStocks) {
portfolioStockService.addStockToPortfolio(portfolio.getPortfolioId(),
stock.stockId(), stock.allocationPct());
}
// 6. PortfolioResponse 반환
return new PortfolioResponse(
portfolio.getPortfolioId(), userId,
convertToPortfolioRecommendedStocks(safeStocks),
llmResponse.allocationSavings(),
riskAnalysis.highestValue(), riskAnalysis.lowestValue(),
portfolio.getCreatedAt(), portfolio.getUpdatedAt()
);
}
}
// InvestmentProfileUseCase.java (발췌)
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class InvestmentProfileUseCase {
private final InvestmentProfileService investmentProfileService;
private final UserRepository userRepository;
@Transactional
public InvestmentProfileResponse completeInvestmentProfile(String userId, CompleteInvestmentProfileRequest request) {
Optional<InvestmentProfile> existingProfile = investmentProfileService.getInvestmentProfileByUserId(userId);
InvestmentProfile profile;
if (existingProfile.isPresent()) {
profile = investmentProfileService.updateInvestmentProfile(
existingProfile.get().getProfileId(),
request.investmentProfile(), request.availableAssets(),
request.investmentGoal(), request.lossTolerance(),
request.financialKnowledge(), request.expectedProfit(),
request.interestedSectorNames()
);
} else {
profile = investmentProfileService.createInvestmentProfile(
userId, request.investmentProfile(), request.availableAssets(),
request.investmentGoal(), request.lossTolerance(),
request.financialKnowledge(), request.expectedProfit(),
request.interestedSectorNames()
);
}
return investmentProfileService.toInvestmentProfileResponse(profile);
}
}
// ChatUseCase.java (발췌)
@Service
@RequiredArgsConstructor
public class ChatUseCase {
private final ChatSessionService chatSessionService;
@Transactional
public ChatResponse processChat(ChatRequest request, String userId) {
ChatRequest chatRequest = new ChatRequest(
request.message(), userId, request.session_id()
);
return chatSessionService.processChat(chatRequest);
}
}
7) Controller
// InvestmentProfileController.java (발췌)
@RestController
@RequestMapping("/api/user/investment-profile")
@RequiredArgsConstructor
@Tag(name = "InvestmentProfile", description = "투자성향 프로필 관리 API")
public class InvestmentProfileController implements BaseApi {
private final InvestmentProfileUseCase investmentProfileUseCase;
private final PortfolioUseCase portfolioRecommendationUseCase;
@PostMapping("/complete")
@Operation(summary = "투자성향 설문 완료", description = "투자성향 설문을 완료, 새 프로필을 생성 및 저장합니다.")
public BaseResponse<InvestmentProfileResponse> completeInvestmentProfile(
@RequestBody CompleteInvestmentProfileRequest request) {
String userId = SecurityUtils.getCurrentUserId();
InvestmentProfileResponse profile = investmentProfileUseCase.completeInvestmentProfile(userId, request);
return BaseResponse.onSuccess(profile);
}
@PostMapping("/send-to-llm")
@Operation(summary = "LLM 포트폴리오 추천 요청", description = "투자성향 프로필을 기반으로 포트폴리오를 생성하고 저장합니다.")
public BaseResponse<PortfolioResponse> sendProfileToLLM() {
String userId = SecurityUtils.getCurrentUserId();
PortfolioResponse response = portfolioRecommendationUseCase.generateAiPortfolioRecommendation(userId);
return BaseResponse.onSuccess(response);
}
@GetMapping("/exists")
@Operation(summary = "투자성향 완료 여부 확인", description = "사용자가 투자성향 설문을 완료했는지 확인합니다.")
public BaseResponse<Boolean> hasCompletedInvestmentProfile() {
String userId = SecurityUtils.getCurrentUserId();
boolean exists = investmentProfileUseCase.hasCompletedInvestmentProfile(userId);
return BaseResponse.onSuccess(exists);
}
}
// AiChatController.java (발췌)
@RestController
@RequestMapping("/api/ai/chat")
@RequiredArgsConstructor
@Tag(name = "AI 챗봇", description = "AI 챗봇과의 대화 관련 API")
@SecurityRequirement(name = "X-User-Id")
public class AiChatController implements BaseApi {
private final ChatUseCase chatUseCase;
@PostMapping
@Operation(summary = "AI 챗봇 채팅", description = "AI 챗봇과 대화를 진행합니다.")
public BaseResponse<ChatResponse> chat(@Valid @RequestBody ChatRequest request) {
String userId = SecurityUtils.getCurrentUserId();
ChatResponse response = chatUseCase.processChat(request, userId);
return BaseResponse.onSuccess(response);
}
}
| 기간 | 스프린트 목표 | 주요 산출물 |
|---|---|---|
| 2025-09-01 ~ 2025-09-14 (1~2주차) | 요구사항·UI/UX 설계·DB/Swagger 초안 | 요구분석서, ERD, OpenAPI v3, 컴포넌트 맵 |
| 2025-09-15 ~ 2025-10-05 (3~5주차) | 핵심 기능 개발(정책/예적금/성향/포트폴리오/모의투자) · 프롬프트 엔지니어링 | FE 페이지/상태, BE 도메인·ETL, RAG 프롬프트 |
| 2025-10-06 ~ 2025-10-16 (6~7주차) | 기능구현 및 로컬 테스트 | BE 도메인, RAG 로컬 테스트, 프론트 API 연결 |
| 2025-10-17 ~ 2025-10-24 (8주차 · 진행중) | 통합·안정화 테스트, 배포·운영 | E2E/통합 테스트, 성능·보안 점검, 버그픽스, 릴리즈 노트, 대시보드, 알림 룰, 운영 가이드 |
- 이슈 추적: GitHub Issues (템플릿: bug/feature/chore)
- JIRA: GitHub Projects — Backlog → In Progress → In Review → Done
- Main Merge 제한: 브렌치 제약을 통한 리뷰어 필수
- 품질 게이트: CI 빌드/테스트/리포트, 린트·포맷·타입체크
사용자의 투자성향 설문을 기반으로 LangGraph 메타 에이전트 시스템이 맞춤형 포트폴리오를 생성하고, Pinecone RAG와 Neo4j 지식그래프를 활용한 실시간 위험도 분석을 통해 안전한 투자 조합을 제공합니다.
- 5가지 투자성향: 안정형, 안정추구형, 위험중립형, 적극투자형, 공격투자형
- 종합 프로필 수집: 투자목표, 손실감수능력, 금융이해도, 기대수익률, 관심섹터
- 동적 설문 완료: 기존 프로필 존재 시 업데이트, 신규 사용자 시 생성
- LLM 기반 프로필 분석: QueryAnalyzerAgent가 사용자 응답을 분석하여 투자 성향 정확도 평가
- 프로필 검증: ConfidenceCalculatorAgent가 A~F 등급으로 프로필 신뢰도 평가
- LangGraph 워크플로우: ServicePlannerAgent가 포트폴리오 생성 전략 수립
- 다중 에이전트 협업: AnalysisAgent(투자분석) + DataAgent(실시간데이터) + NewsAgent(시장동향) 병렬 실행
- RAG 기반 분석: Pinecone에서 4,961개 금융 문서 검색하여 재무제표 분석
- Neo4j 지식그래프: 30,000+ 관계를 활용한 섹터별 연관성 분석
- 종목 배분 최적화: ResultCombinerAgent가 LLM 기반으로 최적 비중 계산
- 예적금 비율 제안: 투자성향별 자산배분 규칙 적용 (보수형 60:40, 공격형 30:70)
- Yahoo Finance API 연동: 58개 한국/미국 주요 종목 실시간 주가 데이터 수집
- 52주 고가/저가 기반: 각 종목의 최고/최저 가격을 활용한 수익률 범위 계산
- 포트폴리오 리스크 등급: HIGH(50%+), MEDIUM(20-50%), LOW(20% 미만) 자동 분류
- 투자금액별 시뮬레이션: 사용자 투자가능 자산에 따른 최고/최저 예상 수익 계산
- 실시간 주가 연동: yfinance 라이브러리로 현재가 기준 정확한 리스크 분석
- 변동성 분석: 과거 1년 데이터 기반 표준편차 계산으로 리스크 정량화
LangGraph 기반 11개 에이전트 시스템으로 투자 관련 질문에 대해 실시간으로 답변하는 AI 챗봇을 제공합니다. Pinecone RAG와 Neo4j 지식그래프를 활용한 지능형 응답 생성과 GCP 인증을 통한 안전한 API 통신을 구현했습니다.
- QueryAnalyzerAgent: 사용자 질문의 의도 파악 및 복잡도 평가 (simple/moderate/complex)
- ServicePlannerAgent: 질문 유형에 따른 최적 실행 전략 수립 (병렬/순차)
- ParallelExecutor: 독립적 작업 동시 실행으로 최대 50% 응답 시간 단축
- 전문 에이전트 라우팅: data_agent, analysis_agent, news_agent, knowledge_agent, visualization_agent 자동 선택
- 세션 기반 대화: 사용자별 고유 세션 ID로 대화 컨텍스트 유지
- Pinecone 벡터 검색: kakaobank/kf-deberta-base 모델로 한국어 금융 문서 의미 검색
- 네임스페이스 분리: terminology(금융용어), financial_analysis(재무제표), youth_policy(청년정책)별 검색
- Neo4j 지식그래프: SIMILAR_TO, SAME_CATEGORY, MENTIONS 관계로 관련 정보 탐색
- 실시간 뉴스 통합: 매일경제 RSS + Google RSS + 자동 번역으로 최신 시장 정보 제공
- 컨텍스트 품질 관리: 관련도 점수 기반 문서 필터링 및 상위 5개 문서 선별
- Gemini 2.0 Flash Exp: Google AI API를 통한 고성능 LLM 활용
- OAuth 2.0 인증: GCP 서비스 계정을 통한 안전한 API 인증
- LangSmith 모니터링: 모든 LLM 호출 추적 및 성능 메트릭 수집
- 에러 핸들링: 인증 실패, 네트워크 오류, 모델 과부하 등 다양한 예외 상황 처리
- 폴백 시스템: FallbackAgent가 에러 발생 시 대체 응답 제공
- 스트리밍 응답: 사용자 질문에 대한 즉시 응답 제공
- 신뢰도 평가: ConfidenceCalculatorAgent가 A~F 등급으로 응답 품질 실시간 평가
- 차트 생성: VisualizationAgent가 matplotlib 기반 주가/거래량 차트 자동 생성
- 응답 검증: ResponseAgent가 최종 응답 포맷팅 및 유효성 검사
- 사용자 피드백: 응답 만족도 수집 및 시스템 개선을 위한 피드백 분석
LangGraph 에이전트 시스템과 통합된 포트폴리오 관리 기능을 제공합니다. 실시간 데이터 연동과 AI 기반 분석을 통해 사용자의 투자 이력을 체계적으로 추적하고 관리합니다.
- ComprehensiveAnalysisService: 뉴스 분석 + 재무제표 분석 종합 서비스
- EnhancedPortfolioService: 투자성향별 자산배분 규칙 적용
- 섹터 분석 통합: sector_analysis_service로 섹터별 전망 분석
- 재무 데이터 분석: financial_data_service로 PER/PBR/ROE/배당수익률 분석
- 포트폴리오 메타데이터: 포트폴리오명, 최고가, 최저가, 생성일시, 위험등급 등 정보 저장
- Yahoo Finance API: 58개 주요 종목 실시간 주가, 거래량, 시가총액 데이터
- 동적 종목 설정: YAML 기반 stocks.yaml로 종목 관리 및 쉬운 확장
- 캐싱 최적화: 자주 조회되는 데이터 메모리 캐싱으로 API 호출 최소화
- 에러 복구: API 장애 시 폴백 데이터 제공 및 재시도 메커니즘
- 데이터 검증: 수신 데이터 유효성 검사 및 이상치 필터링
- 종목별 비중 설정: 각 종목의 배분 비율을 백분율로 관리
- 배분 비율 검증: normalize_integer_allocations로 전체 배분 비율 100% 보장
- 섹터별 분산: 동일 섹터 집중 투자 방지를 위한 섹터별 제한
- 기업 규모 선호도: 투자성향에 따른 대형/중형/소형주 비율 자동 조정
- 리스크 기반 조정: 변동성과 유동성을 고려한 동적 비중 조정
- 사용자별 포트폴리오 목록: 생성일시 기준 내림차순 정렬
- 최신 포트폴리오 조회: 가장 최근에 생성된 포트폴리오 우선 표시
- 포트폴리오 상세 정보: 포함된 종목, 배분 비율, 위험도, 예상수익률 등 상세 정보
- 성과 추적: 포트폴리오 수익률 계산 및 벤치마크 대비 성과 분석
- 시각화: matplotlib 기반 포트폴리오 구성 차트 및 성과 그래프
- 투자성향 설문: 사용자 입력 → QueryAnalyzerAgent 분석 → ConfidenceCalculatorAgent 검증 → 데이터베이스 저장
- AI 포트폴리오 생성: 프로필 조회 → ServicePlannerAgent 전략수립 → 다중에이전트 병렬실행 → ResultCombinerAgent 통합 → 포트폴리오 저장
- AI 챗봇 상담: 사용자 질문 → QueryAnalyzerAgent 분석 → 전문에이전트 라우팅 → RAG 검색 → ResponseAgent 응답생성
- 포트폴리오 관리: 생성/조회/수정/삭제 → 실시간데이터 연동 → AI 분석 → 사용자에게 결과 반환
- 실시간 모니터링: LangSmith 추적 → 성능메트릭 수집 → 에러감지 → 시스템 최적화













