diff --git a/README.md b/README.md index 7c8d8e6..21fb92b 100644 --- a/README.md +++ b/README.md @@ -1 +1,146 @@ -# mooney-backend \ No newline at end of file +# mooney-backend + +**Mooney 프로젝트의 백엔드 서버 리포지토리입니다.** +이 서버는 사용자 정보, 소비 데이터, 예산 관리, 챌린지 생성 및 통계 조회 등 **Mooney 전체 기능의 핵심 비즈니스 로직**을 담당합니다. Spring Boot, JPA, PostgreSQL, Redis, Spring Security, JWT, OpenAI API 등의 기술을 기반으로 구현되어 있습니다. + +## ✨ 프로젝트 개요 + +**Mooney(무니)** 는 예산 내 소비에 어려움을 겪는 **Z세대**를 위한 AI 기반 절약 가계부 서비스입니다. +사용자가 스스로 설정한 예산 안에서 **지속 가능한 소비 습관**을 형성할 수 있도록 다음과 같은 기능을 제공합니다: + +* 📊 **Prophet 기반 시계열 예측 모델**을 통해 **다음 주 과소비 예상 카테고리 자동 탐지** +* 🎯 지출 습관 개선을 유도하는 **맞춤형 절약 챌린지 생성** +* 💬 GPT-4o-mini 기반 챗봇 **‘똑똑소비봇’** 으로 예산 내 소비 가능 여부 실시간 조언 +* 🧩 소비 성공 시 **경험치, 캐릭터 해금, UI 변화 등 게이미피케이션 요소 제공** + +무니는 단순한 기록형 가계부가 아닌, 사용자와 상호작용하며 소비 습관을 바꾸는 **AI 소비 파트너**입니다. +**Mooney Backend**는 이러한 기능을 실현하기 위해 다음과 같은 역할을 수행합니다: + +* FastAPI 기반 AI 서버와의 통신 및 예측 결과 처리 +* 사용자 인증 및 보안 처리 (JWT, Spring Security) +* PostgreSQL 기반 예산/소비/챌린지 데이터 저장 및 로직 관리 +* Redis 기반 캐시 처리 및 실시간 처리 최적화 + +--- + +## 사전 설치/필요 항목 + +1. **Java 21 (JDK)** + - **설치 방법** + - Windows: [Temurin 공식 사이트](https://adoptium.net/temurin/releases/?version=21)에서 JDK21-LTS Windows 버전 내려받아 설치 + - macOS (Homebrew 권장): + ```bash + brew install openjdk@21 + ``` + - Linux (Debian/Ubuntu): + ```bash + sudo apt update + sudo apt install -y openjdk-21-jdk + ``` + +2. **Gradle** (프로젝트 포함) + - 프로젝트 내 `gradlew`(Wrapper) 사용 권장 (별도 설치 불필요) + +3. **PostgreSQL** + - **설치 방법** + - Windows: [PostgreSQL 공식 사이트](https://www.postgresql.org/download/)에서 Windows 버전 내려받아 설치 + - macOS (Homebrew 권장): + ```bash + brew install postgresql + ``` + - Linux (Debian/Ubuntu): + ```bash + sudo apt update + sudo apt install -y postgresql postgresql-contrib + ``` + +4. **Redis** + - **설치 방법** + - Windows: [Redis 공식 사이트](https://redis.io/download)에서 Windows 버전 내려받아 설치 + - macOS (Homebrew 권장): + ```bash + brew install redis + ``` + - Linux (Debian/Ubuntu): + ```bash + sudo apt update + sudo apt install -y redis-server + ``` + - **서버 실행** + ```bash + redis-server + ``` + +5. **Docker** (선택) + - Windows/macOS: [Docker Desktop](https://www.docker.com/products/docker-desktop/)에서 Windows 버전 내려받아 설치 + - Linux: + ```bash + sudo apt update + sudo apt install -y docker.io + sudo systemctl start docker + sudo usermod -aG docker $USER + ``` + +6. **OpenAI API 키** + - [OpenAI 플랫폼](https://platform.openai.com/)에서 발급 후 `application.yml`에 입력 + +--- + +## 설치 및 실행 방법 + +### 1. 깃 레포지토리 다운로드 또는 클론 +- git 명령어로 클론: +```bash +git clone https://github.com/TeamTamtam/mooney-backend.git +cd mooney-backend/mooney +``` + +### 2. 환경 변수 설정 +- 예시 파일(`src/main/resources/application-example.yml`)이 제공됩니다. 이 파일의 이름을 `application.yml`로 변경해 각 항목에 실제 값을 채워넣거나, `application.yml` 파일을 새로 작성해야 합니다. datasource(PostgreSQL), Redis, OpenAI API 키 등의 환경변수를 본인의 환경에 맞게 수정하세요. +- 예: +```bash +cat > src/main/resources/application.yml +# 준비된 내용을 복사 붙여넣고, ctrl+c로 저장 +``` +- 또한, 로컬 Redis를 사용하는 경우 Redis 서버가 실행중이어야 합니다. + +### 3. 빌드 +```bash +./gradlew build -x test # 테스트는 빌드 생략 +``` + +### 4. 실행 +```bash +# 로컬 실행 +./gradlew bootRun -x test + +# 또는 빌드된 JAR로 실행 +java -jar build/libs/*.jar +``` + +### 5. Docker로 실행하고자 하는 경우 +```bash +docker build -t mooney-backend . +docker run -p 8080:8080 mooney-backend +``` +### 6. API 명세 확인 +- 서버가 정상 실행되면 `http://localhost:8080/swagger-ui.html`에 접속하여 Swagger UI로 API 명세를 확인할 수 있습니다. + +--- + +## 데이터베이스 +- PostgreSQL 사전 설치 및 유저네임, 비밀번호 설정이 필요합니다. +- Redis 설정 및 서버 실행이 필요합니다. +- `application.yml`에 접속 정보를 입력하세요. + +--- + +## 외부 라이브러리 +- Spring Boot, Spring Data JPA, Spring Security, Spring AI, Swagger 등 +- 자세한 의존성 목록은 `build.gradle`을 참고 부탁드립니다. + +--- + +## 참고/문서 +- [Spring Boot 공식문서](https://spring.io/projects/spring-boot) +- [Gradle 공식문서](https://docs.gradle.org) diff --git a/build-native.sh b/build-native.sh new file mode 100755 index 0000000..407717f --- /dev/null +++ b/build-native.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -e + +# 1) 스크립트 기준 최상위 프로젝트 루트 +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 2) 모듈 디렉터리로 이동 +cd "$ROOT_DIR/mooney" + +JAR="build/libs/mooney-0.0.1-SNAPSHOT.jar" +NAME="Mooney" +VERSION="1.0.0" +BASE_OPTS=( + --input build/libs + --main-jar "$(basename "$JAR")" + --name "$NAME" + --app-version "$VERSION" + --java-options "-Xmx512m" +) + +# 3) OS 감지 +OS="$(uname -s)" +case "$OS" in + Darwin|Linux) TYPE="app-image" ;; + MINGW*|MSYS*|CYGWIN*) TYPE="exe" ;; + *) echo "Unsupported OS: $OS" >&2; exit 1 ;; +esac + +echo "Building $NAME for $OS..." + +jpackage \ + "${BASE_OPTS[@]}" \ + --type "$TYPE" \ + --dest "$ROOT_DIR" # 생성 결과물을 스크립트가 있는 루트에 배치 + +echo "$NAME package created at $ROOT_DIR" diff --git a/mooney/build.gradle b/mooney/build.gradle index 2202a66..996c4d4 100644 --- a/mooney/build.gradle +++ b/mooney/build.gradle @@ -73,6 +73,6 @@ dependencyManagement { } } -tasks.named('test') { - useJUnitPlatform() -} +test { + enabled = false +} \ No newline at end of file diff --git a/mooney/src/main/java/tamtam/mooney/domain/chat/service/ChatService.java b/mooney/src/main/java/tamtam/mooney/domain/chat/service/ChatService.java index 47ee9c1..5e57ffb 100644 --- a/mooney/src/main/java/tamtam/mooney/domain/chat/service/ChatService.java +++ b/mooney/src/main/java/tamtam/mooney/domain/chat/service/ChatService.java @@ -15,7 +15,6 @@ import tamtam.mooney.domain.chat.dto.ChatResponseDto; import tamtam.mooney.domain.enums.ExpenseCategory; import tamtam.mooney.domain.transaction.service.ExpenseService; -import tamtam.mooney.domain.transaction.service.TransactionService; import tamtam.mooney.domain.user.entity.User; import tamtam.mooney.domain.user.service.UserService; import tamtam.mooney.global.openai.OpenAIOptionEnum; @@ -53,13 +52,11 @@ public void saveChatMessage(String userId, String message, String role) { chatRedisRepository.save("chat:" + userId, chatMessage); } - @Transactional(readOnly = true) public List getChatHistory(String userId) { return chatRedisRepository.findAll("chat:" + userId); } - public ChatResponseDto chat(ChatRequestDto requestDto) { User user = userService.getCurrentUser(); @@ -170,17 +167,12 @@ public String generateGPTResponseForChat(User user, String userMessage, String b UserAgent userAgent = userAgentRepository.findByUserAndIsActiveTrue(user) .orElseThrow(() -> new IllegalArgumentException("No active UserAgent found.")); - String agentPrompt = openAIService.generateUserAgentPrompt(userAgent); - String finalInstruction = openAIService.generateFinalInstruction(userAgent); + String agentPrompt = generateUserAgentPrompt(userAgent); + String finalInstruction = generateFinalInstruction(userAgent); String scenarioPrompt = generateScenarioPrompt(); String budgetAnalysisPrompt = generateBudgetAnalysisPrompt(); String sampleResponse = generateSampleResponse(user.getNickname()); -// String expenseInfo = """ -// - 식비: 월평균 400,000원, 주 3회 지출 -// - 쇼핑: 월평균 80,000원, 월 2회 지출 -// - 교통: 월평균 70,000원, 주 5회 이용 -// """; LocalDate today = LocalDate.now(); String message = String.format( @@ -207,7 +199,6 @@ public String generateGPTResponseForChat(User user, String userMessage, String b scenarioPrompt, budgetAnalysisPrompt, budgetInfo, -// expenseInfo, sampleResponse, today, user.getNickname(), @@ -216,4 +207,27 @@ public String generateGPTResponseForChat(User user, String userMessage, String b ); return openAIService.generateGPTResponse(message, OpenAIOptionEnum.BALANCED); } + + + // UserAgent(캐릭터 관련) 프롬프트 생성 + public String generateUserAgentPrompt(UserAgent userAgent) { + return String.format( + """ + 너는 "%s"야. + - %s의 성격, %s한 어조를 가진 금융 어시스턴트. 항상 이 성격과 말투를 유지. + - 한국어로 대답, 마크다운은 절대 사용 금지 + """, + userAgent.getAgent().getAgentName(), + userAgent.getAgent().getPersonality(), + userAgent.getAgentTone() + ); + } + + // UserAgent(캐릭터) 답변 마무리 문장 생성 + public String generateFinalInstruction(UserAgent userAgent) { + return String.format( + "이제 %s의 개성을 반영하여 자연스럽게 답변을 생성해.", + userAgent.getAgent().getAgentName() + ); + } } diff --git a/mooney/src/main/java/tamtam/mooney/domain/mission/controller/MissionController.java b/mooney/src/main/java/tamtam/mooney/domain/mission/controller/MissionController.java index 60703e8..0c4277a 100644 --- a/mooney/src/main/java/tamtam/mooney/domain/mission/controller/MissionController.java +++ b/mooney/src/main/java/tamtam/mooney/domain/mission/controller/MissionController.java @@ -37,13 +37,13 @@ public ResponseEntity getMissionResultByDate() { return ResponseEntity.ok(missionTabDto); } - @Operation(summary = "해당 사용자에 대해서만 미션 스케줄링 강제로 수행하기") - @PostMapping("/run") - public ResponseEntity> getNewMission(@RequestParam LocalDate startDate) { - User user = userService.getCurrentUser(); - List newMissions = missionScheduler.runSchedulerManually(user, startDate); - return ResponseEntity.ok(newMissions); - } + // @Operation(summary = "해당 사용자에 대해서만 미션 스케줄링 강제로 수행하기") + // @PostMapping("/run") + // public ResponseEntity> getNewMission(@RequestParam LocalDate startDate) { + // User user = userService.getCurrentUser(); + // List newMissions = missionScheduler.runSchedulerManually(user, startDate); + // return ResponseEntity.ok(newMissions); + // } diff --git a/mooney/src/main/java/tamtam/mooney/domain/transaction/service/LlmCategoryClassifier.java b/mooney/src/main/java/tamtam/mooney/domain/transaction/service/LlmCategoryClassifier.java index e84b843..8ec05f5 100644 --- a/mooney/src/main/java/tamtam/mooney/domain/transaction/service/LlmCategoryClassifier.java +++ b/mooney/src/main/java/tamtam/mooney/domain/transaction/service/LlmCategoryClassifier.java @@ -12,7 +12,7 @@ @Service public class LlmCategoryClassifier { - @Value("${openai.api.key}") + @Value("${spring.ai.openai.api-key}") private String openAiApiKey; private static final String OPENAI_URL = "https://api.openai.com/v1/chat/completions"; diff --git a/mooney/src/main/java/tamtam/mooney/global/config/AgentInitializer.java b/mooney/src/main/java/tamtam/mooney/global/config/AgentInitializer.java index 001c027..151bf48 100644 --- a/mooney/src/main/java/tamtam/mooney/global/config/AgentInitializer.java +++ b/mooney/src/main/java/tamtam/mooney/global/config/AgentInitializer.java @@ -22,7 +22,7 @@ public void initAgents() { Set.of( "무니, 정석적이고 친절한 가계부 전문가, /png", - "써니, 활발하고 쾌활한 응원러, /png", + "써니, 활발하고 쾌활한 치어리더, /png", "티타, 깐깐하고 엄격한 절약 전문가, /png", "에피, 감성적이고 위로를 잘하는 힐링 캐릭터, /png" ).forEach(agent -> { diff --git a/mooney/src/main/java/tamtam/mooney/global/openai/OpenAIService.java b/mooney/src/main/java/tamtam/mooney/global/openai/OpenAIService.java index 100acf2..c05cf95 100644 --- a/mooney/src/main/java/tamtam/mooney/global/openai/OpenAIService.java +++ b/mooney/src/main/java/tamtam/mooney/global/openai/OpenAIService.java @@ -5,7 +5,6 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.stereotype.Service; -import tamtam.mooney.domain.agent.entity.UserAgent; @Service @RequiredArgsConstructor @@ -19,26 +18,4 @@ public String generateGPTResponse(String message, OpenAIOptionEnum optionType) { ); return openAiChatModel.call(prompt).getResult().getOutput().getText(); } - - // UserAgent(캐릭터 관련) 프롬프트 생성 - public String generateUserAgentPrompt(UserAgent userAgent) { - return String.format( - """ - 너는 "%s"야. - - %s의 성격, %s한 어조를 가진 금융 어시스턴트. 항상 이 성격과 말투를 유지. - - 한국어로 대답, 마크다운은 절대 사용 금지 - """, - userAgent.getAgent().getAgentName(), - userAgent.getAgent().getPersonality(), - userAgent.getAgentTone() - ); - } - - // UserAgent(캐릭터) 답변 마무리 문장 생성 - public String generateFinalInstruction(UserAgent userAgent) { - return String.format( - "이제 %s의 개성을 반영하여 자연스럽게 답변을 생성해.", - userAgent.getAgent().getAgentName() - ); - } } diff --git a/mooney/src/main/resources/application-example.yml b/mooney/src/main/resources/application-example.yml new file mode 100644 index 0000000..16512db --- /dev/null +++ b/mooney/src/main/resources/application-example.yml @@ -0,0 +1,46 @@ +spring: + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://:/?createDatabaseIfNotExist=true&characterEncoding=UTF-8&characterSetResults=UTF-8 # <-- DB 주소 입력 + username: # <-- DB 사용자명 입력 + password: # <-- DB 비밀번호 입력 + hikari: + maximum-pool-size: 10 + minimum-idle: 2 + idle-timeout: 30000 + connection-timeout: 20000 + pool-name: HikariPool + + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: update + show-sql: false + properties: + hibernate: + format_sql: false + jdbc: + lob: + non_contextual_creation: true + open-in-view: false + + data: + redis: + host: # <-- Redis 주소 입력 + port: # <-- Redis 포트 입력 + + ai: + openai: + api-key: # <-- OpenAI API 키 입력 + +server: + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true + +jwt: + secret: + access: # <-- JWT Access Secret 입력 + refresh: # <-- JWT Refresh Secret 입력 \ No newline at end of file