소중한 사람들의 기념일을 절대 잊지 않도록, AI가 추천하는 완벽한 선물까지
기념일과 이벤트를 관리하고, Google Gemini AI 기반 선물 추천을 받을 수 있는 풀스택 웹 애플리케이션입니다.
배포 주소: http://3.37.123.125
Day Memory는 생일, 기념일 등 중요한 날짜를 관리하고, AI를 활용한 스마트 선물 추천 시스템을 제공합니다. 사용자는 이벤트를 관리하고, 자동 리마인더를 받으며, AI 추천으로 최적의 선물을 찾을 수 있습니다.
주요 특징:
- 완전한 프로덕션 배포 (AWS EC2 + RDS)
- 멀티 테넌시 지원 (사용자별 데이터 격리)
- 실시간 D-day 계산 및 자동 리마인더
- Google Gemini API 기반 AI 선물 추천
- JWT 기반 보안 인증 및 사용자 권한 관리
- 25가지 이벤트 타입: 생일, 기념일(100일, 200일, 1주년), 발렌타인데이, 화이트데이, 크리스마스 등
- D-day 자동 계산: 각 이벤트까지 남은 날짜 실시간 계산
- 반복 이벤트: 매년 반복되는 기념일 자동 관리
- 수신자 정보 관리: 받는 사람의 이름, 관계 정보 저장
- 다단계 알림 시스템: 30일, 7일, 1일 전 이메일 자동 발송
- 개인화 설정: 사용자별 리마인더 활성화/비활성화
- 이벤트별 커스터마이징: 각 이벤트마다 다른 알림 일정 설정
- 발송 로그 추적: 모든 리마인더 발송 이력 관리
- 위시리스트 시스템: 선물 아이디어를 카테고리별로 저장
- 구매 상태 추적: 구매 완료/미완료 상태 실시간 관리
- 10가지 카테고리: 꽃, 주얼리, 화장품, 패션, 전자기기 등
- 이벤트 연결: 특정 이벤트와 선물 연결 관리
- Google Gemini API: 최신 LLM 기술로 개인화된 선물 추천
- 맞춤형 추천: 예산, 선호 카테고리, 수신자 성별/나이 고려
- 추천 이력 관리: 모든 추천 결과를 데이터베이스에 저장
- Fallback 시스템: API 실패 시에도 기본 추천 제공
- 요약 통계: 다가오는 이벤트, 미구매 선물 수량
- 오늘 발송 리마인더: 오늘 발송 예정인 리마인더 목록
- 이번 달 이벤트: 현재 달의 이벤트 캘린더 뷰
- 최근 리마인더 현황: 최근 발송 통계
| 분야 | 기술 | 버전 |
|---|---|---|
| 언어 | Java | 17 |
| 프레임워크 | Spring Boot | 3.2.1 |
| 데이터베이스 | PostgreSQL | 16.4 |
| ORM | Spring Data JPA (Hibernate) | - |
| 보안 | Spring Security + JWT | 6.x |
| 이메일 | Spring Mail (Gmail SMTP) | - |
| 스케줄링 | Spring @Scheduled | - |
| 빌드 도구 | Gradle | 8.5 |
| 분야 | 기술 | 버전 |
|---|---|---|
| 언어 | TypeScript | 5.2 |
| 프레임워크 | React | 18.2 |
| 상태관리 | Redux Toolkit + RTK Query | 1.9+ |
| 스타일링 | TailwindCSS | 3.x |
| 라우팅 | React Router | 6.x |
| 번들러 | Vite | 5.0 |
| 분야 | 기술 | 상세 |
|---|---|---|
| 클라우드 | AWS | EC2 (t2.micro) + RDS (PostgreSQL) |
| 웹 서버 | Nginx | Static 파일 제공 + API 프록시 |
| 컨테이너화 | Docker + Docker Compose | 멀티 컨테이너 배포 |
- Google Gemini API: AI 선물 추천
- Gmail SMTP: 이메일 발송
┌────────────────────────────────────────────────────────┐
│ 사용자 브라우저 (Client Layer) │
│ React + TypeScript + TailwindCSS │
└────────────────────┬─────────────────────────────────────┘
│ HTTP/HTTPS (Axios)
▼
┌────────────────────────────────────────────────────────┐
│ AWS EC2 인스턴스 (3.37.123.125, t2.micro) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Nginx (Port 80) │ │
│ │ - React 정적 파일 제공 │ │
│ │ - API 프록시 (/api/* → localhost:8080) │ │
│ └────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌────────────▼─────────────────────────────────────┐ │
│ │ Spring Boot (Port 8080) │ │
│ │ - REST API 엔드포인트 │ │
│ │ - JWT 인증/인가 │ │
│ │ - 비즈니스 로직 │ │
│ │ - 이메일 리마인더 스케줄러 │ │
│ │ - AI 추천 엔진 │ │
│ └────────────┬─────────────────────────────────────┘ │
└────────────────┼──────────────────────────────────────┘
│ PostgreSQL Protocol
▼
┌────────────────────────────────────────────────────────┐
│ AWS RDS (PostgreSQL 16.4) │
│ ap-northeast-2.rds.amazonaws.com │
│ - 사용자 정보, 이벤트, 선물, 리마인더 │
│ - AI 추천 이력, 발송 로그 │
└────────────────────────────────────────────────────────┘
External Services:
┌──────────────────┐ ┌──────────────────┐
│ Google Gemini │ │ Gmail SMTP │
│ API (LLM) │ │ (이메일 발송) │
└──────────────────┘ └──────────────────┘
사용자가 AI 추천 요청
│
▼
[React] POST /api/ai/recommendations
│
▼
[Nginx] API 프록시 → localhost:8080
│
▼
[Spring Boot] AIRecommendationController
│
▼
[Service] AIRecommendationService.recommendGifts()
│
├─> Google Gemini API 호출
│ ├─> 성공 → JSON 응답 파싱
│ └─> 실패 → Fallback 추천
│
├─> JPA로 결과 저장
│
└─> Response JSON 반환
│
▼
[React] Redux 상태 업데이트 및 UI 렌더링
backend/
├── src/main/java/com/daymemory/
│ ├── DayMemoryApplication.java # 메인 애플리케이션
│ ├── config/ # 설정
│ │ ├── SecurityConfig.java # Spring Security + JWT
│ │ ├── CorsConfig.java # CORS 정책
│ │ └── ...
│ ├── controller/ # REST API
│ │ ├── UserController.java # 인증
│ │ ├── EventController.java # 이벤트 관리
│ │ ├── GiftItemController.java # 선물 관리
│ │ ├── AIRecommendationController.java # AI 추천
│ │ └── DashboardController.java # 대시보드
│ ├── service/ # 비즈니스 로직
│ │ ├── UserService.java
│ │ ├── EventService.java # 권한 체크 포함
│ │ ├── GiftItemService.java # 권한 체크 포함
│ │ ├── AIRecommendationService.java
│ │ ├── ReminderScheduler.java # 이메일 스케줄
│ │ └── EmailService.java
│ ├── domain/
│ │ ├── entity/ # JPA Entity
│ │ │ ├── User.java
│ │ │ ├── Event.java
│ │ │ ├── EventReminder.java
│ │ │ ├── GiftItem.java
│ │ │ └── AIRecommendation.java
│ │ ├── dto/ # DTO
│ │ │ ├── UserDto.java
│ │ │ ├── EventDto.java
│ │ │ └── ...
│ │ └── repository/ # JPA Repository
│ ├── security/ # 보안
│ │ ├── JwtTokenProvider.java # JWT 토큰
│ │ ├── JwtAuthenticationFilter.java # JWT 필터
│ │ ├── SecurityUtils.java # 현재 사용자 조회
│ │ └── CustomUserDetailsService.java
│ └── exception/ # 예외 처리
│ ├── CustomException.java
│ ├── ErrorCode.java
│ └── GlobalExceptionHandler.java
├── src/main/resources/
│ └── application.yml # 설정 파일
└── build.gradle.kts
frontend/
├── src/
│ ├── pages/ # 페이지
│ │ ├── LoginPage.tsx
│ │ ├── DashboardPage.tsx
│ │ ├── EventListPage.tsx
│ │ ├── GiftListPage.tsx
│ │ └── RecommendationResultPage.tsx
│ ├── components/ # 컴포넌트
│ │ ├── layout/
│ │ │ ├── Header.tsx # 캐시 초기화 포함
│ │ │ └── Sidebar.tsx
│ │ ├── common/
│ │ └── dashboard/
│ ├── store/
│ │ ├── slices/
│ │ │ └── authSlice.ts # localStorage 초기화
│ │ └── services/
│ │ ├── authApi.ts # RTK Query
│ │ ├── eventsApi.ts
│ │ └── ...
│ ├── types/ # TypeScript 타입
│ ├── hooks/ # 커스텀 훅
│ ├── utils/ # 유틸리티
│ ├── App.tsx
│ └── main.tsx
├── tailwind.config.js
└── vite.config.ts
┌─────────────┐
│ users │
├─────────────┤
│ id (PK) │
│ email │
│ password │
│ nickname │
│ created_at │
└──────┬──────┘
│ (1:N)
├──────────────────┬──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ events │ │ gift_items │ │ai_recomm.. │
├─────────────┤ ├──────────────┤ ├────────────┤
│ id │ │ id │ │ id │
│ user_id (FK)│ │ user_id (FK) │ │ user_id(FK)│
│ title │ │ event_id(FK) │ │ event_id..│
│ event_date │ │ name │ │ response │
│ event_type │ │ is_purchased │ │ created_at │
└──────┬──────┘ └──────────────┘ └────────────┘
│
│ (1:N)
▼
┌────────────────────┐
│ event_reminders │
├────────────────────┤
│ id │
│ event_id (FK) │
│ days_before_event │
│ is_active │
└────────────────────┘
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id |
BIGINT | PK | 사용자 ID |
email |
VARCHAR(255) | UNIQUE | 이메일 (로그인 ID) |
password |
VARCHAR(255) | NOT NULL | 암호화된 비밀번호 |
nickname |
VARCHAR(100) | NOT NULL | 닉네임 |
created_at |
TIMESTAMP | NOT NULL | 생성 일시 |
updated_at |
TIMESTAMP | NOT NULL | 수정 일시 |
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id |
BIGINT | PK | 이벤트 ID |
user_id |
BIGINT | FK | 사용자 ID |
title |
VARCHAR(255) | NOT NULL | 제목 |
recipient_name |
VARCHAR(100) | NULL | 받는 사람 |
event_date |
DATE | NOT NULL | 이벤트 날짜 |
event_type |
VARCHAR(50) | NOT NULL | 이벤트 타입 |
is_recurring |
BOOLEAN | DEFAULT false | 반복 여부 |
is_tracking |
BOOLEAN | DEFAULT true | 추적 여부 |
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id |
BIGINT | PK | 선물 ID |
user_id |
BIGINT | FK | 사용자 ID |
event_id |
BIGINT | FK | 이벤트 ID |
name |
VARCHAR(255) | NOT NULL | 선물 이름 |
price |
DECIMAL | NULL | 가격 |
category |
VARCHAR(50) | NOT NULL | 카테고리 |
is_purchased |
BOOLEAN | DEFAULT false | 구매 여부 |
- Base URL:
http://3.37.123.125/api - 인증: JWT Bearer Token
- 응답: JSON
회원가입
POST /api/users/signup
{
"email": "user@example.com",
"password": "securePassword123!",
"nickname": "홍길동"
}로그인
POST /api/users/login
{
"email": "user@example.com",
"password": "securePassword123!"
}
Response:
{
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc...",
"user": {...}
}이벤트 생성
POST /api/events
Authorization: Bearer {token}
{
"title": "생일",
"recipientName": "김철수",
"eventDate": "2025-02-14",
"eventType": "BIRTHDAY",
"reminderDays": [30, 7, 1]
}이벤트 목록
GET /api/events
Authorization: Bearer {token}선물 생성
POST /api/gifts
Authorization: Bearer {token}
{
"name": "향수",
"estimatedPrice": 50000,
"category": "BEAUTY",
"eventId": 1
}추천 받기
POST /api/ai/recommendations
Authorization: Bearer {token}
{
"eventId": 1,
"recipientName": "김철수",
"budget": 100000,
"occasion": "생일"
}- JWT 토큰: HS256 알고리즘
- Access Token: 15분 유효기간
- Refresh Token: 7일 유효기간
- 모든 API에 권한 확인: 사용자는 자신의 데이터만 접근
- 예시 (EventService.getEvent()):
Long currentUserId = SecurityUtils.getCurrentUserId();
if (!event.getUser().getId().equals(currentUserId)) {
throw new CustomException(ErrorCode.FORBIDDEN);
}- 로그인 시: RTK Query 모든 캐시 초기화 + 페이지 전체 리로드
- 로그아웃 시: localStorage 완전 초기화 + Redux 리셋
- 결과: 사용자 간 데이터 완전 격리
- Java 17+
- Node.js 18+
- PostgreSQL 14+
- Docker & Docker Compose
1. 환경변수 (.env)
# Backend
DB_HOST=localhost
DB_PORT=5432
DB_NAME=day_memory
DB_USERNAME=postgres
DB_PASSWORD=postgres
JWT_SECRET=your-secret-key-here
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
AI_API_KEY=your-gemini-api-key2. Docker Compose 실행
docker-compose up -d3. 프론트엔드 실행
cd frontend
npm install
npm run dev4. 접속
- Frontend: http://localhost:5173
- Backend: http://localhost:8080/api
- Swagger: http://localhost:8080/swagger-ui.html
- EC2: Ubuntu t2.micro (1GB RAM, 30GB Storage)
- RDS: PostgreSQL 16.4
- Region: Asia Pacific (Seoul)
- IP: 3.37.123.125
1. Docker 이미지 빌드
docker build -t easter1201/day-memory-backend:latest backend/
docker build -t easter1201/day-memory-frontend:latest frontend/
docker push easter1201/day-memory-backend:latest
docker push easter1201/day-memory-frontend:latest2. EC2 배포
ssh ubuntu@3.37.123.125
cd day-memory
docker-compose -f docker-compose.prod.yml up -d3. 접속 확인
curl http://3.37.123.125/
curl http://3.37.123.125/api/health✅ 모든 API에 사용자 권한 확인 추가 ✅ EventService, GiftItemService의 모든 메서드에 권한 검증 ✅ 로그인/로그아웃 시 RTK Query 캐시 초기화 ✅ 로그인 후 페이지 전체 리로드로 컴포넌트 새로 마운트
✅ GitHub에 노출된 키 제거 ✅ 새로운 Google Gemini API 키 발급 ✅ 환경변수로 민감 정보 관리
✅ RTK Query를 이용한 효율적 캐싱 ✅ 로그인 시 모든 API 캐시 리셋 ✅ 로그아웃 시 localStorage 초기화
✅ N+1 쿼리 해결 (LEFT JOIN FETCH) ✅ 커스텀 쿼리로 필요한 데이터만 조회 ✅ 데이터베이스 인덱스 최적화
배포 IP: 3.37.123.125 | 마지막 업데이트: 2025-01-07