Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 146 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,146 @@
# mooney-backend
# 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)
36 changes: 36 additions & 0 deletions build-native.sh
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 3 additions & 3 deletions mooney/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ dependencyManagement {
}
}

tasks.named('test') {
useJUnitPlatform()
}
test {
enabled = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,13 +52,11 @@ public void saveChatMessage(String userId, String message, String role) {
chatRedisRepository.save("chat:" + userId, chatMessage);
}


@Transactional(readOnly = true)
public List<ChatMessage> getChatHistory(String userId) {
return chatRedisRepository.findAll("chat:" + userId);
}


public ChatResponseDto chat(ChatRequestDto requestDto) {
User user = userService.getCurrentUser();

Expand Down Expand Up @@ -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(
Expand All @@ -207,7 +199,6 @@ public String generateGPTResponseForChat(User user, String userMessage, String b
scenarioPrompt,
budgetAnalysisPrompt,
budgetInfo,
// expenseInfo,
sampleResponse,
today,
user.getNickname(),
Expand All @@ -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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ public ResponseEntity<MissionTabDto> getMissionResultByDate() {
return ResponseEntity.ok(missionTabDto);
}

@Operation(summary = "해당 사용자에 대해서만 미션 스케줄링 강제로 수행하기")
@PostMapping("/run")
public ResponseEntity<List<String>> getNewMission(@RequestParam LocalDate startDate) {
User user = userService.getCurrentUser();
List<String> newMissions = missionScheduler.runSchedulerManually(user, startDate);
return ResponseEntity.ok(newMissions);
}
// @Operation(summary = "해당 사용자에 대해서만 미션 스케줄링 강제로 수행하기")
// @PostMapping("/run")
// public ResponseEntity<List<String>> getNewMission(@RequestParam LocalDate startDate) {
// User user = userService.getCurrentUser();
// List<String> newMissions = missionScheduler.runSchedulerManually(user, startDate);
// return ResponseEntity.ok(newMissions);
// }



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void initAgents() {

Set.of(
"무니, 정석적이고 친절한 가계부 전문가, /png",
"써니, 활발하고 쾌활한 응원러, /png",
"써니, 활발하고 쾌활한 치어리더, /png",
"티타, 깐깐하고 엄격한 절약 전문가, /png",
"에피, 감성적이고 위로를 잘하는 힐링 캐릭터, /png"
).forEach(agent -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
);
}
}
46 changes: 46 additions & 0 deletions mooney/src/main/resources/application-example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://<DB_HOST>:<PORT>/<DB_NAME>?createDatabaseIfNotExist=true&characterEncoding=UTF-8&characterSetResults=UTF-8 # <-- DB 주소 입력
username: <DB_USERNAME> # <-- DB 사용자명 입력
password: <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_HOST> # <-- Redis 주소 입력
port: <REDIS_PORT> # <-- Redis 포트 입력

ai:
openai:
api-key: <OPENAI_API_KEY> # <-- OpenAI API 키 입력

server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true

jwt:
secret:
access: <JWT_ACCESS_SECRET> # <-- JWT Access Secret 입력
refresh: <JWT_REFRESH_SECRET> # <-- JWT Refresh Secret 입력