diff --git a/mindiary.db b/mindiary.db
new file mode 100644
index 0000000..38db2f8
Binary files /dev/null and b/mindiary.db differ
diff --git a/pom.xml b/pom.xml
index c0bead2..68a4942 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
org.xerial
sqlite-jdbc
- 3.45.0.0
+ 3.46.1.3
diff --git a/src/main/java/dao/DAOIntegrationTest.class b/src/main/java/dao/DAOIntegrationTest.class
new file mode 100644
index 0000000..47e3017
Binary files /dev/null and b/src/main/java/dao/DAOIntegrationTest.class differ
diff --git a/src/main/java/dao/DAOIntegrationTest.java b/src/main/java/dao/DAOIntegrationTest.java
new file mode 100644
index 0000000..6237a7c
--- /dev/null
+++ b/src/main/java/dao/DAOIntegrationTest.java
@@ -0,0 +1,320 @@
+package dao;
+
+import model.Diary;
+import dao.TagDAO.Tag;
+import dao.EmotionAnalysisDAO.EmotionAnalysis;
+import dao.StatisticsDAO.DailyStats;
+import dao.StatisticsDAO.MonthlyStats;
+import dao.StatisticsDAO.EmotionStats;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * 모든 DAO 클래스들을 종합적으로 테스트하는 클래스
+ */
+public class DAOIntegrationTest {
+
+ public static void main(String[] args) {
+ System.out.println("=== mindiary DAO 통합 테스트 시작 ===");
+
+ try {
+ // DAO 인스턴스 생성
+ DiaryDAO diaryDAO = new DiaryDAO();
+ TagDAO tagDAO = new TagDAO();
+ EmotionAnalysisDAO emotionDAO = new EmotionAnalysisDAO();
+ StatisticsDAO statsDAO = new StatisticsDAO();
+
+ // 1. 기본 연결 테스트
+ System.out.println("\n1. 기본 연결 테스트");
+ testBasicConnections(diaryDAO, tagDAO, emotionDAO, statsDAO);
+
+ // 2. 일기 CRUD 테스트
+ System.out.println("\n2. 일기 CRUD 테스트");
+ testDiaryCRUD(diaryDAO);
+
+ // 3. 태그 관리 테스트
+ System.out.println("\n3. 태그 관리 테스트");
+ testTagManagement(tagDAO, diaryDAO);
+
+ // 4. 감정 분석 테스트
+ System.out.println("\n4. 감정 분석 테스트");
+ testEmotionAnalysis(emotionDAO, diaryDAO);
+
+ // 5. 통계 기능 테스트
+ System.out.println("\n5. 통계 기능 테스트");
+ testStatistics(statsDAO);
+
+ // 6. 통합 시나리오 테스트
+ System.out.println("\n6. 통합 시나리오 테스트");
+ testIntegratedScenario(diaryDAO, tagDAO, emotionDAO, statsDAO);
+
+ System.out.println("\n=== 모든 DAO 테스트 완료 ===");
+ System.out.println("✅ 모든 테스트가 성공적으로 완료되었습니다!");
+
+ } catch (Exception e) {
+ System.err.println("❌ DAO 테스트 중 오류 발생: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 기본 연결 테스트
+ */
+ private static void testBasicConnections(DiaryDAO diaryDAO, TagDAO tagDAO,
+ EmotionAnalysisDAO emotionDAO, StatisticsDAO statsDAO) {
+ System.out.println(" 📡 DiaryDAO 연결 테스트: " + (diaryDAO.testConnection() ? "✅" : "❌"));
+
+ // 각 DAO의 기본 조회 기능 테스트
+ System.out.println(" 📊 기본 데이터 조회:");
+ System.out.println(" - 전체 일기 수: " + diaryDAO.getTotalDiaryCount());
+ System.out.println(" - 전체 태그 수: " + tagDAO.getAllTags().size());
+
+ Map overallStats = statsDAO.getOverallStats();
+ System.out.println(" - 전체 통계: " + overallStats);
+ }
+
+ /**
+ * 일기 CRUD 테스트
+ */
+ private static void testDiaryCRUD(DiaryDAO diaryDAO) {
+ System.out.println(" ✏️ 일기 생성 테스트");
+
+ // CREATE: 새 일기 생성
+ Diary testDiary = new Diary("DAO 테스트를 위한 일기입니다. 모든 기능이 정상적으로 작동하는지 확인하고 있습니다.", "positive");
+ boolean created = diaryDAO.insertDiary(testDiary);
+ System.out.println(" - 일기 생성: " + (created ? "✅ ID=" + testDiary.getId() : "❌"));
+
+ if (created && testDiary.getId() != null) {
+ // READ: 생성된 일기 조회
+ Optional retrieved = diaryDAO.getDiaryById(testDiary.getId());
+ System.out.println(" - 일기 조회: " + (retrieved.isPresent() ? "✅" : "❌"));
+
+ if (retrieved.isPresent()) {
+ Diary diary = retrieved.get();
+ System.out.println(" 내용: " + diary.getContent().substring(0, Math.min(30, diary.getContent().length())) + "...");
+ System.out.println(" 감정: " + diary.getEmotionSummary());
+
+ // UPDATE: 일기 수정
+ diary.setContent(diary.getContent() + " [수정됨]");
+ diary.setEmotionSummary("neutral");
+ boolean updated = diaryDAO.updateDiary(diary);
+ System.out.println(" - 일기 수정: " + (updated ? "✅" : "❌"));
+
+ // 다양한 조회 테스트
+ List allDiaries = diaryDAO.getAllDiaries(5, 0);
+ System.out.println(" - 일기 목록 조회 (5개): " + allDiaries.size() + "개");
+
+ List positiveEmotions = diaryDAO.getDiariesByEmotion("positive");
+ System.out.println(" - 긍정 감정 일기: " + positiveEmotions.size() + "개");
+
+ List searchResults = diaryDAO.searchDiariesByKeyword("테스트");
+ System.out.println(" - '테스트' 키워드 검색: " + searchResults.size() + "개");
+
+ // DELETE: 테스트 일기 삭제
+ boolean deleted = diaryDAO.deleteDiary(testDiary.getId());
+ System.out.println(" - 일기 삭제: " + (deleted ? "✅" : "❌"));
+ }
+ }
+ }
+
+ /**
+ * 태그 관리 테스트
+ */
+ private static void testTagManagement(TagDAO tagDAO, DiaryDAO diaryDAO) {
+ System.out.println(" 🏷️ 태그 관리 테스트");
+
+ // 기존 태그 조회
+ List existingTags = tagDAO.getAllTags();
+ System.out.println(" - 기존 태그 수: " + existingTags.size());
+
+ // 새 태그 생성
+ Tag testTag = new Tag("테스트태그", "#ff6b35", "DAO 테스트용 태그");
+ boolean tagCreated = tagDAO.createTag(testTag);
+ System.out.println(" - 태그 생성: " + (tagCreated ? "✅ ID=" + testTag.getId() : "❌"));
+
+ if (tagCreated && testTag.getId() != null) {
+ // 태그 조회
+ Optional retrievedTag = tagDAO.getTagById(testTag.getId());
+ System.out.println(" - 태그 조회: " + (retrievedTag.isPresent() ? "✅" : "❌"));
+
+ // 일기에 태그 연결 테스트 (기존 일기가 있는 경우)
+ List diaries = diaryDAO.getAllDiaries(1, 0);
+ if (!diaries.isEmpty()) {
+ Diary firstDiary = diaries.get(0);
+ boolean tagAdded = tagDAO.addTagToDiary(firstDiary.getId(), testTag.getId());
+ System.out.println(" - 일기-태그 연결: " + (tagAdded ? "✅" : "❌"));
+
+ // 일기의 태그 조회
+ List diaryTags = tagDAO.getTagsByDiaryId(firstDiary.getId());
+ System.out.println(" - 일기의 태그 조회: " + diaryTags.size() + "개");
+
+ // 태그 제거
+ boolean tagRemoved = tagDAO.removeTagFromDiary(firstDiary.getId(), testTag.getId());
+ System.out.println(" - 일기-태그 연결 해제: " + (tagRemoved ? "✅" : "❌"));
+ }
+
+ // 태그 삭제
+ boolean tagDeleted = tagDAO.deleteTag(testTag.getId());
+ System.out.println(" - 태그 삭제: " + (tagDeleted ? "✅" : "❌"));
+ }
+ }
+
+ /**
+ * 감정 분석 테스트
+ */
+ private static void testEmotionAnalysis(EmotionAnalysisDAO emotionDAO, DiaryDAO diaryDAO) {
+ System.out.println(" 😊 감정 분석 테스트");
+
+ // 기존 일기가 있는 경우에만 테스트
+ List diaries = diaryDAO.getAllDiaries(1, 0);
+ if (!diaries.isEmpty()) {
+ Diary testDiary = diaries.get(0);
+
+ // 감정 분석 생성
+ EmotionAnalysis analysis = new EmotionAnalysis(
+ testDiary.getId(),
+ "positive",
+ 0.85,
+ "[\"테스트\", \"기능\", \"좋음\"]",
+ "rule_based"
+ );
+
+ boolean analysisCreated = emotionDAO.saveEmotionAnalysis(analysis);
+ System.out.println(" - 감정 분석 저장: " + (analysisCreated ? "✅ ID=" + analysis.getId() : "❌"));
+
+ if (analysisCreated) {
+ // 감정 분석 조회
+ Optional retrieved = emotionDAO.getEmotionAnalysisByDiaryId(testDiary.getId());
+ System.out.println(" - 감정 분석 조회: " + (retrieved.isPresent() ? "✅" : "❌"));
+
+ if (retrieved.isPresent()) {
+ EmotionAnalysis retrievedAnalysis = retrieved.get();
+ System.out.println(" 감정 유형: " + retrievedAnalysis.getEmotionType());
+ System.out.println(" 신뢰도: " + retrievedAnalysis.getConfidenceScore());
+
+ // 감정 분석 수정
+ retrievedAnalysis.setEmotionType("neutral");
+ retrievedAnalysis.setConfidenceScore(0.70);
+ boolean updated = emotionDAO.updateEmotionAnalysis(retrievedAnalysis);
+ System.out.println(" - 감정 분석 수정: " + (updated ? "✅" : "❌"));
+ }
+
+ // 통계 조회
+ Map emotionDistribution = emotionDAO.getEmotionTypeDistribution();
+ System.out.println(" - 감정 유형 분포: " + emotionDistribution);
+
+ double avgConfidence = emotionDAO.getAverageConfidenceScore();
+ System.out.println(" - 평균 신뢰도: " + String.format("%.2f", avgConfidence));
+
+ // 감정 분석 삭제
+ if (analysis.getId() != null) {
+ boolean deleted = emotionDAO.deleteEmotionAnalysis(analysis.getId());
+ System.out.println(" - 감정 분석 삭제: " + (deleted ? "✅" : "❌"));
+ }
+ }
+ } else {
+ System.out.println(" - 테스트할 일기가 없어 감정 분석 테스트를 건너뜁니다.");
+ }
+ }
+
+ /**
+ * 통계 기능 테스트
+ */
+ private static void testStatistics(StatisticsDAO statsDAO) {
+ System.out.println(" 📊 통계 기능 테스트");
+
+ // 오늘 통계 업데이트
+ boolean todayUpdated = statsDAO.updateTodayStats();
+ System.out.println(" - 오늘 통계 업데이트: " + (todayUpdated ? "✅" : "❌"));
+
+ // 전체 통계 조회
+ Map overallStats = statsDAO.getOverallStats();
+ System.out.println(" - 전체 통계:");
+ overallStats.forEach((key, value) ->
+ System.out.println(" " + key + ": " + value));
+
+ // 최근 활동 요약
+ Map recentActivity = statsDAO.getRecentActivitySummary();
+ System.out.println(" - 최근 활동 요약:");
+ recentActivity.forEach((key, value) ->
+ System.out.println(" " + key + ": " + value));
+
+ // 월별 통계
+ List monthlyStats = statsDAO.getMonthlyStats(3);
+ System.out.println(" - 최근 3개월 통계: " + monthlyStats.size() + "개월");
+ monthlyStats.forEach(stat -> System.out.println(" " + stat.toString()));
+
+ // 감정 통계
+ List emotionStats = statsDAO.getEmotionStats();
+ System.out.println(" - 감정 통계: " + emotionStats.size() + "개 감정");
+ emotionStats.forEach(stat -> System.out.println(" " + stat.toString()));
+
+ // 최근 일일 통계
+ List dailyStats = statsDAO.getRecentDailyStats(7);
+ System.out.println(" - 최근 7일 일일 통계: " + dailyStats.size() + "일");
+ dailyStats.forEach(stat -> System.out.println(" " + stat.toString()));
+ }
+
+ /**
+ * 통합 시나리오 테스트
+ */
+ private static void testIntegratedScenario(DiaryDAO diaryDAO, TagDAO tagDAO,
+ EmotionAnalysisDAO emotionDAO, StatisticsDAO statsDAO) {
+ System.out.println(" 🔄 통합 시나리오 테스트");
+
+ try {
+ // 1. 새 일기 작성
+ Diary newDiary = new Diary("통합 테스트를 위한 일기입니다. 오늘 하루 종일 개발을 했는데 정말 보람찬 하루였습니다. 새로운 기능들이 하나씩 완성되어가는 모습을 보니 뿌듯합니다.", "positive");
+ boolean diaryCreated = diaryDAO.insertDiary(newDiary);
+ System.out.println(" 1. 일기 작성: " + (diaryCreated ? "✅" : "❌"));
+
+ if (diaryCreated && newDiary.getId() != null) {
+ // 2. 태그 추가
+ List existingTags = tagDAO.getAllTags();
+ if (!existingTags.isEmpty()) {
+ Tag firstTag = existingTags.get(0);
+ boolean tagAdded = tagDAO.addTagToDiary(newDiary.getId(), firstTag.getId());
+ System.out.println(" 2. 태그 추가: " + (tagAdded ? "✅" : "❌"));
+ }
+
+ // 3. 감정 분석 추가
+ EmotionAnalysis analysis = new EmotionAnalysis(
+ newDiary.getId(),
+ "positive",
+ 0.92,
+ "[\"보람찬\", \"뿌듯합니다\", \"완성\"]",
+ "rule_based"
+ );
+ boolean analysisAdded = emotionDAO.saveEmotionAnalysis(analysis);
+ System.out.println(" 3. 감정 분석 추가: " + (analysisAdded ? "✅" : "❌"));
+
+ // 4. 통계 업데이트
+ boolean statsUpdated = statsDAO.updateTodayStats();
+ System.out.println(" 4. 통계 업데이트: " + (statsUpdated ? "✅" : "❌"));
+
+ // 5. 종합 결과 확인
+ Optional finalDiary = diaryDAO.getDiaryById(newDiary.getId());
+ List diaryTags = tagDAO.getTagsByDiaryId(newDiary.getId());
+ Optional diaryEmotion = emotionDAO.getEmotionAnalysisByDiaryId(newDiary.getId());
+
+ System.out.println(" 5. 종합 결과:");
+ System.out.println(" - 일기 조회: " + (finalDiary.isPresent() ? "✅" : "❌"));
+ System.out.println(" - 연결된 태그 수: " + diaryTags.size());
+ System.out.println(" - 감정 분석: " + (diaryEmotion.isPresent() ? "✅" : "❌"));
+
+ // 6. 정리 (테스트 데이터 삭제)
+ if (diaryEmotion.isPresent()) {
+ emotionDAO.deleteEmotionAnalysis(diaryEmotion.get().getId());
+ }
+ tagDAO.removeAllTagsFromDiary(newDiary.getId());
+ diaryDAO.deleteDiary(newDiary.getId());
+ System.out.println(" 6. 테스트 데이터 정리: ✅");
+ }
+
+ } catch (Exception e) {
+ System.err.println(" ❌ 통합 시나리오 테스트 중 오류: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/dao/DiaryDAO.class b/src/main/java/dao/DiaryDAO.class
new file mode 100644
index 0000000..d1b7399
Binary files /dev/null and b/src/main/java/dao/DiaryDAO.class differ
diff --git a/src/main/java/dao/DiaryDAO.java b/src/main/java/dao/DiaryDAO.java
index fa947c9..d585ade 100644
--- a/src/main/java/dao/DiaryDAO.java
+++ b/src/main/java/dao/DiaryDAO.java
@@ -1,6 +1,7 @@
package dao;
import model.Diary;
+import util.DatabaseUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -11,109 +12,160 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+/**
+ * 일기 데이터 액세스 객체
+ * SQLite 데이터베이스와 연동하여 일기 관련 CRUD 작업을 처리합니다.
+ */
public class DiaryDAO {
private static final Logger logger = LoggerFactory.getLogger(DiaryDAO.class);
- private static final String DB_URL = "jdbc:sqlite:mindiary.db";
- private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-
+ private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private final DatabaseUtil dbUtil;
+
public DiaryDAO() {
- initializeDatabase();
+ this.dbUtil = DatabaseUtil.getInstance();
}
-
+
+ // ========================================
+ // CREATE 작업
+ // ========================================
+
/**
- * 데이터베이스 초기화 - 테이블 생성
+ * 새로운 일기 저장
*/
- private void initializeDatabase() {
- String createTableSQL = """
- CREATE TABLE IF NOT EXISTS diary (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- content TEXT NOT NULL,
- emotion_summary TEXT,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
+ public boolean insertDiary(Diary diary) {
+ if (diary == null || !diary.isValid()) {
+ logger.error("Invalid diary object provided for insertion");
+ return false;
+ }
+
+ String insertSQL = """
+ INSERT INTO diary (content, emotion_summary, created_at, updated_at)
+ VALUES (?, ?, ?, ?)
""";
-
- try (Connection conn = DriverManager.getConnection(DB_URL);
- Statement stmt = conn.createStatement()) {
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS)) {
+
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ pstmt.setString(1, diary.getContent());
+ pstmt.setString(2, diary.getEmotionSummary());
+ pstmt.setString(3, diary.getCreatedAt() != null ? diary.getCreatedAt() : now);
+ pstmt.setString(4, now);
+
+ int result = pstmt.executeUpdate();
- stmt.execute(createTableSQL);
- logger.info("Database initialized successfully");
+ if (result > 0) {
+ // 생성된 ID 가져오기
+ try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
+ if (generatedKeys.next()) {
+ diary.setId(generatedKeys.getInt(1));
+ diary.setUpdatedAt(now);
+ logger.info("Diary inserted successfully with ID: {}", diary.getId());
+ return true;
+ }
+ }
+ }
+
+ return false;
} catch (SQLException e) {
- logger.error("Database initialization failed", e);
- throw new RuntimeException("Failed to initialize database", e);
+ logger.error("Failed to insert diary", e);
+ return false;
}
}
-
+
/**
- * 새로운 일기 저장
+ * 편의 메서드: 문자열로 일기 저장
*/
public boolean insertDiary(String content, String emotionSummary) {
- String insertSQL = """
- INSERT INTO diary (content, emotion_summary, created_at, updated_at)
- VALUES (?, ?, ?, ?)
+ return insertDiary(new Diary(content, emotionSummary));
+ }
+
+ // ========================================
+ // READ 작업
+ // ========================================
+
+ /**
+ * ID로 특정 일기 조회
+ */
+ public Optional getDiaryById(int id) {
+ String selectSQL = """
+ SELECT id, content, emotion_summary, created_at, updated_at
+ FROM diary
+ WHERE id = ?
""";
-
- String now = LocalDateTime.now().format(formatter);
- try (Connection conn = DriverManager.getConnection(DB_URL);
- PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
- pstmt.setString(1, content);
- pstmt.setString(2, emotionSummary);
- pstmt.setString(3, now);
- pstmt.setString(4, now);
+ pstmt.setInt(1, id);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ return Optional.of(mapResultSetToDiary(rs));
+ }
+ }
- int result = pstmt.executeUpdate();
- logger.info("Diary inserted successfully. Rows affected: {}", result);
- return result > 0;
} catch (SQLException e) {
- logger.error("Failed to insert diary", e);
- return false;
+ logger.error("Failed to get diary by ID: {}", id, e);
}
+
+ return Optional.empty();
}
-
+
/**
* 모든 일기 조회 (최신순)
*/
public List getAllDiaries() {
- String selectSQL = """
- SELECT content, emotion_summary, created_at
+ return getAllDiaries(0, 0); // 제한 없음
+ }
+
+ /**
+ * 페이지네이션을 지원하는 일기 조회
+ */
+ public List getAllDiaries(int limit, int offset) {
+ StringBuilder sqlBuilder = new StringBuilder("""
+ SELECT id, content, emotion_summary, created_at, updated_at
FROM diary
ORDER BY created_at DESC
- """;
+ """);
+
+ if (limit > 0) {
+ sqlBuilder.append(" LIMIT ").append(limit);
+ if (offset > 0) {
+ sqlBuilder.append(" OFFSET ").append(offset);
+ }
+ }
List diaries = new ArrayList<>();
- try (Connection conn = DriverManager.getConnection(DB_URL);
+ try (Connection conn = dbUtil.getConnection();
Statement stmt = conn.createStatement();
- ResultSet rs = stmt.executeQuery(selectSQL)) {
+ ResultSet rs = stmt.executeQuery(sqlBuilder.toString())) {
while (rs.next()) {
- Diary diary = new Diary(
- rs.getString("content"),
- rs.getString("emotion_summary"),
- rs.getString("created_at")
- );
- diaries.add(diary);
+ diaries.add(mapResultSetToDiary(rs));
}
- logger.info("Retrieved {} diaries", diaries.size());
+ logger.info("Retrieved {} diaries (limit: {}, offset: {})", diaries.size(), limit, offset);
+
} catch (SQLException e) {
logger.error("Failed to retrieve diaries", e);
}
return diaries;
}
-
+
/**
* 감정별 일기 필터링 조회
*/
public List getDiariesByEmotion(String emotion) {
String selectSQL = """
- SELECT content, emotion_summary, created_at
+ SELECT id, content, emotion_summary, created_at, updated_at
FROM diary
WHERE emotion_summary LIKE ?
ORDER BY created_at DESC
@@ -121,19 +173,14 @@ public List getDiariesByEmotion(String emotion) {
List diaries = new ArrayList<>();
- try (Connection conn = DriverManager.getConnection(DB_URL);
+ try (Connection conn = dbUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
pstmt.setString(1, "%" + emotion + "%");
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
- Diary diary = new Diary(
- rs.getString("content"),
- rs.getString("emotion_summary"),
- rs.getString("created_at")
- );
- diaries.add(diary);
+ diaries.add(mapResultSetToDiary(rs));
}
}
@@ -145,125 +192,304 @@ public List getDiariesByEmotion(String emotion) {
return diaries;
}
-
+
/**
- * 감정 통계 데이터 조회
+ * 특정 날짜의 일기 조회
*/
- public Map getEmotionStatistics() {
- String statsSQL = """
- SELECT emotion_summary, COUNT(*) as count
+ public List getDiariesByDate(String date) {
+ String selectSQL = """
+ SELECT id, content, emotion_summary, created_at, updated_at
FROM diary
- WHERE emotion_summary IS NOT NULL AND emotion_summary != ''
- GROUP BY emotion_summary
- ORDER BY count DESC
+ WHERE DATE(created_at) = ?
+ ORDER BY created_at DESC
""";
- Map stats = new HashMap<>();
+ List diaries = new ArrayList<>();
- try (Connection conn = DriverManager.getConnection(DB_URL);
- Statement stmt = conn.createStatement();
- ResultSet rs = stmt.executeQuery(statsSQL)) {
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
- while (rs.next()) {
- stats.put(rs.getString("emotion_summary"), rs.getInt("count"));
+ pstmt.setString(1, date);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ while (rs.next()) {
+ diaries.add(mapResultSetToDiary(rs));
+ }
}
- logger.info("Retrieved emotion statistics: {}", stats);
+ logger.info("Retrieved {} diaries for date: {}", diaries.size(), date);
} catch (SQLException e) {
- logger.error("Failed to retrieve emotion statistics", e);
+ logger.error("Failed to retrieve diaries for date: {}", date, e);
}
- return stats;
+ return diaries;
}
-
+
/**
- * 최근 N일간의 일기 개수 조회
+ * 날짜 범위로 일기 조회
*/
- public int getDiaryCountForLastDays(int days) {
- String countSQL = """
- SELECT COUNT(*) as count
+ public List getDiariesByDateRange(String startDate, String endDate) {
+ String selectSQL = """
+ SELECT id, content, emotion_summary, created_at, updated_at
FROM diary
- WHERE datetime(created_at) >= datetime('now', '-' || ? || ' days')
+ WHERE DATE(created_at) BETWEEN ? AND ?
+ ORDER BY created_at DESC
""";
- try (Connection conn = DriverManager.getConnection(DB_URL);
- PreparedStatement pstmt = conn.prepareStatement(countSQL)) {
+ List diaries = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
- pstmt.setInt(1, days);
+ pstmt.setString(1, startDate);
+ pstmt.setString(2, endDate);
try (ResultSet rs = pstmt.executeQuery()) {
- if (rs.next()) {
- int count = rs.getInt("count");
- logger.info("Found {} diaries in last {} days", count, days);
- return count;
+ while (rs.next()) {
+ diaries.add(mapResultSetToDiary(rs));
}
- }
+ }
+
+ logger.info("Retrieved {} diaries for date range: {} to {}", diaries.size(), startDate, endDate);
+
} catch (SQLException e) {
- logger.error("Failed to get diary count for last {} days", days, e);
+ logger.error("Failed to retrieve diaries for date range: {} to {}", startDate, endDate, e);
}
- return 0;
+ return diaries;
}
-
+
/**
- * 특정 날짜의 일기 조회
+ * 키워드로 일기 내용 검색
*/
- public List getDiariesByDate(String date) {
+ public List searchDiariesByKeyword(String keyword) {
String selectSQL = """
- SELECT content, emotion_summary, created_at
+ SELECT id, content, emotion_summary, created_at, updated_at
FROM diary
- WHERE DATE(created_at) = ?
+ WHERE content LIKE ? OR emotion_summary LIKE ?
ORDER BY created_at DESC
""";
List diaries = new ArrayList<>();
- try (Connection conn = DriverManager.getConnection(DB_URL);
+ try (Connection conn = dbUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
- pstmt.setString(1, date);
+ String searchPattern = "%" + keyword + "%";
+ pstmt.setString(1, searchPattern);
+ pstmt.setString(2, searchPattern);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
- Diary diary = new Diary(
- rs.getString("content"),
- rs.getString("emotion_summary"),
- rs.getString("created_at")
- );
- diaries.add(diary);
+ diaries.add(mapResultSetToDiary(rs));
}
}
- logger.info("Retrieved {} diaries for date: {}", diaries.size(), date);
+ logger.info("Found {} diaries containing keyword: {}", diaries.size(), keyword);
} catch (SQLException e) {
- logger.error("Failed to retrieve diaries for date: {}", date, e);
+ logger.error("Failed to search diaries by keyword: {}", keyword, e);
}
return diaries;
}
-
+
+ // ========================================
+ // UPDATE 작업
+ // ========================================
+
/**
- * 데이터베이스 연결 테스트
+ * 일기 수정
*/
- public boolean testConnection() {
- try (Connection conn = DriverManager.getConnection(DB_URL)) {
- logger.info("Database connection test successful");
- return true;
+ public boolean updateDiary(Diary diary) {
+ if (diary == null || diary.getId() == null || !diary.isValid()) {
+ logger.error("Invalid diary object provided for update");
+ return false;
+ }
+
+ String updateSQL = """
+ UPDATE diary
+ SET content = ?, emotion_summary = ?, updated_at = ?
+ WHERE id = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(updateSQL)) {
+
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ pstmt.setString(1, diary.getContent());
+ pstmt.setString(2, diary.getEmotionSummary());
+ pstmt.setString(3, now);
+ pstmt.setInt(4, diary.getId());
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ diary.setUpdatedAt(now);
+ logger.info("Diary updated successfully: ID={}", diary.getId());
+ return true;
+ } else {
+ logger.warn("No diary found with ID: {}", diary.getId());
+ return false;
+ }
+
} catch (SQLException e) {
- logger.error("Database connection test failed", e);
+ logger.error("Failed to update diary: ID={}", diary.getId(), e);
return false;
}
}
-
+
+ /**
+ * 일기 내용만 수정
+ */
+ public boolean updateDiaryContent(int id, String content) {
+ Optional diaryOpt = getDiaryById(id);
+ if (diaryOpt.isPresent()) {
+ Diary diary = diaryOpt.get();
+ diary.setContent(content);
+ return updateDiary(diary);
+ }
+ return false;
+ }
+
+ /**
+ * 일기 감정 요약만 수정
+ */
+ public boolean updateDiaryEmotion(int id, String emotionSummary) {
+ Optional diaryOpt = getDiaryById(id);
+ if (diaryOpt.isPresent()) {
+ Diary diary = diaryOpt.get();
+ diary.setEmotionSummary(emotionSummary);
+ return updateDiary(diary);
+ }
+ return false;
+ }
+
+ // ========================================
+ // DELETE 작업
+ // ========================================
+
+ /**
+ * 특정 일기 삭제
+ */
+ public boolean deleteDiary(int id) {
+ String deleteSQL = "DELETE FROM diary WHERE id = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+
+ pstmt.setInt(1, id);
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Diary deleted successfully: ID={}", id);
+ return true;
+ } else {
+ logger.warn("No diary found with ID: {}", id);
+ return false;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to delete diary: ID={}", id, e);
+ return false;
+ }
+ }
+
+ /**
+ * 특정 날짜의 모든 일기 삭제
+ */
+ public int deleteDiariesByDate(String date) {
+ String deleteSQL = "DELETE FROM diary WHERE DATE(created_at) = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+
+ pstmt.setString(1, date);
+ int result = pstmt.executeUpdate();
+
+ logger.info("Deleted {} diaries for date: {}", result, date);
+ return result;
+
+ } catch (SQLException e) {
+ logger.error("Failed to delete diaries for date: {}", date, e);
+ return -1;
+ }
+ }
+
+ // ========================================
+ // 통계 및 집계 작업
+ // ========================================
+
+ /**
+ * 감정별 통계 조회
+ */
+ public Map getEmotionStatistics() {
+ String statsSQL = """
+ SELECT emotion_summary, COUNT(*) as count
+ FROM diary
+ WHERE emotion_summary IS NOT NULL AND emotion_summary != ''
+ GROUP BY emotion_summary
+ ORDER BY count DESC
+ """;
+
+ Map stats = new HashMap<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(statsSQL)) {
+
+ while (rs.next()) {
+ stats.put(rs.getString("emotion_summary"), rs.getInt("count"));
+ }
+
+ logger.info("Retrieved emotion statistics: {} emotions", stats.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to retrieve emotion statistics", e);
+ }
+
+ return stats;
+ }
+
+ /**
+ * 최근 N일간의 일기 개수 조회
+ */
+ public int getDiaryCountForLastDays(int days) {
+ String countSQL = """
+ SELECT COUNT(*) as count
+ FROM diary
+ WHERE datetime(created_at) >= datetime('now', '-' || ? || ' days')
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(countSQL)) {
+
+ pstmt.setInt(1, days);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ int count = rs.getInt("count");
+ logger.info("Found {} diaries in last {} days", count, days);
+ return count;
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get diary count for last {} days", days, e);
+ }
+
+ return 0;
+ }
+
/**
* 전체 일기 개수 조회
*/
public int getTotalDiaryCount() {
String countSQL = "SELECT COUNT(*) as count FROM diary";
- try (Connection conn = DriverManager.getConnection(DB_URL);
+ try (Connection conn = dbUtil.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(countSQL)) {
@@ -279,4 +505,68 @@ public int getTotalDiaryCount() {
return 0;
}
-}
\ No newline at end of file
+
+ /**
+ * 월별 통계 조회
+ */
+ public Map getMonthlyStatistics() {
+ String statsSQL = """
+ SELECT strftime('%Y-%m', created_at) as month, COUNT(*) as count
+ FROM diary
+ GROUP BY strftime('%Y-%m', created_at)
+ ORDER BY month DESC
+ """;
+
+ Map stats = new HashMap<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(statsSQL)) {
+
+ while (rs.next()) {
+ stats.put(rs.getString("month"), rs.getInt("count"));
+ }
+
+ logger.info("Retrieved monthly statistics: {} months", stats.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to retrieve monthly statistics", e);
+ }
+
+ return stats;
+ }
+
+ // ========================================
+ // 유틸리티 메서드
+ // ========================================
+
+ /**
+ * ResultSet을 Diary 객체로 매핑
+ */
+ private Diary mapResultSetToDiary(ResultSet rs) throws SQLException {
+ return new Diary(
+ rs.getInt("id"),
+ rs.getString("content"),
+ rs.getString("emotion_summary"),
+ rs.getString("created_at"),
+ rs.getString("updated_at")
+ );
+ }
+
+ /**
+ * 데이터베이스 연결 테스트
+ */
+ public boolean testConnection() {
+ return dbUtil.testConnection();
+ }
+
+ /**
+ * 일기 유효성 검사
+ */
+ public boolean validateDiary(Diary diary) {
+ if (diary == null) return false;
+ if (diary.getContent() == null || diary.getContent().trim().isEmpty()) return false;
+ if (diary.getContentLength() > 10000) return false; // 최대 길이 제한
+ return true;
+ }
+}
diff --git a/src/main/java/dao/DirectDAOTest.class b/src/main/java/dao/DirectDAOTest.class
new file mode 100644
index 0000000..a072165
Binary files /dev/null and b/src/main/java/dao/DirectDAOTest.class differ
diff --git a/src/main/java/dao/DirectDAOTest.java b/src/main/java/dao/DirectDAOTest.java
new file mode 100644
index 0000000..2b1d69c
--- /dev/null
+++ b/src/main/java/dao/DirectDAOTest.java
@@ -0,0 +1,284 @@
+package dao;
+
+import model.Diary;
+import java.sql.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 완전히 초기화된 데이터베이스에서 DAO 기능을 직접 테스트
+ */
+public class DirectDAOTest {
+ private static final String DB_URL = "jdbc:sqlite:mindiary.db";
+ private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ public static void main(String[] args) {
+ System.out.println("=== Direct DAO Test ===");
+
+ try {
+ // 1. 데이터베이스 연결 테스트
+ System.out.println("1. Testing database connection...");
+ testConnection();
+
+ // 2. 직접 일기 CRUD 테스트
+ System.out.println("2. Testing diary CRUD operations...");
+ testDiaryCRUD();
+
+ // 3. 태그 기능 테스트
+ System.out.println("3. Testing tag operations...");
+ testTagOperations();
+
+ // 4. 감정 분석 테스트
+ System.out.println("4. Testing emotion analysis...");
+ testEmotionAnalysis();
+
+ // 5. 통계 조회 테스트
+ System.out.println("5. Testing statistics queries...");
+ testStatistics();
+
+ // 6. 뷰 조회 테스트
+ System.out.println("6. Testing view queries...");
+ testViews();
+
+ System.out.println("=== All tests completed successfully! ===");
+
+ } catch (Exception e) {
+ System.err.println("Test failed: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ private static void testConnection() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ System.out.println(" ✅ Database connection successful");
+
+ // 테이블 존재 확인
+ String[] tables = {"diary", "user_settings", "backup_log", "emotion_analysis", "tags", "diary_tags", "usage_stats"};
+ try (Statement stmt = conn.createStatement()) {
+ for (String table : tables) {
+ ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table);
+ rs.next();
+ System.out.println(" 📊 Table '" + table + "': " + rs.getInt(1) + " rows");
+ }
+ }
+ }
+ }
+
+ private static void testDiaryCRUD() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ // CREATE: 새 일기 추가
+ String insertSQL = "INSERT INTO diary (content, emotion_summary, created_at, updated_at) VALUES (?, ?, ?, ?)";
+ int newDiaryId;
+ try (PreparedStatement pstmt = conn.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS)) {
+ pstmt.setString(1, "Direct DAO 테스트를 위한 일기입니다. 모든 기능이 잘 작동하고 있습니다!");
+ pstmt.setString(2, "positive");
+ pstmt.setString(3, now);
+ pstmt.setString(4, now);
+
+ int result = pstmt.executeUpdate();
+ try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
+ generatedKeys.next();
+ newDiaryId = generatedKeys.getInt(1);
+ System.out.println(" ✅ Diary created with ID: " + newDiaryId);
+ }
+ }
+
+ // READ: 생성된 일기 조회
+ String selectSQL = "SELECT * FROM diary WHERE id = ?";
+ try (PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+ pstmt.setInt(1, newDiaryId);
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ System.out.println(" ✅ Diary retrieved: " + rs.getString("content").substring(0, 30) + "...");
+ }
+ }
+ }
+
+ // UPDATE: 일기 수정
+ String updateSQL = "UPDATE diary SET content = ?, updated_at = ? WHERE id = ?";
+ try (PreparedStatement pstmt = conn.prepareStatement(updateSQL)) {
+ pstmt.setString(1, "수정된 일기 내용입니다. 업데이트 기능이 정상 작동합니다!");
+ pstmt.setString(2, LocalDateTime.now().format(TIMESTAMP_FORMAT));
+ pstmt.setInt(3, newDiaryId);
+
+ int result = pstmt.executeUpdate();
+ System.out.println(" ✅ Diary updated: " + result + " rows affected");
+ }
+
+ // DELETE: 테스트 일기 삭제
+ String deleteSQL = "DELETE FROM diary WHERE id = ?";
+ try (PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+ pstmt.setInt(1, newDiaryId);
+ int result = pstmt.executeUpdate();
+ System.out.println(" ✅ Diary deleted: " + result + " rows affected");
+ }
+ }
+ }
+
+ private static void testTagOperations() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ // 새 태그 생성
+ String insertTagSQL = "INSERT INTO tags (name, color, description, created_at, usage_count) VALUES (?, ?, ?, ?, ?)";
+ int newTagId;
+ try (PreparedStatement pstmt = conn.prepareStatement(insertTagSQL, Statement.RETURN_GENERATED_KEYS)) {
+ pstmt.setString(1, "테스트태그");
+ pstmt.setString(2, "#ff6b35");
+ pstmt.setString(3, "DAO 테스트용 태그");
+ pstmt.setString(4, now);
+ pstmt.setInt(5, 0);
+
+ pstmt.executeUpdate();
+ try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
+ generatedKeys.next();
+ newTagId = generatedKeys.getInt(1);
+ System.out.println(" ✅ Tag created with ID: " + newTagId);
+ }
+ }
+
+ // 기존 일기에 태그 연결
+ try (Statement stmt = conn.createStatement()) {
+ ResultSet rs = stmt.executeQuery("SELECT id FROM diary LIMIT 1");
+ if (rs.next()) {
+ int diaryId = rs.getInt(1);
+
+ String linkSQL = "INSERT INTO diary_tags (diary_id, tag_id, created_at) VALUES (?, ?, ?)";
+ try (PreparedStatement pstmt = conn.prepareStatement(linkSQL)) {
+ pstmt.setInt(1, diaryId);
+ pstmt.setInt(2, newTagId);
+ pstmt.setString(3, now);
+ pstmt.executeUpdate();
+ System.out.println(" ✅ Tag linked to diary: " + diaryId);
+ }
+
+ // 태그 사용 횟수 확인 (트리거 테스트)
+ ResultSet usageRs = stmt.executeQuery("SELECT usage_count FROM tags WHERE id = " + newTagId);
+ usageRs.next();
+ System.out.println(" ✅ Tag usage count: " + usageRs.getInt(1) + " (trigger working)");
+
+ // 연결 해제
+ stmt.executeUpdate("DELETE FROM diary_tags WHERE diary_id = " + diaryId + " AND tag_id = " + newTagId);
+ System.out.println(" ✅ Tag unlinked from diary");
+ }
+ }
+
+ // 태그 삭제
+ try (Statement stmt = conn.createStatement()) {
+ int result = stmt.executeUpdate("DELETE FROM tags WHERE id = " + newTagId);
+ System.out.println(" ✅ Tag deleted: " + result + " rows affected");
+ }
+ }
+ }
+
+ private static void testEmotionAnalysis() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ // 기존 일기에 감정 분석 추가
+ try (Statement stmt = conn.createStatement()) {
+ ResultSet rs = stmt.executeQuery("SELECT id FROM diary LIMIT 1");
+ if (rs.next()) {
+ int diaryId = rs.getInt(1);
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ String insertSQL = """
+ INSERT INTO emotion_analysis (diary_id, emotion_type, confidence_score, keywords, analysis_method, created_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """;
+
+ try (PreparedStatement pstmt = conn.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS)) {
+ pstmt.setInt(1, diaryId);
+ pstmt.setString(2, "positive");
+ pstmt.setDouble(3, 0.85);
+ pstmt.setString(4, "[\"테스트\", \"성공\", \"좋음\"]");
+ pstmt.setString(5, "rule_based");
+ pstmt.setString(6, now);
+
+ pstmt.executeUpdate();
+ try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
+ generatedKeys.next();
+ int analysisId = generatedKeys.getInt(1);
+ System.out.println(" ✅ Emotion analysis created with ID: " + analysisId);
+
+ // 감정 분석 조회
+ ResultSet analysisRs = stmt.executeQuery("SELECT * FROM emotion_analysis WHERE id = " + analysisId);
+ if (analysisRs.next()) {
+ System.out.println(" ✅ Emotion analysis retrieved: " +
+ analysisRs.getString("emotion_type") + " (" +
+ analysisRs.getDouble("confidence_score") + ")");
+ }
+
+ // 테스트 데이터 정리
+ stmt.executeUpdate("DELETE FROM emotion_analysis WHERE id = " + analysisId);
+ System.out.println(" ✅ Test emotion analysis deleted");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static void testStatistics() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL);
+ Statement stmt = conn.createStatement()) {
+
+ // 전체 통계
+ ResultSet rs1 = stmt.executeQuery("SELECT COUNT(*) as total_diaries FROM diary");
+ rs1.next();
+ System.out.println(" 📊 Total diaries: " + rs1.getInt("total_diaries"));
+
+ ResultSet rs2 = stmt.executeQuery("SELECT COUNT(*) as total_tags FROM tags");
+ rs2.next();
+ System.out.println(" 📊 Total tags: " + rs2.getInt("total_tags"));
+
+ ResultSet rs3 = stmt.executeQuery("SELECT COUNT(*) as total_emotions FROM emotion_analysis");
+ rs3.next();
+ System.out.println(" 📊 Total emotion analyses: " + rs3.getInt("total_emotions"));
+
+ // 감정별 분포
+ ResultSet rs4 = stmt.executeQuery("""
+ SELECT emotion_summary, COUNT(*) as count
+ FROM diary
+ WHERE emotion_summary IS NOT NULL
+ GROUP BY emotion_summary
+ """);
+ System.out.println(" 📊 Emotion distribution:");
+ while (rs4.next()) {
+ System.out.println(" - " + rs4.getString("emotion_summary") + ": " + rs4.getInt("count"));
+ }
+ }
+ }
+
+ private static void testViews() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL);
+ Statement stmt = conn.createStatement()) {
+
+ // recent_diaries 뷰 테스트
+ ResultSet rs1 = stmt.executeQuery("SELECT COUNT(*) FROM recent_diaries");
+ rs1.next();
+ System.out.println(" 📄 Recent diaries view: " + rs1.getInt(1) + " entries");
+
+ // emotion_stats 뷰 테스트
+ ResultSet rs2 = stmt.executeQuery("SELECT COUNT(*) FROM emotion_stats");
+ rs2.next();
+ System.out.println(" 😊 Emotion stats view: " + rs2.getInt(1) + " emotions");
+
+ // monthly_stats 뷰 테스트
+ ResultSet rs3 = stmt.executeQuery("SELECT COUNT(*) FROM monthly_stats");
+ rs3.next();
+ System.out.println(" 📅 Monthly stats view: " + rs3.getInt(1) + " months");
+
+ // 뷰 상세 데이터 샘플
+ ResultSet rs4 = stmt.executeQuery("SELECT * FROM emotion_stats LIMIT 3");
+ System.out.println(" 📊 Emotion stats sample:");
+ while (rs4.next()) {
+ System.out.println(" - " + rs4.getString("emotion_summary") +
+ ": " + rs4.getInt("count") + " entries, avg length: " +
+ rs4.getDouble("avg_content_length"));
+ }
+ }
+ }
+}
diff --git a/src/main/java/dao/EmotionAnalysisDAO$EmotionAnalysis.class b/src/main/java/dao/EmotionAnalysisDAO$EmotionAnalysis.class
new file mode 100644
index 0000000..c88ca01
Binary files /dev/null and b/src/main/java/dao/EmotionAnalysisDAO$EmotionAnalysis.class differ
diff --git a/src/main/java/dao/EmotionAnalysisDAO.class b/src/main/java/dao/EmotionAnalysisDAO.class
new file mode 100644
index 0000000..12964b5
Binary files /dev/null and b/src/main/java/dao/EmotionAnalysisDAO.class differ
diff --git a/src/main/java/dao/EmotionAnalysisDAO.java b/src/main/java/dao/EmotionAnalysisDAO.java
new file mode 100644
index 0000000..f0185b5
--- /dev/null
+++ b/src/main/java/dao/EmotionAnalysisDAO.java
@@ -0,0 +1,569 @@
+package dao;
+
+import util.DatabaseUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * 감정 분석 데이터 액세스 객체
+ * 일기의 감정 분석 결과를 관리합니다.
+ */
+public class EmotionAnalysisDAO {
+ private static final Logger logger = LoggerFactory.getLogger(EmotionAnalysisDAO.class);
+ private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private final DatabaseUtil dbUtil;
+
+ public EmotionAnalysisDAO() {
+ this.dbUtil = DatabaseUtil.getInstance();
+ }
+
+ // ========================================
+ // EmotionAnalysis 모델 클래스
+ // ========================================
+
+ public static class EmotionAnalysis {
+ private Integer id;
+ private int diaryId;
+ private String emotionType;
+ private Double confidenceScore;
+ private String keywords;
+ private String analysisMethod;
+ private String createdAt;
+
+ public EmotionAnalysis() {}
+
+ public EmotionAnalysis(int diaryId, String emotionType, Double confidenceScore,
+ String keywords, String analysisMethod) {
+ this.diaryId = diaryId;
+ this.emotionType = emotionType;
+ this.confidenceScore = confidenceScore;
+ this.keywords = keywords;
+ this.analysisMethod = analysisMethod;
+ this.createdAt = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+ }
+
+ // Getters and Setters
+ public Integer getId() { return id; }
+ public void setId(Integer id) { this.id = id; }
+
+ public int getDiaryId() { return diaryId; }
+ public void setDiaryId(int diaryId) { this.diaryId = diaryId; }
+
+ public String getEmotionType() { return emotionType; }
+ public void setEmotionType(String emotionType) { this.emotionType = emotionType; }
+
+ public Double getConfidenceScore() { return confidenceScore; }
+ public void setConfidenceScore(Double confidenceScore) { this.confidenceScore = confidenceScore; }
+
+ public String getKeywords() { return keywords; }
+ public void setKeywords(String keywords) { this.keywords = keywords; }
+
+ public String getAnalysisMethod() { return analysisMethod; }
+ public void setAnalysisMethod(String analysisMethod) { this.analysisMethod = analysisMethod; }
+
+ public String getCreatedAt() { return createdAt; }
+ public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
+
+ @Override
+ public String toString() {
+ return String.format("EmotionAnalysis{id=%d, diaryId=%d, emotion='%s', confidence=%.2f, method='%s'}",
+ id, diaryId, emotionType, confidenceScore, analysisMethod);
+ }
+ }
+
+ // ========================================
+ // CREATE 작업
+ // ========================================
+
+ /**
+ * 새로운 감정 분석 결과 저장
+ */
+ public boolean saveEmotionAnalysis(EmotionAnalysis analysis) {
+ if (analysis == null || analysis.getDiaryId() <= 0 ||
+ analysis.getEmotionType() == null || analysis.getEmotionType().trim().isEmpty()) {
+ logger.error("Invalid emotion analysis provided for saving");
+ return false;
+ }
+
+ String insertSQL = """
+ INSERT INTO emotion_analysis (diary_id, emotion_type, confidence_score, keywords, analysis_method, created_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS)) {
+
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ pstmt.setInt(1, analysis.getDiaryId());
+ pstmt.setString(2, analysis.getEmotionType());
+ pstmt.setObject(3, analysis.getConfidenceScore());
+ pstmt.setString(4, analysis.getKeywords());
+ pstmt.setString(5, analysis.getAnalysisMethod() != null ? analysis.getAnalysisMethod() : "rule_based");
+ pstmt.setString(6, now);
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
+ if (generatedKeys.next()) {
+ analysis.setId(generatedKeys.getInt(1));
+ analysis.setCreatedAt(now);
+ logger.info("Emotion analysis saved successfully with ID: {}", analysis.getId());
+ return true;
+ }
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to save emotion analysis", e);
+ }
+
+ return false;
+ }
+
+ /**
+ * 편의 메서드: 기본 정보로 감정 분석 저장
+ */
+ public boolean saveEmotionAnalysis(int diaryId, String emotionType, double confidenceScore) {
+ EmotionAnalysis analysis = new EmotionAnalysis(diaryId, emotionType, confidenceScore, null, "rule_based");
+ return saveEmotionAnalysis(analysis);
+ }
+
+ // ========================================
+ // READ 작업
+ // ========================================
+
+ /**
+ * 특정 일기의 감정 분석 결과 조회
+ */
+ public Optional getEmotionAnalysisByDiaryId(int diaryId) {
+ String selectSQL = """
+ SELECT id, diary_id, emotion_type, confidence_score, keywords, analysis_method, created_at
+ FROM emotion_analysis
+ WHERE diary_id = ?
+ ORDER BY created_at DESC
+ LIMIT 1
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setInt(1, diaryId);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ return Optional.of(mapResultSetToEmotionAnalysis(rs));
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get emotion analysis for diary ID: {}", diaryId, e);
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * ID로 감정 분석 결과 조회
+ */
+ public Optional getEmotionAnalysisById(int id) {
+ String selectSQL = """
+ SELECT id, diary_id, emotion_type, confidence_score, keywords, analysis_method, created_at
+ FROM emotion_analysis
+ WHERE id = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setInt(1, id);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ return Optional.of(mapResultSetToEmotionAnalysis(rs));
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get emotion analysis by ID: {}", id, e);
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * 특정 감정 유형의 모든 분석 결과 조회
+ */
+ public List getEmotionAnalysesByType(String emotionType) {
+ String selectSQL = """
+ SELECT id, diary_id, emotion_type, confidence_score, keywords, analysis_method, created_at
+ FROM emotion_analysis
+ WHERE emotion_type = ?
+ ORDER BY created_at DESC
+ """;
+
+ List analyses = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setString(1, emotionType);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ while (rs.next()) {
+ analyses.add(mapResultSetToEmotionAnalysis(rs));
+ }
+ }
+
+ logger.info("Retrieved {} emotion analyses for type: {}", analyses.size(), emotionType);
+
+ } catch (SQLException e) {
+ logger.error("Failed to get emotion analyses by type: {}", emotionType, e);
+ }
+
+ return analyses;
+ }
+
+ /**
+ * 신뢰도 범위로 감정 분석 결과 조회
+ */
+ public List getEmotionAnalysesByConfidenceRange(double minConfidence, double maxConfidence) {
+ String selectSQL = """
+ SELECT id, diary_id, emotion_type, confidence_score, keywords, analysis_method, created_at
+ FROM emotion_analysis
+ WHERE confidence_score BETWEEN ? AND ?
+ ORDER BY confidence_score DESC, created_at DESC
+ """;
+
+ List analyses = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setDouble(1, minConfidence);
+ pstmt.setDouble(2, maxConfidence);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ while (rs.next()) {
+ analyses.add(mapResultSetToEmotionAnalysis(rs));
+ }
+ }
+
+ logger.info("Retrieved {} emotion analyses with confidence between {} and {}",
+ analyses.size(), minConfidence, maxConfidence);
+
+ } catch (SQLException e) {
+ logger.error("Failed to get emotion analyses by confidence range", e);
+ }
+
+ return analyses;
+ }
+
+ // ========================================
+ // UPDATE 작업
+ // ========================================
+
+ /**
+ * 감정 분석 결과 업데이트
+ */
+ public boolean updateEmotionAnalysis(EmotionAnalysis analysis) {
+ if (analysis == null || analysis.getId() == null) {
+ logger.error("Invalid emotion analysis provided for update");
+ return false;
+ }
+
+ String updateSQL = """
+ UPDATE emotion_analysis
+ SET emotion_type = ?, confidence_score = ?, keywords = ?, analysis_method = ?
+ WHERE id = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(updateSQL)) {
+
+ pstmt.setString(1, analysis.getEmotionType());
+ pstmt.setObject(2, analysis.getConfidenceScore());
+ pstmt.setString(3, analysis.getKeywords());
+ pstmt.setString(4, analysis.getAnalysisMethod());
+ pstmt.setInt(5, analysis.getId());
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Emotion analysis updated successfully: ID={}", analysis.getId());
+ return true;
+ } else {
+ logger.warn("No emotion analysis found with ID: {}", analysis.getId());
+ return false;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to update emotion analysis: ID={}", analysis.getId(), e);
+ return false;
+ }
+ }
+
+ // ========================================
+ // DELETE 작업
+ // ========================================
+
+ /**
+ * 특정 감정 분석 결과 삭제
+ */
+ public boolean deleteEmotionAnalysis(int id) {
+ String deleteSQL = "DELETE FROM emotion_analysis WHERE id = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+
+ pstmt.setInt(1, id);
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Emotion analysis deleted successfully: ID={}", id);
+ return true;
+ } else {
+ logger.warn("No emotion analysis found with ID: {}", id);
+ return false;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to delete emotion analysis: ID={}", id, e);
+ return false;
+ }
+ }
+
+ /**
+ * 특정 일기의 모든 감정 분석 결과 삭제
+ */
+ public int deleteEmotionAnalysesByDiaryId(int diaryId) {
+ String deleteSQL = "DELETE FROM emotion_analysis WHERE diary_id = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+
+ pstmt.setInt(1, diaryId);
+ int result = pstmt.executeUpdate();
+
+ logger.info("Deleted {} emotion analyses for diary ID: {}", result, diaryId);
+ return result;
+
+ } catch (SQLException e) {
+ logger.error("Failed to delete emotion analyses for diary ID: {}", diaryId, e);
+ return -1;
+ }
+ }
+
+ // ========================================
+ // 통계 및 분석 작업
+ // ========================================
+
+ /**
+ * 감정 유형별 분포 통계
+ */
+ public Map getEmotionTypeDistribution() {
+ String statsSQL = """
+ SELECT emotion_type, COUNT(*) as count
+ FROM emotion_analysis
+ GROUP BY emotion_type
+ ORDER BY count DESC
+ """;
+
+ Map distribution = new HashMap<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(statsSQL)) {
+
+ while (rs.next()) {
+ distribution.put(rs.getString("emotion_type"), rs.getInt("count"));
+ }
+
+ logger.info("Retrieved emotion type distribution: {} types", distribution.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to get emotion type distribution", e);
+ }
+
+ return distribution;
+ }
+
+ /**
+ * 평균 신뢰도 점수 조회
+ */
+ public double getAverageConfidenceScore() {
+ String avgSQL = "SELECT AVG(confidence_score) as avg_confidence FROM emotion_analysis WHERE confidence_score IS NOT NULL";
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(avgSQL)) {
+
+ if (rs.next()) {
+ double avgConfidence = rs.getDouble("avg_confidence");
+ logger.info("Average confidence score: {}", avgConfidence);
+ return avgConfidence;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get average confidence score", e);
+ }
+
+ return 0.0;
+ }
+
+ /**
+ * 감정 유형별 평균 신뢰도
+ */
+ public Map getAverageConfidenceByEmotionType() {
+ String avgSQL = """
+ SELECT emotion_type, AVG(confidence_score) as avg_confidence
+ FROM emotion_analysis
+ WHERE confidence_score IS NOT NULL
+ GROUP BY emotion_type
+ ORDER BY avg_confidence DESC
+ """;
+
+ Map avgConfidences = new HashMap<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(avgSQL)) {
+
+ while (rs.next()) {
+ avgConfidences.put(rs.getString("emotion_type"), rs.getDouble("avg_confidence"));
+ }
+
+ logger.info("Retrieved average confidence by emotion type: {} types", avgConfidences.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to get average confidence by emotion type", e);
+ }
+
+ return avgConfidences;
+ }
+
+ /**
+ * 최근 N일간 감정 분석 개수
+ */
+ public int getEmotionAnalysisCountForLastDays(int days) {
+ String countSQL = """
+ SELECT COUNT(*) as count
+ FROM emotion_analysis
+ WHERE datetime(created_at) >= datetime('now', '-' || ? || ' days')
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(countSQL)) {
+
+ pstmt.setInt(1, days);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ int count = rs.getInt("count");
+ logger.info("Found {} emotion analyses in last {} days", count, days);
+ return count;
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get emotion analysis count for last {} days", days, e);
+ }
+
+ return 0;
+ }
+
+ /**
+ * 높은 신뢰도 감정 분석 조회 (임계값 이상)
+ */
+ public List getHighConfidenceAnalyses(double threshold) {
+ String selectSQL = """
+ SELECT id, diary_id, emotion_type, confidence_score, keywords, analysis_method, created_at
+ FROM emotion_analysis
+ WHERE confidence_score >= ?
+ ORDER BY confidence_score DESC, created_at DESC
+ """;
+
+ List analyses = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setDouble(1, threshold);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ while (rs.next()) {
+ analyses.add(mapResultSetToEmotionAnalysis(rs));
+ }
+ }
+
+ logger.info("Retrieved {} high confidence analyses (threshold: {})", analyses.size(), threshold);
+
+ } catch (SQLException e) {
+ logger.error("Failed to get high confidence analyses", e);
+ }
+
+ return analyses;
+ }
+
+ // ========================================
+ // 유틸리티 메서드
+ // ========================================
+
+ /**
+ * ResultSet을 EmotionAnalysis 객체로 매핑
+ */
+ private EmotionAnalysis mapResultSetToEmotionAnalysis(ResultSet rs) throws SQLException {
+ EmotionAnalysis analysis = new EmotionAnalysis();
+ analysis.setId(rs.getInt("id"));
+ analysis.setDiaryId(rs.getInt("diary_id"));
+ analysis.setEmotionType(rs.getString("emotion_type"));
+
+ double confidence = rs.getDouble("confidence_score");
+ analysis.setConfidenceScore(rs.wasNull() ? null : confidence);
+
+ analysis.setKeywords(rs.getString("keywords"));
+ analysis.setAnalysisMethod(rs.getString("analysis_method"));
+ analysis.setCreatedAt(rs.getString("created_at"));
+
+ return analysis;
+ }
+
+ /**
+ * 감정 분석 유효성 검사
+ */
+ public boolean validateEmotionAnalysis(EmotionAnalysis analysis) {
+ if (analysis == null) return false;
+ if (analysis.getDiaryId() <= 0) return false;
+ if (analysis.getEmotionType() == null || analysis.getEmotionType().trim().isEmpty()) return false;
+
+ // 지원되는 감정 유형 확인
+ String[] supportedEmotions = {"positive", "negative", "neutral", "mixed"};
+ boolean validEmotion = false;
+ for (String emotion : supportedEmotions) {
+ if (emotion.equals(analysis.getEmotionType())) {
+ validEmotion = true;
+ break;
+ }
+ }
+ if (!validEmotion) return false;
+
+ // 신뢰도 점수 범위 확인
+ if (analysis.getConfidenceScore() != null) {
+ double confidence = analysis.getConfidenceScore();
+ if (confidence < 0.0 || confidence > 1.0) return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/dao/StatisticsDAO$DailyStats.class b/src/main/java/dao/StatisticsDAO$DailyStats.class
new file mode 100644
index 0000000..37947bd
Binary files /dev/null and b/src/main/java/dao/StatisticsDAO$DailyStats.class differ
diff --git a/src/main/java/dao/StatisticsDAO$EmotionStats.class b/src/main/java/dao/StatisticsDAO$EmotionStats.class
new file mode 100644
index 0000000..109d1fa
Binary files /dev/null and b/src/main/java/dao/StatisticsDAO$EmotionStats.class differ
diff --git a/src/main/java/dao/StatisticsDAO$MonthlyStats.class b/src/main/java/dao/StatisticsDAO$MonthlyStats.class
new file mode 100644
index 0000000..8f9c083
Binary files /dev/null and b/src/main/java/dao/StatisticsDAO$MonthlyStats.class differ
diff --git a/src/main/java/dao/StatisticsDAO.class b/src/main/java/dao/StatisticsDAO.class
new file mode 100644
index 0000000..b21fb29
Binary files /dev/null and b/src/main/java/dao/StatisticsDAO.class differ
diff --git a/src/main/java/dao/StatisticsDAO.java b/src/main/java/dao/StatisticsDAO.java
new file mode 100644
index 0000000..d9b06cf
--- /dev/null
+++ b/src/main/java/dao/StatisticsDAO.java
@@ -0,0 +1,528 @@
+package dao;
+
+import util.DatabaseUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.*;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 통계 데이터 액세스 객체
+ * 사용 통계, 감정 분석 통계, 월별 통계 등을 관리합니다.
+ */
+public class StatisticsDAO {
+ private static final Logger logger = LoggerFactory.getLogger(StatisticsDAO.class);
+ private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+ private final DatabaseUtil dbUtil;
+
+ public StatisticsDAO() {
+ this.dbUtil = DatabaseUtil.getInstance();
+ }
+
+ // ========================================
+ // 통계 모델 클래스들
+ // ========================================
+
+ public static class DailyStats {
+ private String statDate;
+ private int diariesCreated;
+ private int totalCharacters;
+ private int emotionsAnalyzed;
+ private int tagsUsed;
+
+ // Getters and Setters
+ public String getStatDate() { return statDate; }
+ public void setStatDate(String statDate) { this.statDate = statDate; }
+
+ public int getDiariesCreated() { return diariesCreated; }
+ public void setDiariesCreated(int diariesCreated) { this.diariesCreated = diariesCreated; }
+
+ public int getTotalCharacters() { return totalCharacters; }
+ public void setTotalCharacters(int totalCharacters) { this.totalCharacters = totalCharacters; }
+
+ public int getEmotionsAnalyzed() { return emotionsAnalyzed; }
+ public void setEmotionsAnalyzed(int emotionsAnalyzed) { this.emotionsAnalyzed = emotionsAnalyzed; }
+
+ public int getTagsUsed() { return tagsUsed; }
+ public void setTagsUsed(int tagsUsed) { this.tagsUsed = tagsUsed; }
+
+ @Override
+ public String toString() {
+ return String.format("DailyStats{date='%s', diaries=%d, chars=%d, emotions=%d, tags=%d}",
+ statDate, diariesCreated, totalCharacters, emotionsAnalyzed, tagsUsed);
+ }
+ }
+
+ public static class MonthlyStats {
+ private String month;
+ private int diaryCount;
+ private int totalCharacters;
+ private double avgContentLength;
+ private int uniqueEmotions;
+
+ // Getters and Setters
+ public String getMonth() { return month; }
+ public void setMonth(String month) { this.month = month; }
+
+ public int getDiaryCount() { return diaryCount; }
+ public void setDiaryCount(int diaryCount) { this.diaryCount = diaryCount; }
+
+ public int getTotalCharacters() { return totalCharacters; }
+ public void setTotalCharacters(int totalCharacters) { this.totalCharacters = totalCharacters; }
+
+ public double getAvgContentLength() { return avgContentLength; }
+ public void setAvgContentLength(double avgContentLength) { this.avgContentLength = avgContentLength; }
+
+ public int getUniqueEmotions() { return uniqueEmotions; }
+ public void setUniqueEmotions(int uniqueEmotions) { this.uniqueEmotions = uniqueEmotions; }
+
+ @Override
+ public String toString() {
+ return String.format("MonthlyStats{month='%s', diaries=%d, chars=%d, avgLen=%.1f, emotions=%d}",
+ month, diaryCount, totalCharacters, avgContentLength, uniqueEmotions);
+ }
+ }
+
+ public static class EmotionStats {
+ private String emotionSummary;
+ private int count;
+ private double avgContentLength;
+ private String firstEntry;
+ private String lastEntry;
+
+ // Getters and Setters
+ public String getEmotionSummary() { return emotionSummary; }
+ public void setEmotionSummary(String emotionSummary) { this.emotionSummary = emotionSummary; }
+
+ public int getCount() { return count; }
+ public void setCount(int count) { this.count = count; }
+
+ public double getAvgContentLength() { return avgContentLength; }
+ public void setAvgContentLength(double avgContentLength) { this.avgContentLength = avgContentLength; }
+
+ public String getFirstEntry() { return firstEntry; }
+ public void setFirstEntry(String firstEntry) { this.firstEntry = firstEntry; }
+
+ public String getLastEntry() { return lastEntry; }
+ public void setLastEntry(String lastEntry) { this.lastEntry = lastEntry; }
+
+ @Override
+ public String toString() {
+ return String.format("EmotionStats{emotion='%s', count=%d, avgLen=%.1f}",
+ emotionSummary, count, avgContentLength);
+ }
+ }
+
+ // ========================================
+ // 일일 통계 관리
+ // ========================================
+
+ /**
+ * 특정 날짜의 통계 업데이트 또는 생성
+ */
+ public boolean updateDailyStats(String date) {
+ String upsertSQL = """
+ INSERT OR REPLACE INTO usage_stats (stat_date, diaries_created, total_characters, emotions_analyzed, tags_used)
+ SELECT
+ ? as stat_date,
+ COUNT(*) as diaries_created,
+ COALESCE(SUM(length(content)), 0) as total_characters,
+ COUNT(CASE WHEN emotion_summary IS NOT NULL AND emotion_summary != '' THEN 1 END) as emotions_analyzed,
+ COALESCE((SELECT COUNT(*) FROM diary_tags dt INNER JOIN diary d ON dt.diary_id = d.id WHERE date(d.created_at) = ?), 0) as tags_used
+ FROM diary
+ WHERE date(created_at) = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(upsertSQL)) {
+
+ pstmt.setString(1, date);
+ pstmt.setString(2, date);
+ pstmt.setString(3, date);
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Daily stats updated for date: {}", date);
+ return true;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to update daily stats for date: {}", date, e);
+ }
+
+ return false;
+ }
+
+ /**
+ * 오늘의 통계 업데이트
+ */
+ public boolean updateTodayStats() {
+ String today = LocalDate.now().format(DATE_FORMAT);
+ return updateDailyStats(today);
+ }
+
+ /**
+ * 특정 날짜의 통계 조회
+ */
+ public DailyStats getDailyStats(String date) {
+ String selectSQL = """
+ SELECT stat_date, diaries_created, total_characters, emotions_analyzed, tags_used
+ FROM usage_stats
+ WHERE stat_date = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setString(1, date);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ return mapResultSetToDailyStats(rs);
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get daily stats for date: {}", date, e);
+ }
+
+ return null;
+ }
+
+ /**
+ * 최근 N일간의 일일 통계 조회
+ */
+ public List getRecentDailyStats(int days) {
+ String selectSQL = """
+ SELECT stat_date, diaries_created, total_characters, emotions_analyzed, tags_used
+ FROM usage_stats
+ WHERE stat_date >= date('now', '-' || ? || ' days')
+ ORDER BY stat_date DESC
+ """;
+
+ List statsList = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setInt(1, days);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ while (rs.next()) {
+ statsList.add(mapResultSetToDailyStats(rs));
+ }
+ }
+
+ logger.info("Retrieved {} daily stats for last {} days", statsList.size(), days);
+
+ } catch (SQLException e) {
+ logger.error("Failed to get recent daily stats", e);
+ }
+
+ return statsList;
+ }
+
+ // ========================================
+ // 월별 통계 (뷰 활용)
+ // ========================================
+
+ /**
+ * 월별 통계 조회 (뷰 활용)
+ */
+ public List getMonthlyStats() {
+ return getMonthlyStats(12); // 기본 12개월
+ }
+
+ /**
+ * 최근 N개월 통계 조회
+ */
+ public List getMonthlyStats(int months) {
+ String selectSQL = """
+ SELECT month, diary_count, total_characters, avg_content_length, unique_emotions
+ FROM monthly_stats
+ ORDER BY month DESC
+ LIMIT ?
+ """;
+
+ List statsList = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setInt(1, months);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ while (rs.next()) {
+ statsList.add(mapResultSetToMonthlyStats(rs));
+ }
+ }
+
+ logger.info("Retrieved {} monthly stats", statsList.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to get monthly stats", e);
+ }
+
+ return statsList;
+ }
+
+ // ========================================
+ // 감정 통계 (뷰 활용)
+ // ========================================
+
+ /**
+ * 감정별 통계 조회 (뷰 활용)
+ */
+ public List getEmotionStats() {
+ String selectSQL = """
+ SELECT emotion_summary, count, avg_content_length, first_entry, last_entry
+ FROM emotion_stats
+ ORDER BY count DESC
+ """;
+
+ List statsList = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(selectSQL)) {
+
+ while (rs.next()) {
+ statsList.add(mapResultSetToEmotionStats(rs));
+ }
+
+ logger.info("Retrieved {} emotion stats", statsList.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to get emotion stats", e);
+ }
+
+ return statsList;
+ }
+
+ // ========================================
+ // 종합 통계
+ // ========================================
+
+ /**
+ * 전체 애플리케이션 통계 조회
+ */
+ public Map getOverallStats() {
+ Map stats = new HashMap<>();
+
+ try (Connection conn = dbUtil.getConnection()) {
+
+ // 기본 통계
+ stats.put("totalDiaries", getTotalCount(conn, "diary"));
+ stats.put("totalTags", getTotalCount(conn, "tags"));
+ stats.put("totalEmotionAnalyses", getTotalCount(conn, "emotion_analysis"));
+ stats.put("totalBackups", getTotalCount(conn, "backup_log"));
+
+ // 평균 통계
+ stats.put("avgDiaryLength", getAverageDiaryLength(conn));
+ stats.put("avgDiariesPerDay", getAverageDiariesPerDay(conn));
+
+ // 최근 활동
+ stats.put("diariesThisWeek", getDiariesInPeriod(conn, 7));
+ stats.put("diariesThisMonth", getDiariesInPeriod(conn, 30));
+
+ // 가장 많이 사용된 감정
+ stats.put("topEmotion", getTopEmotion(conn));
+
+ // 가장 많이 사용된 태그
+ stats.put("topTag", getTopTag(conn));
+
+ logger.info("Retrieved overall application statistics");
+
+ } catch (SQLException e) {
+ logger.error("Failed to get overall stats", e);
+ }
+
+ return stats;
+ }
+
+ /**
+ * 최근 활동 요약
+ */
+ public Map getRecentActivitySummary() {
+ Map activity = new HashMap<>();
+
+ try (Connection conn = dbUtil.getConnection()) {
+
+ // 최근 7일 활동
+ activity.put("diariesLast7Days", getDiariesInPeriod(conn, 7));
+ activity.put("avgLengthLast7Days", getAverageDiaryLengthInPeriod(conn, 7));
+ activity.put("emotionsLast7Days", getEmotionCountInPeriod(conn, 7));
+ activity.put("tagsLast7Days", getTagUsageInPeriod(conn, 7));
+
+ // 어제와 오늘 비교
+ activity.put("diariesToday", getDiariesInPeriod(conn, 1));
+ activity.put("diariesYesterday", getDiariesInPeriod(conn, 1, 1)); // 1일 전부터 1일간
+
+ logger.info("Retrieved recent activity summary");
+
+ } catch (SQLException e) {
+ logger.error("Failed to get recent activity summary", e);
+ }
+
+ return activity;
+ }
+
+ // ========================================
+ // 헬퍼 메서드들
+ // ========================================
+
+ private int getTotalCount(Connection conn, String tableName) throws SQLException {
+ String sql = "SELECT COUNT(*) FROM " + tableName;
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ return rs.next() ? rs.getInt(1) : 0;
+ }
+ }
+
+ private double getAverageDiaryLength(Connection conn) throws SQLException {
+ String sql = "SELECT AVG(length(content)) FROM diary";
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ return rs.next() ? rs.getDouble(1) : 0.0;
+ }
+ }
+
+ private double getAverageDiariesPerDay(Connection conn) throws SQLException {
+ String sql = """
+ SELECT CAST(COUNT(*) AS REAL) / CAST(COUNT(DISTINCT date(created_at)) AS REAL) as avg_per_day
+ FROM diary
+ """;
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ return rs.next() ? rs.getDouble(1) : 0.0;
+ }
+ }
+
+ private int getDiariesInPeriod(Connection conn, int days) throws SQLException {
+ return getDiariesInPeriod(conn, days, 0);
+ }
+
+ private int getDiariesInPeriod(Connection conn, int days, int offsetDays) throws SQLException {
+ String sql = """
+ SELECT COUNT(*) FROM diary
+ WHERE date(created_at) >= date('now', '-' || ? || ' days')
+ AND date(created_at) < date('now', '-' || ? || ' days')
+ """;
+ try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
+ pstmt.setInt(1, days + offsetDays);
+ pstmt.setInt(2, offsetDays);
+ try (ResultSet rs = pstmt.executeQuery()) {
+ return rs.next() ? rs.getInt(1) : 0;
+ }
+ }
+ }
+
+ private double getAverageDiaryLengthInPeriod(Connection conn, int days) throws SQLException {
+ String sql = """
+ SELECT AVG(length(content)) FROM diary
+ WHERE date(created_at) >= date('now', '-' || ? || ' days')
+ """;
+ try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
+ pstmt.setInt(1, days);
+ try (ResultSet rs = pstmt.executeQuery()) {
+ return rs.next() ? rs.getDouble(1) : 0.0;
+ }
+ }
+ }
+
+ private int getEmotionCountInPeriod(Connection conn, int days) throws SQLException {
+ String sql = """
+ SELECT COUNT(*) FROM diary
+ WHERE date(created_at) >= date('now', '-' || ? || ' days')
+ AND emotion_summary IS NOT NULL AND emotion_summary != ''
+ """;
+ try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
+ pstmt.setInt(1, days);
+ try (ResultSet rs = pstmt.executeQuery()) {
+ return rs.next() ? rs.getInt(1) : 0;
+ }
+ }
+ }
+
+ private int getTagUsageInPeriod(Connection conn, int days) throws SQLException {
+ String sql = """
+ SELECT COUNT(*) FROM diary_tags dt
+ INNER JOIN diary d ON dt.diary_id = d.id
+ WHERE date(d.created_at) >= date('now', '-' || ? || ' days')
+ """;
+ try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
+ pstmt.setInt(1, days);
+ try (ResultSet rs = pstmt.executeQuery()) {
+ return rs.next() ? rs.getInt(1) : 0;
+ }
+ }
+ }
+
+ private String getTopEmotion(Connection conn) throws SQLException {
+ String sql = """
+ SELECT emotion_summary FROM diary
+ WHERE emotion_summary IS NOT NULL AND emotion_summary != ''
+ GROUP BY emotion_summary
+ ORDER BY COUNT(*) DESC
+ LIMIT 1
+ """;
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ return rs.next() ? rs.getString(1) : "없음";
+ }
+ }
+
+ private String getTopTag(Connection conn) throws SQLException {
+ String sql = """
+ SELECT name FROM tags
+ ORDER BY usage_count DESC
+ LIMIT 1
+ """;
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ return rs.next() ? rs.getString(1) : "없음";
+ }
+ }
+
+ // ========================================
+ // 매핑 메서드들
+ // ========================================
+
+ private DailyStats mapResultSetToDailyStats(ResultSet rs) throws SQLException {
+ DailyStats stats = new DailyStats();
+ stats.setStatDate(rs.getString("stat_date"));
+ stats.setDiariesCreated(rs.getInt("diaries_created"));
+ stats.setTotalCharacters(rs.getInt("total_characters"));
+ stats.setEmotionsAnalyzed(rs.getInt("emotions_analyzed"));
+ stats.setTagsUsed(rs.getInt("tags_used"));
+ return stats;
+ }
+
+ private MonthlyStats mapResultSetToMonthlyStats(ResultSet rs) throws SQLException {
+ MonthlyStats stats = new MonthlyStats();
+ stats.setMonth(rs.getString("month"));
+ stats.setDiaryCount(rs.getInt("diary_count"));
+ stats.setTotalCharacters(rs.getInt("total_characters"));
+ stats.setAvgContentLength(rs.getDouble("avg_content_length"));
+ stats.setUniqueEmotions(rs.getInt("unique_emotions"));
+ return stats;
+ }
+
+ private EmotionStats mapResultSetToEmotionStats(ResultSet rs) throws SQLException {
+ EmotionStats stats = new EmotionStats();
+ stats.setEmotionSummary(rs.getString("emotion_summary"));
+ stats.setCount(rs.getInt("count"));
+ stats.setAvgContentLength(rs.getDouble("avg_content_length"));
+ stats.setFirstEntry(rs.getString("first_entry"));
+ stats.setLastEntry(rs.getString("last_entry"));
+ return stats;
+ }
+}
diff --git a/src/main/java/dao/TagDAO$Tag.class b/src/main/java/dao/TagDAO$Tag.class
new file mode 100644
index 0000000..e230445
Binary files /dev/null and b/src/main/java/dao/TagDAO$Tag.class differ
diff --git a/src/main/java/dao/TagDAO.class b/src/main/java/dao/TagDAO.class
new file mode 100644
index 0000000..32904a2
Binary files /dev/null and b/src/main/java/dao/TagDAO.class differ
diff --git a/src/main/java/dao/TagDAO.java b/src/main/java/dao/TagDAO.java
new file mode 100644
index 0000000..b89660d
--- /dev/null
+++ b/src/main/java/dao/TagDAO.java
@@ -0,0 +1,531 @@
+package dao;
+
+import util.DatabaseUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 태그 데이터 액세스 객체
+ * 태그 관리 및 일기-태그 연결 기능을 제공합니다.
+ */
+public class TagDAO {
+ private static final Logger logger = LoggerFactory.getLogger(TagDAO.class);
+ private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private final DatabaseUtil dbUtil;
+
+ public TagDAO() {
+ this.dbUtil = DatabaseUtil.getInstance();
+ }
+
+ // ========================================
+ // Tag 모델 클래스
+ // ========================================
+
+ public static class Tag {
+ private Integer id;
+ private String name;
+ private String color;
+ private String description;
+ private String createdAt;
+ private int usageCount;
+
+ public Tag() {}
+
+ public Tag(String name, String color, String description) {
+ this.name = name;
+ this.color = color;
+ this.description = description;
+ this.createdAt = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+ this.usageCount = 0;
+ }
+
+ // Getters and Setters
+ public Integer getId() { return id; }
+ public void setId(Integer id) { this.id = id; }
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+
+ public String getColor() { return color; }
+ public void setColor(String color) { this.color = color; }
+
+ public String getDescription() { return description; }
+ public void setDescription(String description) { this.description = description; }
+
+ public String getCreatedAt() { return createdAt; }
+ public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
+
+ public int getUsageCount() { return usageCount; }
+ public void setUsageCount(int usageCount) { this.usageCount = usageCount; }
+
+ @Override
+ public String toString() {
+ return String.format("Tag{id=%d, name='%s', color='%s', usage=%d}", id, name, color, usageCount);
+ }
+ }
+
+ // ========================================
+ // CREATE 작업
+ // ========================================
+
+ /**
+ * 새 태그 생성
+ */
+ public boolean createTag(Tag tag) {
+ if (tag == null || tag.getName() == null || tag.getName().trim().isEmpty()) {
+ logger.error("Invalid tag provided for creation");
+ return false;
+ }
+
+ String insertSQL = """
+ INSERT INTO tags (name, color, description, created_at, usage_count)
+ VALUES (?, ?, ?, ?, ?)
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS)) {
+
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ pstmt.setString(1, tag.getName());
+ pstmt.setString(2, tag.getColor() != null ? tag.getColor() : "#007bff");
+ pstmt.setString(3, tag.getDescription());
+ pstmt.setString(4, now);
+ pstmt.setInt(5, 0);
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
+ if (generatedKeys.next()) {
+ tag.setId(generatedKeys.getInt(1));
+ tag.setCreatedAt(now);
+ logger.info("Tag created successfully with ID: {}", tag.getId());
+ return true;
+ }
+ }
+ }
+
+ } catch (SQLException e) {
+ if (e.getMessage().contains("UNIQUE constraint failed")) {
+ logger.warn("Tag name already exists: {}", tag.getName());
+ } else {
+ logger.error("Failed to create tag", e);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 편의 메서드: 이름으로 태그 생성
+ */
+ public boolean createTag(String name, String color, String description) {
+ return createTag(new Tag(name, color, description));
+ }
+
+ // ========================================
+ // READ 작업
+ // ========================================
+
+ /**
+ * 모든 태그 조회 (사용 횟수 순)
+ */
+ public List getAllTags() {
+ String selectSQL = """
+ SELECT id, name, color, description, created_at, usage_count
+ FROM tags
+ ORDER BY usage_count DESC, name ASC
+ """;
+
+ List tags = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(selectSQL)) {
+
+ while (rs.next()) {
+ tags.add(mapResultSetToTag(rs));
+ }
+
+ logger.info("Retrieved {} tags", tags.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to retrieve tags", e);
+ }
+
+ return tags;
+ }
+
+ /**
+ * ID로 태그 조회
+ */
+ public Optional getTagById(int id) {
+ String selectSQL = """
+ SELECT id, name, color, description, created_at, usage_count
+ FROM tags
+ WHERE id = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setInt(1, id);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ return Optional.of(mapResultSetToTag(rs));
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get tag by ID: {}", id, e);
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * 이름으로 태그 조회
+ */
+ public Optional getTagByName(String name) {
+ String selectSQL = """
+ SELECT id, name, color, description, created_at, usage_count
+ FROM tags
+ WHERE name = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setString(1, name);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ return Optional.of(mapResultSetToTag(rs));
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get tag by name: {}", name, e);
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * 특정 일기의 태그들 조회
+ */
+ public List getTagsByDiaryId(int diaryId) {
+ String selectSQL = """
+ SELECT t.id, t.name, t.color, t.description, t.created_at, t.usage_count
+ FROM tags t
+ INNER JOIN diary_tags dt ON t.id = dt.tag_id
+ WHERE dt.diary_id = ?
+ ORDER BY t.name ASC
+ """;
+
+ List tags = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(selectSQL)) {
+
+ pstmt.setInt(1, diaryId);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ while (rs.next()) {
+ tags.add(mapResultSetToTag(rs));
+ }
+ }
+
+ logger.info("Retrieved {} tags for diary ID: {}", tags.size(), diaryId);
+
+ } catch (SQLException e) {
+ logger.error("Failed to get tags for diary ID: {}", diaryId, e);
+ }
+
+ return tags;
+ }
+
+ // ========================================
+ // UPDATE 작업
+ // ========================================
+
+ /**
+ * 태그 정보 수정
+ */
+ public boolean updateTag(Tag tag) {
+ if (tag == null || tag.getId() == null) {
+ logger.error("Invalid tag provided for update");
+ return false;
+ }
+
+ String updateSQL = """
+ UPDATE tags
+ SET name = ?, color = ?, description = ?
+ WHERE id = ?
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(updateSQL)) {
+
+ pstmt.setString(1, tag.getName());
+ pstmt.setString(2, tag.getColor());
+ pstmt.setString(3, tag.getDescription());
+ pstmt.setInt(4, tag.getId());
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Tag updated successfully: ID={}", tag.getId());
+ return true;
+ } else {
+ logger.warn("No tag found with ID: {}", tag.getId());
+ return false;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to update tag: ID={}", tag.getId(), e);
+ return false;
+ }
+ }
+
+ // ========================================
+ // DELETE 작업
+ // ========================================
+
+ /**
+ * 태그 삭제
+ */
+ public boolean deleteTag(int id) {
+ String deleteSQL = "DELETE FROM tags WHERE id = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+
+ pstmt.setInt(1, id);
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Tag deleted successfully: ID={}", id);
+ return true;
+ } else {
+ logger.warn("No tag found with ID: {}", id);
+ return false;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to delete tag: ID={}", id, e);
+ return false;
+ }
+ }
+
+ // ========================================
+ // 일기-태그 연결 작업
+ // ========================================
+
+ /**
+ * 일기에 태그 추가
+ */
+ public boolean addTagToDiary(int diaryId, int tagId) {
+ String insertSQL = """
+ INSERT OR IGNORE INTO diary_tags (diary_id, tag_id, created_at)
+ VALUES (?, ?, ?)
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
+
+ pstmt.setInt(1, diaryId);
+ pstmt.setInt(2, tagId);
+ pstmt.setString(3, LocalDateTime.now().format(TIMESTAMP_FORMAT));
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Tag {} added to diary {}", tagId, diaryId);
+ return true;
+ } else {
+ logger.info("Tag-diary relationship already exists: diary={}, tag={}", diaryId, tagId);
+ return false;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to add tag {} to diary {}", tagId, diaryId, e);
+ return false;
+ }
+ }
+
+ /**
+ * 일기에서 태그 제거
+ */
+ public boolean removeTagFromDiary(int diaryId, int tagId) {
+ String deleteSQL = "DELETE FROM diary_tags WHERE diary_id = ? AND tag_id = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+
+ pstmt.setInt(1, diaryId);
+ pstmt.setInt(2, tagId);
+
+ int result = pstmt.executeUpdate();
+
+ if (result > 0) {
+ logger.info("Tag {} removed from diary {}", tagId, diaryId);
+ return true;
+ } else {
+ logger.warn("Tag-diary relationship not found: diary={}, tag={}", diaryId, tagId);
+ return false;
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to remove tag {} from diary {}", tagId, diaryId, e);
+ return false;
+ }
+ }
+
+ /**
+ * 일기의 모든 태그 제거
+ */
+ public int removeAllTagsFromDiary(int diaryId) {
+ String deleteSQL = "DELETE FROM diary_tags WHERE diary_id = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) {
+
+ pstmt.setInt(1, diaryId);
+ int result = pstmt.executeUpdate();
+
+ logger.info("Removed {} tags from diary {}", result, diaryId);
+ return result;
+
+ } catch (SQLException e) {
+ logger.error("Failed to remove tags from diary {}", diaryId, e);
+ return -1;
+ }
+ }
+
+ /**
+ * 일기에 여러 태그 추가 (배치 처리)
+ */
+ public int addTagsToDiary(int diaryId, List tagIds) {
+ if (tagIds == null || tagIds.isEmpty()) {
+ return 0;
+ }
+
+ String insertSQL = """
+ INSERT OR IGNORE INTO diary_tags (diary_id, tag_id, created_at)
+ VALUES (?, ?, ?)
+ """;
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
+
+ conn.setAutoCommit(false);
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+ int successCount = 0;
+
+ for (Integer tagId : tagIds) {
+ pstmt.setInt(1, diaryId);
+ pstmt.setInt(2, tagId);
+ pstmt.setString(3, now);
+
+ if (pstmt.executeUpdate() > 0) {
+ successCount++;
+ }
+ }
+
+ conn.commit();
+ logger.info("Added {} tags to diary {}", successCount, diaryId);
+ return successCount;
+
+ } catch (SQLException e) {
+ logger.error("Failed to add tags to diary {}", diaryId, e);
+ return -1;
+ }
+ }
+
+ // ========================================
+ // 통계 작업
+ // ========================================
+
+ /**
+ * 사용되지 않는 태그 조회
+ */
+ public List getUnusedTags() {
+ String selectSQL = """
+ SELECT id, name, color, description, created_at, usage_count
+ FROM tags
+ WHERE usage_count = 0
+ ORDER BY name ASC
+ """;
+
+ List tags = new ArrayList<>();
+
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(selectSQL)) {
+
+ while (rs.next()) {
+ tags.add(mapResultSetToTag(rs));
+ }
+
+ logger.info("Found {} unused tags", tags.size());
+
+ } catch (SQLException e) {
+ logger.error("Failed to get unused tags", e);
+ }
+
+ return tags;
+ }
+
+ /**
+ * 특정 태그가 사용된 일기 개수 조회
+ */
+ public int getDiaryCountByTag(int tagId) {
+ String countSQL = "SELECT COUNT(*) FROM diary_tags WHERE tag_id = ?";
+
+ try (Connection conn = dbUtil.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(countSQL)) {
+
+ pstmt.setInt(1, tagId);
+
+ try (ResultSet rs = pstmt.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt(1);
+ }
+ }
+
+ } catch (SQLException e) {
+ logger.error("Failed to get diary count for tag {}", tagId, e);
+ }
+
+ return 0;
+ }
+
+ // ========================================
+ // 유틸리티 메서드
+ // ========================================
+
+ /**
+ * ResultSet을 Tag 객체로 매핑
+ */
+ private Tag mapResultSetToTag(ResultSet rs) throws SQLException {
+ Tag tag = new Tag();
+ tag.setId(rs.getInt("id"));
+ tag.setName(rs.getString("name"));
+ tag.setColor(rs.getString("color"));
+ tag.setDescription(rs.getString("description"));
+ tag.setCreatedAt(rs.getString("created_at"));
+ tag.setUsageCount(rs.getInt("usage_count"));
+ return tag;
+ }
+}
diff --git a/src/main/java/mindiary.db b/src/main/java/mindiary.db
new file mode 100644
index 0000000..38db2f8
Binary files /dev/null and b/src/main/java/mindiary.db differ
diff --git a/src/main/java/model/Diary.class b/src/main/java/model/Diary.class
new file mode 100644
index 0000000..79452ed
Binary files /dev/null and b/src/main/java/model/Diary.class differ
diff --git a/src/main/java/model/Diary.java b/src/main/java/model/Diary.java
index 8030309..b7106bd 100644
--- a/src/main/java/model/Diary.java
+++ b/src/main/java/model/Diary.java
@@ -1,25 +1,132 @@
package model;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 일기 데이터를 나타내는 모델 클래스
+ */
public class Diary {
+ private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private Integer id;
private String content;
private String emotionSummary;
private String createdAt;
-
+ private String updatedAt;
+
+ // 기본 생성자
+ public Diary() {}
+
+ // 기존 생성자 (하위 호환성 유지)
public Diary(String content, String emotionSummary, String createdAt) {
this.content = content;
this.emotionSummary = emotionSummary;
this.createdAt = createdAt;
+ this.updatedAt = createdAt;
}
-
+
+ // 완전한 생성자
+ public Diary(Integer id, String content, String emotionSummary, String createdAt, String updatedAt) {
+ this.id = id;
+ this.content = content;
+ this.emotionSummary = emotionSummary;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ }
+
+ // 새 일기 생성용 생성자 (ID는 자동 생성)
+ public Diary(String content, String emotionSummary) {
+ this.content = content;
+ this.emotionSummary = emotionSummary;
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+ this.createdAt = now;
+ this.updatedAt = now;
+ }
+
+ // Getter 메서드들
+ public Integer getId() {
+ return id;
+ }
+
public String getContent() {
return content;
}
-
+
public String getEmotionSummary() {
return emotionSummary;
}
-
+
public String getCreatedAt() {
return createdAt;
}
-}
\ No newline at end of file
+
+ public String getUpdatedAt() {
+ return updatedAt;
+ }
+
+ // Setter 메서드들
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ updateTimestamp();
+ }
+
+ public void setEmotionSummary(String emotionSummary) {
+ this.emotionSummary = emotionSummary;
+ updateTimestamp();
+ }
+
+ public void setCreatedAt(String createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public void setUpdatedAt(String updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ // 수정 시간 자동 업데이트
+ private void updateTimestamp() {
+ this.updatedAt = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+ }
+
+ // 유틸리티 메서드들
+ public boolean isValid() {
+ return content != null && !content.trim().isEmpty();
+ }
+
+ public int getContentLength() {
+ return content != null ? content.length() : 0;
+ }
+
+ public boolean hasEmotion() {
+ return emotionSummary != null && !emotionSummary.trim().isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Diary{id=%d, content='%s...', emotion='%s', createdAt='%s', updatedAt='%s'}",
+ id,
+ content != null ? content.substring(0, Math.min(50, content.length())) : "null",
+ emotionSummary,
+ createdAt,
+ updatedAt);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+
+ Diary diary = (Diary) obj;
+ return id != null ? id.equals(diary.id) : diary.id == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return id != null ? id.hashCode() : 0;
+ }
+}
diff --git a/src/main/java/sql/initial_data.sql b/src/main/java/sql/initial_data.sql
new file mode 100644
index 0000000..825a4a7
--- /dev/null
+++ b/src/main/java/sql/initial_data.sql
@@ -0,0 +1,169 @@
+-- ========================================
+-- mindiary 기본 데이터 삽입 스크립트
+-- ========================================
+
+-- ========================================
+-- 1. 기본 설정값 삽입
+-- ========================================
+
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description) VALUES
+-- 애플리케이션 기본 설정
+('app_version', '1.0.0', 'string', '애플리케이션 버전'),
+('app_name', 'mindiary', 'string', '애플리케이션 이름'),
+('app_initialized_at', datetime('now', 'localtime'), 'string', '애플리케이션 초기화 시간'),
+
+-- 일기 관련 설정
+('max_diary_length', '10000', 'number', '일기 최대 글자 수'),
+('min_diary_length', '10', 'number', '일기 최소 글자 수'),
+('auto_save_enabled', 'true', 'boolean', '자동 저장 기능 활성화'),
+('auto_save_interval', '30', 'number', '자동 저장 간격 (초)'),
+
+-- 감정 분석 설정
+('emotion_analysis_enabled', 'true', 'boolean', '감정 분석 기능 활성화'),
+('emotion_analysis_method', 'rule_based', 'string', '감정 분석 방법'),
+('emotion_confidence_threshold', '0.6', 'number', '감정 분석 신뢰도 임계값'),
+('supported_emotions', '["positive", "negative", "neutral", "mixed"]', 'json', '지원하는 감정 유형'),
+
+-- 백업 관련 설정
+('backup_enabled', 'true', 'boolean', '백업 기능 활성화'),
+('backup_interval_days', '7', 'number', '백업 주기 (일)'),
+('backup_retention_days', '30', 'number', '백업 파일 보관 기간 (일)'),
+('auto_backup_enabled', 'false', 'boolean', '자동 백업 활성화'),
+
+-- UI/UX 설정
+('theme', 'light', 'string', '테마 설정 (light/dark)'),
+('language', 'ko', 'string', '언어 설정'),
+('timezone', 'Asia/Seoul', 'string', '시간대 설정'),
+('date_format', 'yyyy-MM-dd HH:mm:ss', 'string', '날짜 형식'),
+
+-- 보안 설정
+('password_protection', 'false', 'boolean', '비밀번호 보호 활성화'),
+('session_timeout', '1800', 'number', '세션 타임아웃 (초)'),
+('max_login_attempts', '5', 'number', '최대 로그인 시도 횟수'),
+
+-- 성능 설정
+('cache_enabled', 'true', 'boolean', '캐시 기능 활성화'),
+('cache_size', '100', 'number', '캐시 크기 (항목 수)'),
+('lazy_loading', 'true', 'boolean', '지연 로딩 활성화'),
+('pagination_size', '20', 'number', '페이지네이션 크기'),
+
+-- 알림 설정
+('notifications_enabled', 'true', 'boolean', '알림 기능 활성화'),
+('reminder_enabled', 'false', 'boolean', '일기 작성 리마인더 활성화'),
+('reminder_time', '21:00', 'string', '리마인더 시간'),
+
+-- 통계 설정
+('stats_enabled', 'true', 'boolean', '통계 기능 활성화'),
+('stats_retention_days', '365', 'number', '통계 데이터 보관 기간 (일)'),
+('daily_stats_enabled', 'true', 'boolean', '일간 통계 수집 활성화'),
+
+-- 개발/디버그 설정
+('debug_mode', 'false', 'boolean', '디버그 모드 활성화'),
+('log_level', 'INFO', 'string', '로그 레벨'),
+('performance_monitoring', 'false', 'boolean', '성능 모니터링 활성화');
+
+-- ========================================
+-- 2. 기본 태그 삽입
+-- ========================================
+
+INSERT OR REPLACE INTO tags (name, color, description) VALUES
+('일상', '#007bff', '일상적인 생활에 대한 기록'),
+('감정', '#dc3545', '감정적인 경험이나 느낌'),
+('성찰', '#6f42c1', '자기 반성이나 깊은 생각'),
+('목표', '#28a745', '목표 설정이나 계획에 관한 내용'),
+('관계', '#fd7e14', '인간관계나 소통에 관한 이야기'),
+('성장', '#20c997', '개인적 성장이나 발전'),
+('여행', '#6610f2', '여행이나 새로운 경험'),
+('학습', '#e83e8c', '공부나 새로운 지식 습득'),
+('건강', '#17a2b8', '건강이나 운동에 관한 내용'),
+('취미', '#ffc107', '취미 활동이나 여가 시간'),
+('업무', '#6c757d', '직장이나 업무 관련 내용'),
+('가족', '#fd7e14', '가족과의 시간이나 추억'),
+('친구', '#20c997', '친구들과의 만남이나 우정'),
+('도전', '#dc3545', '새로운 도전이나 모험'),
+('감사', '#28a745', '감사한 일들에 대한 기록');
+
+-- ========================================
+-- 3. 샘플 일기 데이터 삽입 (개발/테스트용)
+-- ========================================
+
+-- 최근 며칠간의 샘플 일기들
+INSERT OR REPLACE INTO diary (content, emotion_summary, created_at, updated_at) VALUES
+(
+ '오늘은 새로운 프로젝트를 시작했다. mindiary 애플리케이션을 개발하는 것인데, 일기를 디지털로 관리할 수 있는 시스템을 만드는 것이다. 처음에는 복잡해 보였지만, 하나씩 단계별로 접근하니 생각보다 재미있다. 데이터베이스 설계부터 시작해서 UI까지 모든 것을 고려해야 하지만, 그만큼 배우는 것도 많을 것 같다.',
+ 'positive',
+ datetime('now', '-2 days', 'localtime'),
+ datetime('now', '-2 days', 'localtime')
+),
+(
+ '코딩을 하면서 여러 번 막혔지만, 하나씩 해결해나가는 과정이 즐겁다. 특히 SQLite 데이터베이스를 설계할 때 정규화와 성능을 동시에 고려해야 하는 부분이 흥미로웠다. 인덱스를 적절히 설정하고, 트리거를 활용해서 자동화할 수 있는 부분들을 찾아내는 것도 재미있는 작업이었다.',
+ 'positive',
+ datetime('now', '-1 days', 'localtime'),
+ datetime('now', '-1 days', 'localtime')
+),
+(
+ '오늘은 좀 피곤했다. 밤늦게까지 코딩을 하다 보니 수면 부족이 심하다. 그래도 프로젝트가 조금씩 형태를 갖춰가는 것을 보니 뿌듯하다. 내일은 좀 더 체계적으로 시간을 관리해서 건강도 챙기면서 개발을 진행해야겠다.',
+ 'neutral',
+ datetime('now', 'localtime'),
+ datetime('now', 'localtime')
+);
+
+-- ========================================
+-- 4. 샘플 감정 분석 데이터 삽입
+-- ========================================
+
+INSERT OR REPLACE INTO emotion_analysis (diary_id, emotion_type, confidence_score, keywords, analysis_method) VALUES
+(1, 'positive', 0.85, '["새로운", "프로젝트", "재미있다", "배우는"]', 'rule_based'),
+(2, 'positive', 0.90, '["즐겁다", "흥미로웠다", "재미있는"]', 'rule_based'),
+(3, 'neutral', 0.75, '["피곤했다", "뿌듯하다", "체계적으로"]', 'rule_based');
+
+-- ========================================
+-- 5. 샘플 일기-태그 연결
+-- ========================================
+
+INSERT OR REPLACE INTO diary_tags (diary_id, tag_id) VALUES
+(1, 1), -- 일상
+(1, 4), -- 목표
+(1, 8), -- 학습
+(2, 8), -- 학습
+(2, 6), -- 성장
+(2, 14), -- 도전
+(3, 1), -- 일상
+(3, 3), -- 성찰
+(3, 9); -- 건강
+
+-- ========================================
+-- 6. 초기 통계 데이터 생성
+-- ========================================
+
+INSERT OR REPLACE INTO usage_stats (stat_date, diaries_created, total_characters, emotions_analyzed, tags_used)
+SELECT
+ date(created_at) as stat_date,
+ COUNT(*) as diaries_created,
+ SUM(length(content)) as total_characters,
+ COUNT(CASE WHEN emotion_summary IS NOT NULL THEN 1 END) as emotions_analyzed,
+ (SELECT COUNT(*) FROM diary_tags WHERE diary_id IN (SELECT id FROM diary WHERE date(created_at) = date(d.created_at))) as tags_used
+FROM diary d
+GROUP BY date(created_at);
+
+-- ========================================
+-- 7. 백업 로그 초기화
+-- ========================================
+
+INSERT INTO backup_log (backup_path, backup_type, status, created_at) VALUES
+('mindiary_initial_backup.db', 'manual', 'SUCCESS', datetime('now', 'localtime'));
+
+-- ========================================
+-- 8. 데이터 초기화 완료 표시
+-- ========================================
+
+UPDATE user_settings
+SET setting_value = datetime('now', 'localtime'), updated_at = datetime('now', 'localtime')
+WHERE setting_key = 'data_initialized_at'
+ OR setting_key = 'sample_data_loaded';
+
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description) VALUES
+('data_initialized_at', datetime('now', 'localtime'), 'string', '데이터 초기화 완료 시간'),
+('sample_data_loaded', 'true', 'boolean', '샘플 데이터 로드 여부'),
+('total_diaries', (SELECT COUNT(*) FROM diary), 'number', '전체 일기 개수'),
+('total_tags', (SELECT COUNT(*) FROM tags), 'number', '전체 태그 개수');
diff --git a/src/main/java/sql/schema.sql b/src/main/java/sql/schema.sql
new file mode 100644
index 0000000..77d3b02
--- /dev/null
+++ b/src/main/java/sql/schema.sql
@@ -0,0 +1,245 @@
+-- ========================================
+-- mindiary 데이터베이스 스키마 초기화 스크립트
+-- ========================================
+
+-- SQLite 설정
+PRAGMA foreign_keys = ON;
+PRAGMA journal_mode = WAL;
+PRAGMA synchronous = NORMAL;
+PRAGMA cache_size = 1000;
+PRAGMA temp_store = MEMORY;
+
+-- ========================================
+-- 1. 메인 테이블들
+-- ========================================
+
+-- 일기 테이블 (핵심 테이블)
+CREATE TABLE IF NOT EXISTS diary (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ content TEXT NOT NULL CHECK(length(content) > 0),
+ emotion_summary TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ -- 검증 제약조건
+ CONSTRAINT content_length_check CHECK(length(content) <= 10000),
+ CONSTRAINT emotion_format_check CHECK(emotion_summary IS NULL OR length(emotion_summary) <= 100),
+ CONSTRAINT date_format_check CHECK(
+ created_at GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]'
+ AND updated_at GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]'
+ )
+);
+
+-- 사용자 설정 테이블
+CREATE TABLE IF NOT EXISTS user_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ setting_key TEXT UNIQUE NOT NULL CHECK(length(setting_key) > 0),
+ setting_value TEXT,
+ setting_type TEXT DEFAULT 'string' CHECK(setting_type IN ('string', 'number', 'boolean', 'json')),
+ description TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ -- 제약조건
+ CONSTRAINT key_format_check CHECK(setting_key NOT GLOB '* *' AND setting_key GLOB '[a-z_]*')
+);
+
+-- 백업 로그 테이블
+CREATE TABLE IF NOT EXISTS backup_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ backup_path TEXT NOT NULL,
+ backup_size INTEGER DEFAULT 0 CHECK(backup_size >= 0),
+ backup_type TEXT DEFAULT 'manual' CHECK(backup_type IN ('manual', 'auto', 'scheduled')),
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ status TEXT DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'SUCCESS', 'FAILED', 'PARTIAL')),
+ error_message TEXT,
+
+ -- 제약조건
+ CONSTRAINT backup_path_check CHECK(length(backup_path) > 0)
+);
+
+-- ========================================
+-- 2. 확장 테이블들 (향후 기능용)
+-- ========================================
+
+-- 감정 분석 상세 정보 테이블
+CREATE TABLE IF NOT EXISTS emotion_analysis (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ diary_id INTEGER NOT NULL,
+ emotion_type TEXT NOT NULL CHECK(emotion_type IN ('positive', 'negative', 'neutral', 'mixed')),
+ confidence_score REAL CHECK(confidence_score >= 0.0 AND confidence_score <= 1.0),
+ keywords TEXT, -- JSON 형태로 저장
+ analysis_method TEXT DEFAULT 'rule_based',
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ -- 외래키
+ FOREIGN KEY (diary_id) REFERENCES diary(id) ON DELETE CASCADE
+);
+
+-- 태그 테이블 (일기 분류용)
+CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL CHECK(length(name) > 0 AND length(name) <= 50),
+ color TEXT DEFAULT '#007bff' CHECK(color GLOB '#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]'),
+ description TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ usage_count INTEGER DEFAULT 0 CHECK(usage_count >= 0)
+);
+
+-- 일기-태그 연결 테이블 (다대다 관계)
+CREATE TABLE IF NOT EXISTS diary_tags (
+ diary_id INTEGER NOT NULL,
+ tag_id INTEGER NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ PRIMARY KEY (diary_id, tag_id),
+ FOREIGN KEY (diary_id) REFERENCES diary(id) ON DELETE CASCADE,
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+);
+
+-- 사용 통계 테이블
+CREATE TABLE IF NOT EXISTS usage_stats (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ stat_date TEXT NOT NULL, -- YYYY-MM-DD 형식
+ diaries_created INTEGER DEFAULT 0 CHECK(diaries_created >= 0),
+ total_characters INTEGER DEFAULT 0 CHECK(total_characters >= 0),
+ emotions_analyzed INTEGER DEFAULT 0 CHECK(emotions_analyzed >= 0),
+ tags_used INTEGER DEFAULT 0 CHECK(tags_used >= 0),
+
+ UNIQUE(stat_date),
+ CONSTRAINT date_format_check CHECK(stat_date GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]')
+);
+
+-- ========================================
+-- 3. 인덱스 생성 (성능 최적화)
+-- ========================================
+
+-- diary 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_diary_created_at ON diary(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_diary_emotion ON diary(emotion_summary) WHERE emotion_summary IS NOT NULL;
+CREATE INDEX IF NOT EXISTS idx_diary_content_fts ON diary(content); -- Full Text Search 준비
+CREATE INDEX IF NOT EXISTS idx_diary_date_only ON diary(date(created_at));
+
+-- user_settings 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_settings_key ON user_settings(setting_key);
+CREATE INDEX IF NOT EXISTS idx_settings_type ON user_settings(setting_type);
+
+-- backup_log 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_backup_created_at ON backup_log(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_backup_status ON backup_log(status);
+
+-- emotion_analysis 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_emotion_diary_id ON emotion_analysis(diary_id);
+CREATE INDEX IF NOT EXISTS idx_emotion_type ON emotion_analysis(emotion_type);
+CREATE INDEX IF NOT EXISTS idx_emotion_confidence ON emotion_analysis(confidence_score DESC);
+
+-- tags 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
+CREATE INDEX IF NOT EXISTS idx_tags_usage ON tags(usage_count DESC);
+
+-- diary_tags 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_diary_tags_tag ON diary_tags(tag_id);
+CREATE INDEX IF NOT EXISTS idx_diary_tags_diary ON diary_tags(diary_id);
+
+-- usage_stats 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_stats_date ON usage_stats(stat_date DESC);
+
+-- ========================================
+-- 4. 트리거 생성 (자동화)
+-- ========================================
+
+-- diary 테이블 updated_at 자동 업데이트
+CREATE TRIGGER IF NOT EXISTS update_diary_timestamp
+ AFTER UPDATE ON diary
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+BEGIN
+ UPDATE diary SET updated_at = datetime('now', 'localtime') WHERE id = NEW.id;
+END;
+
+-- user_settings 테이블 updated_at 자동 업데이트
+CREATE TRIGGER IF NOT EXISTS update_settings_timestamp
+ AFTER UPDATE ON user_settings
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+BEGIN
+ UPDATE user_settings SET updated_at = datetime('now', 'localtime') WHERE id = NEW.id;
+END;
+
+-- tags 사용 횟수 자동 증가
+CREATE TRIGGER IF NOT EXISTS increment_tag_usage
+ AFTER INSERT ON diary_tags
+ FOR EACH ROW
+BEGIN
+ UPDATE tags SET usage_count = usage_count + 1 WHERE id = NEW.tag_id;
+END;
+
+-- tags 사용 횟수 자동 감소
+CREATE TRIGGER IF NOT EXISTS decrement_tag_usage
+ AFTER DELETE ON diary_tags
+ FOR EACH ROW
+BEGIN
+ UPDATE tags SET usage_count = usage_count - 1 WHERE id = OLD.tag_id;
+END;
+
+-- 일기 삭제 시 감정 분석 데이터도 함께 삭제 (CASCADE 보완)
+CREATE TRIGGER IF NOT EXISTS cleanup_emotion_analysis
+ AFTER DELETE ON diary
+ FOR EACH ROW
+BEGIN
+ DELETE FROM emotion_analysis WHERE diary_id = OLD.id;
+ DELETE FROM diary_tags WHERE diary_id = OLD.id;
+END;
+
+-- ========================================
+-- 5. 뷰 생성 (편의성)
+-- ========================================
+
+-- 최근 일기 뷰 (30일)
+CREATE VIEW IF NOT EXISTS recent_diaries AS
+SELECT
+ id,
+ content,
+ emotion_summary,
+ created_at,
+ updated_at,
+ date(created_at) as diary_date,
+ length(content) as content_length
+FROM diary
+WHERE date(created_at) >= date('now', '-30 days')
+ORDER BY created_at DESC;
+
+-- 감정별 통계 뷰
+CREATE VIEW IF NOT EXISTS emotion_stats AS
+SELECT
+ emotion_summary,
+ COUNT(*) as count,
+ ROUND(AVG(length(content)), 2) as avg_content_length,
+ MIN(created_at) as first_entry,
+ MAX(created_at) as last_entry
+FROM diary
+WHERE emotion_summary IS NOT NULL
+GROUP BY emotion_summary
+ORDER BY count DESC;
+
+-- 월별 통계 뷰
+CREATE VIEW IF NOT EXISTS monthly_stats AS
+SELECT
+ strftime('%Y-%m', created_at) as month,
+ COUNT(*) as diary_count,
+ SUM(length(content)) as total_characters,
+ ROUND(AVG(length(content)), 2) as avg_content_length,
+ COUNT(DISTINCT emotion_summary) as unique_emotions
+FROM diary
+GROUP BY strftime('%Y-%m', created_at)
+ORDER BY month DESC;
+
+-- ========================================
+-- 스키마 버전 정보
+-- ========================================
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description, created_at, updated_at)
+VALUES ('schema_version', '1.0.0', 'string', '데이터베이스 스키마 버전', datetime('now', 'localtime'), datetime('now', 'localtime'));
+
+-- 스키마 초기화 완료 로그
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description, created_at, updated_at)
+VALUES ('schema_initialized_at', datetime('now', 'localtime'), 'string', '스키마 초기화 완료 시간', datetime('now', 'localtime'), datetime('now', 'localtime'));
diff --git a/src/main/java/util/DatabaseConnectionTest.java b/src/main/java/util/DatabaseConnectionTest.java
new file mode 100644
index 0000000..55645c0
--- /dev/null
+++ b/src/main/java/util/DatabaseConnectionTest.java
@@ -0,0 +1,99 @@
+package util;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+/**
+ * DatabaseUtil 연결 테스트를 위한 간단한 테스트 클래스
+ */
+public class DatabaseConnectionTest {
+
+ public static void main(String[] args) {
+ System.out.println("=== DatabaseUtil 연결 테스트 시작 ===");
+
+ try {
+ // DatabaseUtil 인스턴스 생성
+ DatabaseUtil dbUtil = DatabaseUtil.getInstance();
+ System.out.println("✅ DatabaseUtil 인스턴스 생성 성공");
+
+ // 연결 테스트
+ boolean connectionTest = dbUtil.testConnection();
+ System.out.println("✅ 연결 테스트 결과: " + (connectionTest ? "성공" : "실패"));
+
+ // 실제 연결 및 테이블 확인
+ try (Connection conn = dbUtil.getConnection()) {
+ System.out.println("✅ 데이터베이스 연결 성공");
+
+ // 테이블 목록 조회
+ try (Statement stmt = conn.createStatement()) {
+ System.out.println("\n=== 테이블 목록 확인 ===");
+ ResultSet tables = stmt.executeQuery("SELECT name FROM sqlite_master WHERE type='table'");
+ while (tables.next()) {
+ String tableName = tables.getString("name");
+ System.out.println("📄 테이블: " + tableName);
+
+ // 각 테이블의 행 수 확인
+ try (ResultSet count = stmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) {
+ if (count.next()) {
+ System.out.println(" ↳ 행 수: " + count.getInt(1));
+ }
+ }
+ }
+ }
+
+ // 기본 설정값 확인
+ System.out.println("\n=== 기본 설정값 확인 ===");
+ String[] settingKeys = {"app_version", "emotion_analysis_enabled", "backup_enabled", "max_diary_length"};
+ for (String key : settingKeys) {
+ String value = dbUtil.getSetting(key, "NOT_FOUND");
+ System.out.println("⚙️ " + key + " = " + value);
+ }
+
+ // 데이터베이스 통계
+ System.out.println("\n=== 데이터베이스 통계 ===");
+ DatabaseUtil.DatabaseStats stats = dbUtil.getDatabaseStats();
+ System.out.println("📊 " + stats.toString());
+
+ // 샘플 일기 데이터 삽입 테스트
+ System.out.println("\n=== 샘플 데이터 삽입 테스트 ===");
+ try (Statement stmt = conn.createStatement()) {
+ String testContent = "데이터베이스 연결 테스트를 위한 샘플 일기입니다. (테스트 시간: " +
+ java.time.LocalDateTime.now() + ")";
+
+ String insertSQL = String.format("""
+ INSERT INTO diary (content, emotion_summary, created_at, updated_at)
+ VALUES ('%s', 'neutral', '%s', '%s')
+ """, testContent,
+ java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
+ java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+
+ int result = stmt.executeUpdate(insertSQL);
+ System.out.println("✅ 샘플 일기 삽입 성공: " + result + "개 행 영향");
+
+ // 방금 삽입한 데이터 조회
+ try (ResultSet rs = stmt.executeQuery("SELECT * FROM diary ORDER BY id DESC LIMIT 1")) {
+ if (rs.next()) {
+ System.out.println("🔍 최근 일기 확인:");
+ System.out.println(" ID: " + rs.getInt("id"));
+ System.out.println(" 내용: " + rs.getString("content").substring(0, Math.min(50, rs.getString("content").length())) + "...");
+ System.out.println(" 감정: " + rs.getString("emotion_summary"));
+ System.out.println(" 생성일: " + rs.getString("created_at"));
+ }
+ }
+ }
+
+ } catch (Exception e) {
+ System.err.println("❌ 데이터베이스 작업 중 오류 발생: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ System.out.println("\n=== 테스트 완료 ===");
+ System.out.println("✅ 모든 데이터베이스 연결 테스트가 성공적으로 완료되었습니다!");
+
+ } catch (Exception e) {
+ System.err.println("❌ DatabaseUtil 테스트 중 오류 발생: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/util/DatabaseInitializer.class b/src/main/java/util/DatabaseInitializer.class
new file mode 100644
index 0000000..2cab73d
Binary files /dev/null and b/src/main/java/util/DatabaseInitializer.class differ
diff --git a/src/main/java/util/DatabaseInitializer.java b/src/main/java/util/DatabaseInitializer.java
new file mode 100644
index 0000000..7e3e584
--- /dev/null
+++ b/src/main/java/util/DatabaseInitializer.java
@@ -0,0 +1,239 @@
+package util;
+
+/**
+ * 데이터베이스 초기화 및 검증 전용 클래스
+ */
+public class DatabaseInitializer {
+
+ public static void main(String[] args) {
+ System.out.println("=== mindiary 데이터베이스 초기화 및 검증 시작 ===");
+
+ try {
+ // 1. 데이터베이스 초기화
+ System.out.println("\n1. 데이터베이스 초기화 중...");
+ DatabaseUtil dbUtil = DatabaseUtil.getInstance();
+
+ if (dbUtil.testConnection()) {
+ System.out.println("✅ 데이터베이스 연결 성공");
+ System.out.println(" 데이터베이스 파일: mindiary.db");
+ } else {
+ System.out.println("❌ 데이터베이스 연결 실패");
+ return;
+ }
+
+ // 2. 스키마 검증
+ System.out.println("\n2. 스키마 검증 중...");
+ SchemaValidator validator = new SchemaValidator();
+ SchemaValidator.SchemaValidationResult result = validator.validateSchema();
+
+ System.out.println(" 스키마 유효성: " + (result.valid ? "✅ 통과" : "❌ 실패"));
+ System.out.println(" - 테이블: " + (result.tablesValid ? "✅" : "❌"));
+ System.out.println(" - 인덱스: " + (result.indexesValid ? "✅" : "❌"));
+ System.out.println(" - 트리거: " + (result.triggersValid ? "✅" : "❌"));
+ System.out.println(" - 뷰: " + (result.viewsValid ? "✅" : "❌"));
+ System.out.println(" - 제약조건: " + (result.constraintsValid ? "✅" : "❌"));
+
+ if (!result.errors.isEmpty()) {
+ System.out.println(" ⚠️ 검증 오류:");
+ result.errors.forEach(error -> System.out.println(" - " + error));
+ }
+
+ // 3. 기본 데이터 확인
+ System.out.println("\n3. 기본 데이터 확인 중...");
+ DatabaseUtil.DatabaseStats stats = dbUtil.getDatabaseStats();
+ System.out.println(" 📊 데이터베이스 통계:");
+ System.out.println(" - 일기 수: " + stats.diaryCount);
+ System.out.println(" - 설정 수: " + stats.settingsCount);
+ System.out.println(" - 백업 로그 수: " + stats.backupLogCount);
+ System.out.println(" - 크기: " + stats.databaseSize + "KB");
+
+ // 4. 샘플 데이터 검증
+ System.out.println("\n4. 샘플 데이터 검증 중...");
+ validateSampleData(dbUtil);
+
+ // 5. 뷰 테스트
+ System.out.println("\n5. 뷰 테스트 중...");
+ testViews(dbUtil);
+
+ // 6. 트리거 테스트
+ System.out.println("\n6. 트리거 테스트 중...");
+ testTriggers(dbUtil);
+
+ // 7. 성능 테스트
+ System.out.println("\n7. 성능 테스트 중...");
+ performanceTest(dbUtil);
+
+ System.out.println("\n=== 데이터베이스 초기화 및 검증 완료 ===");
+ System.out.println("✅ 모든 검증이 성공적으로 완료되었습니다!");
+
+ } catch (Exception e) {
+ System.err.println("❌ 데이터베이스 초기화 중 오류 발생: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 샘플 데이터 검증
+ */
+ private static void validateSampleData(DatabaseUtil dbUtil) {
+ try {
+ java.sql.Connection conn = dbUtil.getConnection();
+ java.sql.Statement stmt = conn.createStatement();
+
+ // 기본 설정값 확인
+ java.sql.ResultSet rs1 = stmt.executeQuery("SELECT COUNT(*) FROM user_settings");
+ if (rs1.next()) {
+ int settingsCount = rs1.getInt(1);
+ System.out.println(" ⚙️ 기본 설정값: " + settingsCount + "개 " + (settingsCount >= 20 ? "✅" : "❌"));
+ }
+
+ // 기본 태그 확인
+ java.sql.ResultSet rs2 = stmt.executeQuery("SELECT COUNT(*) FROM tags");
+ if (rs2.next()) {
+ int tagsCount = rs2.getInt(1);
+ System.out.println(" 🏷️ 기본 태그: " + tagsCount + "개 " + (tagsCount >= 10 ? "✅" : "❌"));
+ }
+
+ // 샘플 일기 확인
+ java.sql.ResultSet rs3 = stmt.executeQuery("SELECT COUNT(*) FROM diary");
+ if (rs3.next()) {
+ int diaryCount = rs3.getInt(1);
+ System.out.println(" 📝 샘플 일기: " + diaryCount + "개 " + (diaryCount >= 1 ? "✅" : "❌"));
+ }
+
+ // 감정 분석 데이터 확인
+ java.sql.ResultSet rs4 = stmt.executeQuery("SELECT COUNT(*) FROM emotion_analysis");
+ if (rs4.next()) {
+ int emotionCount = rs4.getInt(1);
+ System.out.println(" 😊 감정 분석: " + emotionCount + "개 " + (emotionCount >= 1 ? "✅" : "❌"));
+ }
+
+ conn.close();
+
+ } catch (Exception e) {
+ System.err.println(" ❌ 샘플 데이터 검증 실패: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 뷰 테스트
+ */
+ private static void testViews(DatabaseUtil dbUtil) {
+ try {
+ java.sql.Connection conn = dbUtil.getConnection();
+ java.sql.Statement stmt = conn.createStatement();
+
+ // recent_diaries 뷰 테스트
+ java.sql.ResultSet rs1 = stmt.executeQuery("SELECT COUNT(*) FROM recent_diaries");
+ if (rs1.next()) {
+ System.out.println(" 📄 recent_diaries 뷰: " + rs1.getInt(1) + "개 " + "✅");
+ }
+
+ // emotion_stats 뷰 테스트
+ java.sql.ResultSet rs2 = stmt.executeQuery("SELECT COUNT(*) FROM emotion_stats");
+ if (rs2.next()) {
+ System.out.println(" 😊 emotion_stats 뷰: " + rs2.getInt(1) + "개 " + "✅");
+ }
+
+ // monthly_stats 뷰 테스트
+ java.sql.ResultSet rs3 = stmt.executeQuery("SELECT COUNT(*) FROM monthly_stats");
+ if (rs3.next()) {
+ System.out.println(" 📅 monthly_stats 뷰: " + rs3.getInt(1) + "개 " + "✅");
+ }
+
+ conn.close();
+
+ } catch (Exception e) {
+ System.err.println(" ❌ 뷰 테스트 실패: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 트리거 테스트
+ */
+ private static void testTriggers(DatabaseUtil dbUtil) {
+ try {
+ java.sql.Connection conn = dbUtil.getConnection();
+ java.sql.Statement stmt = conn.createStatement();
+
+ // updated_at 자동 업데이트 트리거 테스트
+ java.sql.ResultSet rs1 = stmt.executeQuery("SELECT updated_at FROM diary ORDER BY id DESC LIMIT 1");
+ if (rs1.next()) {
+ String originalTime = rs1.getString(1);
+
+ // 1초 대기 후 업데이트
+ Thread.sleep(1000);
+ stmt.executeUpdate("UPDATE diary SET content = content || ' [트리거 테스트]' WHERE id = (SELECT id FROM diary ORDER BY id DESC LIMIT 1)");
+
+ java.sql.ResultSet rs2 = stmt.executeQuery("SELECT updated_at FROM diary ORDER BY id DESC LIMIT 1");
+ if (rs2.next()) {
+ String newTime = rs2.getString(1);
+ boolean triggerWorked = !originalTime.equals(newTime);
+ System.out.println(" 🔄 updated_at 트리거: " + (triggerWorked ? "✅" : "❌"));
+
+ // 변경사항 되돌리기
+ stmt.executeUpdate("UPDATE diary SET content = REPLACE(content, ' [트리거 테스트]', '') WHERE id = (SELECT id FROM diary ORDER BY id DESC LIMIT 1)");
+ }
+ }
+
+ conn.close();
+
+ } catch (Exception e) {
+ System.err.println(" ❌ 트리거 테스트 실패: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 성능 테스트
+ */
+ private static void performanceTest(DatabaseUtil dbUtil) {
+ try {
+ java.sql.Connection conn = dbUtil.getConnection();
+
+ // 1. 연결 성능 테스트
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < 10; i++) {
+ java.sql.Connection testConn = dbUtil.getConnection();
+ testConn.close();
+ }
+ long connectionTime = System.currentTimeMillis() - startTime;
+ System.out.println(" 🔗 연결 성능 (10회): " + connectionTime + "ms " + (connectionTime < 1000 ? "✅" : "⚠️"));
+
+ // 2. 쿼리 성능 테스트
+ startTime = System.currentTimeMillis();
+ java.sql.Statement stmt = conn.createStatement();
+ for (int i = 0; i < 100; i++) {
+ java.sql.ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM diary");
+ rs.close();
+ }
+ long queryTime = System.currentTimeMillis() - startTime;
+ System.out.println(" 🔍 쿼리 성능 (100회): " + queryTime + "ms " + (queryTime < 1000 ? "✅" : "⚠️"));
+
+ // 3. 삽입 성능 테스트
+ startTime = System.currentTimeMillis();
+ conn.setAutoCommit(false);
+ java.sql.PreparedStatement pstmt = conn.prepareStatement(
+ "INSERT INTO diary (content, emotion_summary, created_at, updated_at) VALUES (?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))"
+ );
+
+ for (int i = 0; i < 10; i++) {
+ pstmt.setString(1, "성능 테스트용 일기 " + i);
+ pstmt.setString(2, "neutral");
+ pstmt.executeUpdate();
+ }
+ conn.commit();
+ long insertTime = System.currentTimeMillis() - startTime;
+ System.out.println(" 📝 삽입 성능 (10개): " + insertTime + "ms " + (insertTime < 1000 ? "✅" : "⚠️"));
+
+ // 테스트 데이터 정리
+ stmt.executeUpdate("DELETE FROM diary WHERE content LIKE '성능 테스트용 일기%'");
+ conn.commit();
+ conn.setAutoCommit(true);
+
+ conn.close();
+
+ } catch (Exception e) {
+ System.err.println(" ❌ 성능 테스트 실패: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/util/DatabaseUtil$DatabaseStats.class b/src/main/java/util/DatabaseUtil$DatabaseStats.class
new file mode 100644
index 0000000..765a7c8
Binary files /dev/null and b/src/main/java/util/DatabaseUtil$DatabaseStats.class differ
diff --git a/src/main/java/util/DatabaseUtil.class b/src/main/java/util/DatabaseUtil.class
new file mode 100644
index 0000000..791fa9c
Binary files /dev/null and b/src/main/java/util/DatabaseUtil.class differ
diff --git a/src/main/java/util/DatabaseUtil.java b/src/main/java/util/DatabaseUtil.java
index df9c192..8ab882a 100644
--- a/src/main/java/util/DatabaseUtil.java
+++ b/src/main/java/util/DatabaseUtil.java
@@ -56,6 +56,8 @@ private DatabaseUtil() {
*/
private void initialize() {
try {
+ // SQLite JDBC 드라이버 명시적 로드
+ loadSQLiteDriver();
loadDatabaseProperties();
setupDatabase();
logger.info("DatabaseUtil initialized successfully");
@@ -65,6 +67,19 @@ private void initialize() {
}
}
+ /**
+ * SQLite JDBC 드라이버 로드
+ */
+ private void loadSQLiteDriver() {
+ try {
+ Class.forName("org.sqlite.JDBC");
+ logger.info("SQLite JDBC driver loaded successfully");
+ } catch (ClassNotFoundException e) {
+ logger.error("SQLite JDBC driver not found", e);
+ throw new RuntimeException("SQLite JDBC driver not found. Please ensure sqlite-jdbc dependency is included.", e);
+ }
+ }
+
/**
* 데이터베이스 설정 로드
*/
@@ -123,39 +138,126 @@ private void createDatabaseIfNotExists() throws SQLException {
}
/**
- * 필요한 테이블들 생성
+ * 스키마 스크립트를 실행하여 테이블 생성
*/
private void createTablesIfNotExist() throws SQLException {
+ try {
+ executeSchemaScript("/sql/schema.sql");
+ logger.info("Database schema initialized successfully");
+
+ // 첫 실행시에만 기본 데이터 삽입
+ if (isFirstTimeSetup()) {
+ executeSchemaScript("/sql/initial_data.sql");
+ logger.info("Initial data loaded successfully");
+ }
+
+ } catch (Exception e) {
+ logger.error("Failed to initialize database schema", e);
+ throw new SQLException("Schema initialization failed", e);
+ }
+ }
+
+ /**
+ * SQL 스크립트 파일 실행
+ */
+ private void executeSchemaScript(String resourcePath) throws SQLException, IOException {
+ try (InputStream is = getClass().getResourceAsStream(resourcePath)) {
+ if (is == null) {
+ logger.warn("Schema script not found: {}, using fallback", resourcePath);
+ createBasicTablesAsFallback();
+ return;
+ }
+
+ String scriptContent = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
+ String[] statements = scriptContent.split(";");
+
+ try (Connection conn = getConnection()) {
+ // 먼저 PRAGMA 문들을 트랜잭션 외부에서 실행
+ try (Statement stmt = conn.createStatement()) {
+ for (String sql : statements) {
+ String trimmedSql = sql.trim();
+ if (!trimmedSql.isEmpty() && !trimmedSql.startsWith("--") &&
+ trimmedSql.toUpperCase().startsWith("PRAGMA")) {
+ stmt.execute(trimmedSql);
+ logger.debug("PRAGMA executed: {}", trimmedSql);
+ }
+ }
+ }
+
+ // 나머지 SQL 문들을 트랜잭션 내에서 실행
+ conn.setAutoCommit(false);
+
+ try (Statement stmt = conn.createStatement()) {
+ for (String sql : statements) {
+ String trimmedSql = sql.trim();
+ if (!trimmedSql.isEmpty() && !trimmedSql.startsWith("--") &&
+ !trimmedSql.toUpperCase().startsWith("PRAGMA")) {
+ stmt.execute(trimmedSql);
+ }
+ }
+ conn.commit(); // 모든 스크립트 실행 성공시 커밋
+ logger.info("Schema script executed successfully: {}", resourcePath);
+
+ } catch (SQLException e) {
+ conn.rollback(); // 오류 발생시 롤백
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * 첫 실행인지 확인
+ */
+ private boolean isFirstTimeSetup() {
+ String checkSQL = "SELECT setting_value FROM user_settings WHERE setting_key = 'schema_initialized_at'";
+
+ try (Connection conn = getConnection();
+ PreparedStatement stmt = conn.prepareStatement(checkSQL)) {
+
+ ResultSet rs = stmt.executeQuery();
+ return !rs.next(); // 설정이 없으면 첫 실행
+
+ } catch (SQLException e) {
+ // 테이블이 없는 경우도 첫 실행으로 간주
+ return true;
+ }
+ }
+
+ /**
+ * 스키마 스크립트가 없을 때 사용할 기본 테이블 생성 (폴백)
+ */
+ private void createBasicTablesAsFallback() throws SQLException {
String[] createTableSQLs = {
- // 일기 테이블
"""
CREATE TABLE IF NOT EXISTS diary (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- content TEXT NOT NULL,
+ content TEXT NOT NULL CHECK(length(content) > 0),
emotion_summary TEXT,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
)
""",
- // 사용자 설정 테이블 (향후 확장용)
"""
CREATE TABLE IF NOT EXISTS user_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
setting_key TEXT UNIQUE NOT NULL,
setting_value TEXT,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
+ setting_type TEXT DEFAULT 'string',
+ description TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
)
""",
- // 백업 로그 테이블
"""
CREATE TABLE IF NOT EXISTS backup_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
backup_path TEXT NOT NULL,
- backup_size INTEGER,
- created_at TEXT NOT NULL,
+ backup_size INTEGER DEFAULT 0,
+ backup_type TEXT DEFAULT 'manual',
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
status TEXT DEFAULT 'SUCCESS'
)
"""
@@ -167,7 +269,7 @@ CREATE TABLE IF NOT EXISTS backup_log (
stmt.execute(sql);
}
}
- logger.info("All required tables created/verified");
+ logger.info("Basic tables created as fallback");
}
}
diff --git a/src/main/java/util/SchemaTest.java b/src/main/java/util/SchemaTest.java
new file mode 100644
index 0000000..5d0395b
--- /dev/null
+++ b/src/main/java/util/SchemaTest.java
@@ -0,0 +1,136 @@
+package util;
+
+/**
+ * 스키마 설계 및 검증 테스트 클래스
+ */
+public class SchemaTest {
+
+ public static void main(String[] args) {
+ System.out.println("=== mindiary 데이터베이스 스키마 테스트 시작 ===");
+
+ try {
+ // 1. DatabaseUtil 초기화 (스키마 자동 생성)
+ System.out.println("\n1. 데이터베이스 초기화 중...");
+ DatabaseUtil dbUtil = DatabaseUtil.getInstance();
+
+ if (dbUtil.testConnection()) {
+ System.out.println("✅ 데이터베이스 연결 성공");
+ } else {
+ System.out.println("❌ 데이터베이스 연결 실패");
+ return;
+ }
+
+ // 2. 스키마 검증
+ System.out.println("\n2. 스키마 검증 중...");
+ SchemaValidator validator = new SchemaValidator();
+ SchemaValidator.SchemaValidationResult validationResult = validator.validateSchema();
+
+ System.out.println("📊 스키마 검증 결과:");
+ System.out.println(" 전체 유효성: " + (validationResult.valid ? "✅ 통과" : "❌ 실패"));
+ System.out.println(" 테이블: " + (validationResult.tablesValid ? "✅" : "❌"));
+ System.out.println(" 인덱스: " + (validationResult.indexesValid ? "✅" : "❌"));
+ System.out.println(" 트리거: " + (validationResult.triggersValid ? "✅" : "❌"));
+ System.out.println(" 뷰: " + (validationResult.viewsValid ? "✅" : "❌"));
+ System.out.println(" 제약조건: " + (validationResult.constraintsValid ? "✅" : "❌"));
+
+ if (!validationResult.errors.isEmpty()) {
+ System.out.println("⚠️ 검증 오류:");
+ validationResult.errors.forEach(error -> System.out.println(" - " + error));
+ }
+
+ // 3. 스키마 정보 조회
+ System.out.println("\n3. 스키마 정보 조회 중...");
+ SchemaValidator.SchemaInfo schemaInfo = validator.getSchemaInfo();
+
+ System.out.println("📋 스키마 정보:");
+ System.out.println(" 버전: " + schemaInfo.schemaVersion);
+ System.out.println(" 초기화 날짜: " + schemaInfo.initializationDate);
+ System.out.println(" 테이블 수: " + (schemaInfo.tables != null ? schemaInfo.tables.size() : 0));
+
+ if (schemaInfo.tables != null) {
+ System.out.println("\n📊 테이블별 상세 정보:");
+ schemaInfo.tables.values().forEach(table -> {
+ System.out.println(String.format(" 📄 %s: %d행, %d컬럼",
+ table.name, table.rowCount,
+ table.columns != null ? table.columns.size() : 0));
+ });
+ }
+
+ // 4. 기본 데이터 확인
+ System.out.println("\n4. 기본 데이터 확인 중...");
+ DatabaseUtil.DatabaseStats stats = dbUtil.getDatabaseStats();
+ System.out.println("📈 데이터베이스 통계:");
+ System.out.println(" 일기 수: " + stats.diaryCount);
+ System.out.println(" 설정 수: " + stats.settingsCount);
+ System.out.println(" 백업 로그 수: " + stats.backupLogCount);
+ System.out.println(" 크기: " + stats.databaseSize + "KB");
+
+ // 5. 샘플 데이터 조회 테스트
+ System.out.println("\n5. 샘플 데이터 조회 테스트 중...");
+ testSampleDataQueries(dbUtil);
+
+ // 6. 설정값 확인
+ System.out.println("\n6. 주요 설정값 확인 중...");
+ String[] importantSettings = {
+ "app_version", "max_diary_length", "emotion_analysis_enabled",
+ "backup_enabled", "theme", "language"
+ };
+
+ for (String setting : importantSettings) {
+ String value = dbUtil.getSetting(setting, "NOT_FOUND");
+ System.out.println(" ⚙️ " + setting + " = " + value);
+ }
+
+ System.out.println("\n=== 스키마 테스트 완료 ===");
+ System.out.println("✅ 모든 테스트가 성공적으로 완료되었습니다!");
+
+ } catch (Exception e) {
+ System.err.println("❌ 스키마 테스트 중 오류 발생: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 샘플 데이터 조회 테스트
+ */
+ private static void testSampleDataQueries(DatabaseUtil dbUtil) {
+ try {
+ // 최근 일기 뷰 테스트
+ java.sql.Connection conn = dbUtil.getConnection();
+ java.sql.Statement stmt = conn.createStatement();
+
+ // 뷰 테스트
+ System.out.println(" 📝 최근 일기 뷰 테스트:");
+ java.sql.ResultSet rs1 = stmt.executeQuery("SELECT COUNT(*) FROM recent_diaries");
+ if (rs1.next()) {
+ System.out.println(" - 최근 30일 일기 수: " + rs1.getInt(1));
+ }
+
+ // 감정 통계 뷰 테스트
+ System.out.println(" 😊 감정 통계 뷰 테스트:");
+ java.sql.ResultSet rs2 = stmt.executeQuery("SELECT emotion_summary, count FROM emotion_stats LIMIT 3");
+ while (rs2.next()) {
+ System.out.println(" - " + rs2.getString("emotion_summary") + ": " + rs2.getInt("count") + "개");
+ }
+
+ // 월별 통계 뷰 테스트
+ System.out.println(" 📅 월별 통계 뷰 테스트:");
+ java.sql.ResultSet rs3 = stmt.executeQuery("SELECT month, diary_count FROM monthly_stats LIMIT 3");
+ while (rs3.next()) {
+ System.out.println(" - " + rs3.getString("month") + ": " + rs3.getInt("diary_count") + "개");
+ }
+
+ // 태그 사용 현황 테스트
+ System.out.println(" 🏷️ 태그 사용 현황 테스트:");
+ java.sql.ResultSet rs4 = stmt.executeQuery("SELECT name, usage_count FROM tags ORDER BY usage_count DESC LIMIT 5");
+ while (rs4.next()) {
+ System.out.println(" - " + rs4.getString("name") + ": " + rs4.getInt("usage_count") + "회");
+ }
+
+ conn.close();
+
+ } catch (Exception e) {
+ System.err.println(" ❌ 샘플 데이터 조회 테스트 실패: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/util/SchemaValidator$ColumnInfo.class b/src/main/java/util/SchemaValidator$ColumnInfo.class
new file mode 100644
index 0000000..5d22ce3
Binary files /dev/null and b/src/main/java/util/SchemaValidator$ColumnInfo.class differ
diff --git a/src/main/java/util/SchemaValidator$SchemaInfo.class b/src/main/java/util/SchemaValidator$SchemaInfo.class
new file mode 100644
index 0000000..f309e67
Binary files /dev/null and b/src/main/java/util/SchemaValidator$SchemaInfo.class differ
diff --git a/src/main/java/util/SchemaValidator$SchemaValidationResult.class b/src/main/java/util/SchemaValidator$SchemaValidationResult.class
new file mode 100644
index 0000000..018189c
Binary files /dev/null and b/src/main/java/util/SchemaValidator$SchemaValidationResult.class differ
diff --git a/src/main/java/util/SchemaValidator$TableInfo.class b/src/main/java/util/SchemaValidator$TableInfo.class
new file mode 100644
index 0000000..f154c8d
Binary files /dev/null and b/src/main/java/util/SchemaValidator$TableInfo.class differ
diff --git a/src/main/java/util/SchemaValidator.class b/src/main/java/util/SchemaValidator.class
new file mode 100644
index 0000000..ae1a7d9
Binary files /dev/null and b/src/main/java/util/SchemaValidator.class differ
diff --git a/src/main/java/util/SchemaValidator.java b/src/main/java/util/SchemaValidator.java
new file mode 100644
index 0000000..10f8838
--- /dev/null
+++ b/src/main/java/util/SchemaValidator.java
@@ -0,0 +1,374 @@
+package util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.*;
+import java.util.*;
+
+/**
+ * 데이터베이스 스키마 정보 및 검증 유틸리티
+ */
+public class SchemaValidator {
+ private static final Logger logger = LoggerFactory.getLogger(SchemaValidator.class);
+
+ private final DatabaseUtil dbUtil;
+
+ public SchemaValidator() {
+ this.dbUtil = DatabaseUtil.getInstance();
+ }
+
+ /**
+ * 스키마 검증 실행
+ */
+ public SchemaValidationResult validateSchema() {
+ SchemaValidationResult result = new SchemaValidationResult();
+
+ try (Connection conn = dbUtil.getConnection()) {
+ result.tablesValid = validateTables(conn);
+ result.indexesValid = validateIndexes(conn);
+ result.triggersValid = validateTriggers(conn);
+ result.viewsValid = validateViews(conn);
+ result.constraintsValid = validateConstraints(conn);
+
+ result.valid = result.tablesValid && result.indexesValid &&
+ result.triggersValid && result.viewsValid && result.constraintsValid;
+
+ if (result.valid) {
+ logger.info("Schema validation passed successfully");
+ } else {
+ logger.warn("Schema validation failed: {}", result.getErrorSummary());
+ }
+
+ } catch (SQLException e) {
+ logger.error("Schema validation error", e);
+ result.valid = false;
+ result.errors.add("Database connection error: " + e.getMessage());
+ }
+
+ return result;
+ }
+
+ /**
+ * 필수 테이블 존재 확인
+ */
+ private boolean validateTables(Connection conn) throws SQLException {
+ String[] requiredTables = {
+ "diary", "user_settings", "backup_log",
+ "emotion_analysis", "tags", "diary_tags", "usage_stats"
+ };
+
+ Set existingTables = getExistingTables(conn);
+ boolean allTablesExist = true;
+
+ for (String table : requiredTables) {
+ if (!existingTables.contains(table)) {
+ logger.warn("Required table missing: {}", table);
+ allTablesExist = false;
+ }
+ }
+
+ logger.info("Table validation: {} tables found, {} required",
+ existingTables.size(), requiredTables.length);
+ return allTablesExist;
+ }
+
+ /**
+ * 인덱스 존재 확인
+ */
+ private boolean validateIndexes(Connection conn) throws SQLException {
+ String[] requiredIndexes = {
+ "idx_diary_created_at", "idx_diary_emotion", "idx_diary_date_only",
+ "idx_settings_key", "idx_backup_created_at", "idx_emotion_diary_id"
+ };
+
+ Set existingIndexes = getExistingIndexes(conn);
+ boolean allIndexesExist = true;
+
+ for (String index : requiredIndexes) {
+ if (!existingIndexes.contains(index)) {
+ logger.warn("Required index missing: {}", index);
+ allIndexesExist = false;
+ }
+ }
+
+ logger.info("Index validation: {} indexes found", existingIndexes.size());
+ return allIndexesExist;
+ }
+
+ /**
+ * 트리거 존재 확인
+ */
+ private boolean validateTriggers(Connection conn) throws SQLException {
+ String[] requiredTriggers = {
+ "update_diary_timestamp", "update_settings_timestamp",
+ "increment_tag_usage", "decrement_tag_usage", "cleanup_emotion_analysis"
+ };
+
+ Set existingTriggers = getExistingTriggers(conn);
+ boolean allTriggersExist = true;
+
+ for (String trigger : requiredTriggers) {
+ if (!existingTriggers.contains(trigger)) {
+ logger.warn("Required trigger missing: {}", trigger);
+ allTriggersExist = false;
+ }
+ }
+
+ logger.info("Trigger validation: {} triggers found", existingTriggers.size());
+ return allTriggersExist;
+ }
+
+ /**
+ * 뷰 존재 확인
+ */
+ private boolean validateViews(Connection conn) throws SQLException {
+ String[] requiredViews = {
+ "recent_diaries", "emotion_stats", "monthly_stats"
+ };
+
+ Set existingViews = getExistingViews(conn);
+ boolean allViewsExist = true;
+
+ for (String view : requiredViews) {
+ if (!existingViews.contains(view)) {
+ logger.warn("Required view missing: {}", view);
+ allViewsExist = false;
+ }
+ }
+
+ logger.info("View validation: {} views found", existingViews.size());
+ return allViewsExist;
+ }
+
+ /**
+ * 제약조건 확인 (기본적인 검증)
+ */
+ private boolean validateConstraints(Connection conn) throws SQLException {
+ // 테이블별 제약조건 확인
+ try (Statement stmt = conn.createStatement()) {
+ // diary 테이블 기본 제약조건 테스트
+ stmt.executeQuery("SELECT 1 FROM diary WHERE 1=0"); // 테이블 접근 테스트
+
+ // user_settings unique 제약조건 테스트
+ stmt.executeQuery("SELECT 1 FROM user_settings WHERE 1=0");
+
+ logger.info("Basic constraint validation passed");
+ return true;
+
+ } catch (SQLException e) {
+ logger.error("Constraint validation failed", e);
+ return false;
+ }
+ }
+
+ /**
+ * 기존 테이블 목록 조회
+ */
+ private Set getExistingTables(Connection conn) throws SQLException {
+ Set tables = new HashSet<>();
+ String sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
+
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ while (rs.next()) {
+ tables.add(rs.getString("name"));
+ }
+ }
+
+ return tables;
+ }
+
+ /**
+ * 기존 인덱스 목록 조회
+ */
+ private Set getExistingIndexes(Connection conn) throws SQLException {
+ Set indexes = new HashSet<>();
+ String sql = "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'";
+
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ while (rs.next()) {
+ indexes.add(rs.getString("name"));
+ }
+ }
+
+ return indexes;
+ }
+
+ /**
+ * 기존 트리거 목록 조회
+ */
+ private Set getExistingTriggers(Connection conn) throws SQLException {
+ Set triggers = new HashSet<>();
+ String sql = "SELECT name FROM sqlite_master WHERE type='trigger'";
+
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ while (rs.next()) {
+ triggers.add(rs.getString("name"));
+ }
+ }
+
+ return triggers;
+ }
+
+ /**
+ * 기존 뷰 목록 조회
+ */
+ private Set getExistingViews(Connection conn) throws SQLException {
+ Set views = new HashSet<>();
+ String sql = "SELECT name FROM sqlite_master WHERE type='view'";
+
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ while (rs.next()) {
+ views.add(rs.getString("name"));
+ }
+ }
+
+ return views;
+ }
+
+ /**
+ * 스키마 정보 조회
+ */
+ public SchemaInfo getSchemaInfo() {
+ SchemaInfo info = new SchemaInfo();
+
+ try (Connection conn = dbUtil.getConnection()) {
+ info.tables = getDetailedTableInfo(conn);
+ info.schemaVersion = dbUtil.getSetting("schema_version", "unknown");
+ info.initializationDate = dbUtil.getSetting("schema_initialized_at", "unknown");
+
+ } catch (SQLException e) {
+ logger.error("Failed to get schema info", e);
+ }
+
+ return info;
+ }
+
+ /**
+ * 상세 테이블 정보 조회
+ */
+ private Map getDetailedTableInfo(Connection conn) throws SQLException {
+ Map tableMap = new HashMap<>();
+ Set tables = getExistingTables(conn);
+
+ for (String tableName : tables) {
+ TableInfo tableInfo = new TableInfo();
+ tableInfo.name = tableName;
+ tableInfo.rowCount = getTableRowCount(conn, tableName);
+ tableInfo.columns = getTableColumns(conn, tableName);
+
+ tableMap.put(tableName, tableInfo);
+ }
+
+ return tableMap;
+ }
+
+ /**
+ * 테이블 행 수 조회
+ */
+ private int getTableRowCount(Connection conn, String tableName) throws SQLException {
+ String sql = "SELECT COUNT(*) FROM " + tableName;
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ return rs.next() ? rs.getInt(1) : 0;
+ }
+ }
+
+ /**
+ * 테이블 컬럼 정보 조회
+ */
+ private List getTableColumns(Connection conn, String tableName) throws SQLException {
+ List columns = new ArrayList<>();
+ String sql = "PRAGMA table_info(" + tableName + ")";
+
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ while (rs.next()) {
+ ColumnInfo column = new ColumnInfo();
+ column.name = rs.getString("name");
+ column.type = rs.getString("type");
+ column.notNull = rs.getInt("notnull") == 1;
+ column.defaultValue = rs.getString("dflt_value");
+ column.primaryKey = rs.getInt("pk") == 1;
+
+ columns.add(column);
+ }
+ }
+
+ return columns;
+ }
+
+ /**
+ * 스키마 검증 결과 클래스
+ */
+ public static class SchemaValidationResult {
+ public boolean valid;
+ public boolean tablesValid;
+ public boolean indexesValid;
+ public boolean triggersValid;
+ public boolean viewsValid;
+ public boolean constraintsValid;
+ public List errors = new ArrayList<>();
+
+ public String getErrorSummary() {
+ return String.join(", ", errors);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SchemaValidation{valid=%s, tables=%s, indexes=%s, triggers=%s, views=%s, constraints=%s}",
+ valid, tablesValid, indexesValid, triggersValid, viewsValid, constraintsValid);
+ }
+ }
+
+ /**
+ * 스키마 정보 클래스
+ */
+ public static class SchemaInfo {
+ public String schemaVersion;
+ public String initializationDate;
+ public Map tables;
+
+ @Override
+ public String toString() {
+ return String.format("SchemaInfo{version='%s', initialized='%s', tables=%d}",
+ schemaVersion, initializationDate, tables != null ? tables.size() : 0);
+ }
+ }
+
+ /**
+ * 테이블 정보 클래스
+ */
+ public static class TableInfo {
+ public String name;
+ public int rowCount;
+ public List columns;
+
+ @Override
+ public String toString() {
+ return String.format("Table{name='%s', rows=%d, columns=%d}",
+ name, rowCount, columns != null ? columns.size() : 0);
+ }
+ }
+
+ /**
+ * 컬럼 정보 클래스
+ */
+ public static class ColumnInfo {
+ public String name;
+ public String type;
+ public boolean notNull;
+ public String defaultValue;
+ public boolean primaryKey;
+
+ @Override
+ public String toString() {
+ return String.format("Column{name='%s', type='%s', notNull=%s, pk=%s}",
+ name, type, notNull, primaryKey);
+ }
+ }
+}
diff --git a/src/main/java/util/SimpleSchemaInitializer.class b/src/main/java/util/SimpleSchemaInitializer.class
new file mode 100644
index 0000000..c9c7d4c
Binary files /dev/null and b/src/main/java/util/SimpleSchemaInitializer.class differ
diff --git a/src/main/java/util/SimpleSchemaInitializer.java b/src/main/java/util/SimpleSchemaInitializer.java
new file mode 100644
index 0000000..874b40d
--- /dev/null
+++ b/src/main/java/util/SimpleSchemaInitializer.java
@@ -0,0 +1,431 @@
+package util;
+
+import java.sql.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 간단한 스키마 초기화 도구
+ * PRAGMA 설정과 스키마 생성을 별도로 처리합니다.
+ */
+public class SimpleSchemaInitializer {
+ private static final String DB_URL = "jdbc:sqlite:mindiary.db";
+ private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ public static void main(String[] args) {
+ System.out.println("=== Simple Schema Initializer ===");
+
+ try {
+ // 1. 연결 및 기본 PRAGMA 설정
+ System.out.println("1. Setting up database connection and PRAGMA settings...");
+ setupPragmaSettings();
+
+ // 2. 테이블 생성
+ System.out.println("2. Creating tables...");
+ createTables();
+
+ // 3. 인덱스 생성
+ System.out.println("3. Creating indexes...");
+ createIndexes();
+
+ // 4. 트리거 생성
+ System.out.println("4. Creating triggers...");
+ createTriggers();
+
+ // 5. 뷰 생성
+ System.out.println("5. Creating views...");
+ createViews();
+
+ // 6. 기본 데이터 삽입
+ System.out.println("6. Inserting initial data...");
+ insertInitialData();
+
+ // 7. 검증
+ System.out.println("7. Validating schema...");
+ validateSchema();
+
+ System.out.println("=== Schema initialization completed successfully! ===");
+
+ } catch (Exception e) {
+ System.err.println("Schema initialization failed: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ private static void setupPragmaSettings() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL);
+ Statement stmt = conn.createStatement()) {
+
+ stmt.execute("PRAGMA foreign_keys = ON");
+ stmt.execute("PRAGMA journal_mode = WAL");
+ stmt.execute("PRAGMA synchronous = NORMAL");
+ stmt.execute("PRAGMA cache_size = 1000");
+ stmt.execute("PRAGMA temp_store = MEMORY");
+
+ System.out.println(" ✅ PRAGMA settings applied");
+ }
+ }
+
+ private static void createTables() throws SQLException {
+ String[] createTableSQLs = {
+ // diary 테이블
+ """
+ CREATE TABLE IF NOT EXISTS diary (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ content TEXT NOT NULL CHECK(length(content) > 0),
+ emotion_summary TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ CONSTRAINT content_length_check CHECK(length(content) <= 10000)
+ )
+ """,
+
+ // user_settings 테이블
+ """
+ CREATE TABLE IF NOT EXISTS user_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ setting_key TEXT UNIQUE NOT NULL CHECK(length(setting_key) > 0),
+ setting_value TEXT,
+ setting_type TEXT DEFAULT 'string' CHECK(setting_type IN ('string', 'number', 'boolean', 'json')),
+ description TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
+ )
+ """,
+
+ // backup_log 테이블
+ """
+ CREATE TABLE IF NOT EXISTS backup_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ backup_path TEXT NOT NULL,
+ backup_size INTEGER DEFAULT 0 CHECK(backup_size >= 0),
+ backup_type TEXT DEFAULT 'manual' CHECK(backup_type IN ('manual', 'auto', 'scheduled')),
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ status TEXT DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'SUCCESS', 'FAILED', 'PARTIAL')),
+ error_message TEXT
+ )
+ """,
+
+ // emotion_analysis 테이블
+ """
+ CREATE TABLE IF NOT EXISTS emotion_analysis (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ diary_id INTEGER NOT NULL,
+ emotion_type TEXT NOT NULL CHECK(emotion_type IN ('positive', 'negative', 'neutral', 'mixed')),
+ confidence_score REAL CHECK(confidence_score >= 0.0 AND confidence_score <= 1.0),
+ keywords TEXT,
+ analysis_method TEXT DEFAULT 'rule_based',
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ FOREIGN KEY (diary_id) REFERENCES diary(id) ON DELETE CASCADE
+ )
+ """,
+
+ // tags 테이블
+ """
+ CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL CHECK(length(name) > 0 AND length(name) <= 50),
+ color TEXT DEFAULT '#007bff',
+ description TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ usage_count INTEGER DEFAULT 0 CHECK(usage_count >= 0)
+ )
+ """,
+
+ // diary_tags 테이블
+ """
+ CREATE TABLE IF NOT EXISTS diary_tags (
+ diary_id INTEGER NOT NULL,
+ tag_id INTEGER NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ PRIMARY KEY (diary_id, tag_id),
+ FOREIGN KEY (diary_id) REFERENCES diary(id) ON DELETE CASCADE,
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+ )
+ """,
+
+ // usage_stats 테이블
+ """
+ CREATE TABLE IF NOT EXISTS usage_stats (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ stat_date TEXT NOT NULL UNIQUE,
+ diaries_created INTEGER DEFAULT 0 CHECK(diaries_created >= 0),
+ total_characters INTEGER DEFAULT 0 CHECK(total_characters >= 0),
+ emotions_analyzed INTEGER DEFAULT 0 CHECK(emotions_analyzed >= 0),
+ tags_used INTEGER DEFAULT 0 CHECK(tags_used >= 0)
+ )
+ """
+ };
+
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ for (String sql : createTableSQLs) {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute(sql);
+ }
+ }
+ System.out.println(" ✅ All tables created successfully");
+ }
+ }
+
+ private static void createIndexes() throws SQLException {
+ String[] indexSQLs = {
+ "CREATE INDEX IF NOT EXISTS idx_diary_created_at ON diary(created_at DESC)",
+ "CREATE INDEX IF NOT EXISTS idx_diary_emotion ON diary(emotion_summary) WHERE emotion_summary IS NOT NULL",
+ "CREATE INDEX IF NOT EXISTS idx_diary_date_only ON diary(date(created_at))",
+ "CREATE INDEX IF NOT EXISTS idx_settings_key ON user_settings(setting_key)",
+ "CREATE INDEX IF NOT EXISTS idx_backup_created_at ON backup_log(created_at DESC)",
+ "CREATE INDEX IF NOT EXISTS idx_emotion_diary_id ON emotion_analysis(diary_id)",
+ "CREATE INDEX IF NOT EXISTS idx_emotion_type ON emotion_analysis(emotion_type)",
+ "CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)",
+ "CREATE INDEX IF NOT EXISTS idx_tags_usage ON tags(usage_count DESC)",
+ "CREATE INDEX IF NOT EXISTS idx_diary_tags_tag ON diary_tags(tag_id)",
+ "CREATE INDEX IF NOT EXISTS idx_stats_date ON usage_stats(stat_date DESC)"
+ };
+
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ for (String sql : indexSQLs) {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute(sql);
+ }
+ }
+ System.out.println(" ✅ All indexes created successfully");
+ }
+ }
+
+ private static void createTriggers() throws SQLException {
+ String[] triggerSQLs = {
+ // diary updated_at 트리거
+ """
+ CREATE TRIGGER IF NOT EXISTS update_diary_timestamp
+ AFTER UPDATE ON diary
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+ BEGIN
+ UPDATE diary SET updated_at = datetime('now', 'localtime') WHERE id = NEW.id;
+ END
+ """,
+
+ // user_settings updated_at 트리거
+ """
+ CREATE TRIGGER IF NOT EXISTS update_settings_timestamp
+ AFTER UPDATE ON user_settings
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+ BEGIN
+ UPDATE user_settings SET updated_at = datetime('now', 'localtime') WHERE id = NEW.id;
+ END
+ """,
+
+ // tags 사용 횟수 증가 트리거
+ """
+ CREATE TRIGGER IF NOT EXISTS increment_tag_usage
+ AFTER INSERT ON diary_tags
+ FOR EACH ROW
+ BEGIN
+ UPDATE tags SET usage_count = usage_count + 1 WHERE id = NEW.tag_id;
+ END
+ """,
+
+ // tags 사용 횟수 감소 트리거
+ """
+ CREATE TRIGGER IF NOT EXISTS decrement_tag_usage
+ AFTER DELETE ON diary_tags
+ FOR EACH ROW
+ BEGIN
+ UPDATE tags SET usage_count = usage_count - 1 WHERE id = OLD.tag_id;
+ END
+ """
+ };
+
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ for (String sql : triggerSQLs) {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute(sql);
+ }
+ }
+ System.out.println(" ✅ All triggers created successfully");
+ }
+ }
+
+ private static void createViews() throws SQLException {
+ String[] viewSQLs = {
+ // 최근 일기 뷰
+ """
+ CREATE VIEW IF NOT EXISTS recent_diaries AS
+ SELECT
+ id,
+ content,
+ emotion_summary,
+ created_at,
+ updated_at,
+ date(created_at) as diary_date,
+ length(content) as content_length
+ FROM diary
+ WHERE date(created_at) >= date('now', '-30 days')
+ ORDER BY created_at DESC
+ """,
+
+ // 감정별 통계 뷰
+ """
+ CREATE VIEW IF NOT EXISTS emotion_stats AS
+ SELECT
+ emotion_summary,
+ COUNT(*) as count,
+ ROUND(AVG(length(content)), 2) as avg_content_length,
+ MIN(created_at) as first_entry,
+ MAX(created_at) as last_entry
+ FROM diary
+ WHERE emotion_summary IS NOT NULL
+ GROUP BY emotion_summary
+ ORDER BY count DESC
+ """,
+
+ // 월별 통계 뷰
+ """
+ CREATE VIEW IF NOT EXISTS monthly_stats AS
+ SELECT
+ strftime('%Y-%m', created_at) as month,
+ COUNT(*) as diary_count,
+ SUM(length(content)) as total_characters,
+ ROUND(AVG(length(content)), 2) as avg_content_length,
+ COUNT(DISTINCT emotion_summary) as unique_emotions
+ FROM diary
+ GROUP BY strftime('%Y-%m', created_at)
+ ORDER BY month DESC
+ """
+ };
+
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ for (String sql : viewSQLs) {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute(sql);
+ }
+ }
+ System.out.println(" ✅ All views created successfully");
+ }
+ }
+
+ private static void insertInitialData() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL)) {
+ String now = LocalDateTime.now().format(TIMESTAMP_FORMAT);
+
+ // 기본 설정값
+ String settingsSQL = """
+ INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """;
+
+ String[][] settings = {
+ {"app_version", "1.0.0", "string", "Application version"},
+ {"max_diary_length", "10000", "number", "Maximum diary content length"},
+ {"emotion_analysis_enabled", "true", "boolean", "Enable emotion analysis"},
+ {"backup_enabled", "true", "boolean", "Enable backup functionality"},
+ {"theme", "light", "string", "UI theme"},
+ {"language", "ko", "string", "Application language"}
+ };
+
+ try (PreparedStatement pstmt = conn.prepareStatement(settingsSQL)) {
+ for (String[] setting : settings) {
+ pstmt.setString(1, setting[0]);
+ pstmt.setString(2, setting[1]);
+ pstmt.setString(3, setting[2]);
+ pstmt.setString(4, setting[3]);
+ pstmt.setString(5, now);
+ pstmt.setString(6, now);
+ pstmt.executeUpdate();
+ }
+ }
+
+ // 기본 태그
+ String tagSQL = """
+ INSERT OR REPLACE INTO tags (name, color, description, created_at, usage_count)
+ VALUES (?, ?, ?, ?, ?)
+ """;
+
+ String[][] tags = {
+ {"일상", "#007bff", "Daily life records"},
+ {"감정", "#dc3545", "Emotional experiences"},
+ {"성찰", "#6f42c1", "Self-reflection"},
+ {"목표", "#28a745", "Goals and plans"},
+ {"관계", "#fd7e14", "Relationships"},
+ {"성장", "#20c997", "Personal growth"},
+ {"여행", "#6610f2", "Travel experiences"},
+ {"학습", "#e83e8c", "Learning and study"},
+ {"건강", "#17a2b8", "Health and fitness"},
+ {"취미", "#ffc107", "Hobbies and interests"}
+ };
+
+ try (PreparedStatement pstmt = conn.prepareStatement(tagSQL)) {
+ for (String[] tag : tags) {
+ pstmt.setString(1, tag[0]);
+ pstmt.setString(2, tag[1]);
+ pstmt.setString(3, tag[2]);
+ pstmt.setString(4, now);
+ pstmt.setInt(5, 0);
+ pstmt.executeUpdate();
+ }
+ }
+
+ // 샘플 일기
+ String diarySQL = """
+ INSERT INTO diary (content, emotion_summary, created_at, updated_at)
+ VALUES (?, ?, ?, ?)
+ """;
+
+ try (PreparedStatement pstmt = conn.prepareStatement(diarySQL)) {
+ pstmt.setString(1, "오늘은 새로운 데이터베이스 스키마를 완성했다. 모든 테이블과 인덱스, 트리거가 정상적으로 작동하는 것을 확인했다. 정말 뿌듯한 하루였다!");
+ pstmt.setString(2, "positive");
+ pstmt.setString(3, now);
+ pstmt.setString(4, now);
+ pstmt.executeUpdate();
+ }
+
+ System.out.println(" ✅ Initial data inserted successfully");
+ }
+ }
+
+ private static void validateSchema() throws SQLException {
+ try (Connection conn = DriverManager.getConnection(DB_URL);
+ Statement stmt = conn.createStatement()) {
+
+ // 테이블 수 확인
+ ResultSet tables = stmt.executeQuery("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
+ tables.next();
+ int tableCount = tables.getInt(1);
+ System.out.println(" 📊 Tables: " + tableCount);
+
+ // 인덱스 수 확인
+ ResultSet indexes = stmt.executeQuery("SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'");
+ indexes.next();
+ int indexCount = indexes.getInt(1);
+ System.out.println(" 📊 Indexes: " + indexCount);
+
+ // 트리거 수 확인
+ ResultSet triggers = stmt.executeQuery("SELECT COUNT(*) FROM sqlite_master WHERE type='trigger'");
+ triggers.next();
+ int triggerCount = triggers.getInt(1);
+ System.out.println(" 📊 Triggers: " + triggerCount);
+
+ // 뷰 수 확인
+ ResultSet views = stmt.executeQuery("SELECT COUNT(*) FROM sqlite_master WHERE type='view'");
+ views.next();
+ int viewCount = views.getInt(1);
+ System.out.println(" 📊 Views: " + viewCount);
+
+ // 데이터 확인
+ ResultSet diaries = stmt.executeQuery("SELECT COUNT(*) FROM diary");
+ diaries.next();
+ System.out.println(" 📊 Sample diaries: " + diaries.getInt(1));
+
+ ResultSet settings = stmt.executeQuery("SELECT COUNT(*) FROM user_settings");
+ settings.next();
+ System.out.println(" 📊 Settings: " + settings.getInt(1));
+
+ ResultSet tagsResult = stmt.executeQuery("SELECT COUNT(*) FROM tags");
+ tagsResult.next();
+ System.out.println(" 📊 Tags: " + tagsResult.getInt(1));
+
+ System.out.println(" ✅ Schema validation completed");
+ }
+ }
+}
diff --git a/src/main/resources/sql/initial_data.sql b/src/main/resources/sql/initial_data.sql
new file mode 100644
index 0000000..825a4a7
--- /dev/null
+++ b/src/main/resources/sql/initial_data.sql
@@ -0,0 +1,169 @@
+-- ========================================
+-- mindiary 기본 데이터 삽입 스크립트
+-- ========================================
+
+-- ========================================
+-- 1. 기본 설정값 삽입
+-- ========================================
+
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description) VALUES
+-- 애플리케이션 기본 설정
+('app_version', '1.0.0', 'string', '애플리케이션 버전'),
+('app_name', 'mindiary', 'string', '애플리케이션 이름'),
+('app_initialized_at', datetime('now', 'localtime'), 'string', '애플리케이션 초기화 시간'),
+
+-- 일기 관련 설정
+('max_diary_length', '10000', 'number', '일기 최대 글자 수'),
+('min_diary_length', '10', 'number', '일기 최소 글자 수'),
+('auto_save_enabled', 'true', 'boolean', '자동 저장 기능 활성화'),
+('auto_save_interval', '30', 'number', '자동 저장 간격 (초)'),
+
+-- 감정 분석 설정
+('emotion_analysis_enabled', 'true', 'boolean', '감정 분석 기능 활성화'),
+('emotion_analysis_method', 'rule_based', 'string', '감정 분석 방법'),
+('emotion_confidence_threshold', '0.6', 'number', '감정 분석 신뢰도 임계값'),
+('supported_emotions', '["positive", "negative", "neutral", "mixed"]', 'json', '지원하는 감정 유형'),
+
+-- 백업 관련 설정
+('backup_enabled', 'true', 'boolean', '백업 기능 활성화'),
+('backup_interval_days', '7', 'number', '백업 주기 (일)'),
+('backup_retention_days', '30', 'number', '백업 파일 보관 기간 (일)'),
+('auto_backup_enabled', 'false', 'boolean', '자동 백업 활성화'),
+
+-- UI/UX 설정
+('theme', 'light', 'string', '테마 설정 (light/dark)'),
+('language', 'ko', 'string', '언어 설정'),
+('timezone', 'Asia/Seoul', 'string', '시간대 설정'),
+('date_format', 'yyyy-MM-dd HH:mm:ss', 'string', '날짜 형식'),
+
+-- 보안 설정
+('password_protection', 'false', 'boolean', '비밀번호 보호 활성화'),
+('session_timeout', '1800', 'number', '세션 타임아웃 (초)'),
+('max_login_attempts', '5', 'number', '최대 로그인 시도 횟수'),
+
+-- 성능 설정
+('cache_enabled', 'true', 'boolean', '캐시 기능 활성화'),
+('cache_size', '100', 'number', '캐시 크기 (항목 수)'),
+('lazy_loading', 'true', 'boolean', '지연 로딩 활성화'),
+('pagination_size', '20', 'number', '페이지네이션 크기'),
+
+-- 알림 설정
+('notifications_enabled', 'true', 'boolean', '알림 기능 활성화'),
+('reminder_enabled', 'false', 'boolean', '일기 작성 리마인더 활성화'),
+('reminder_time', '21:00', 'string', '리마인더 시간'),
+
+-- 통계 설정
+('stats_enabled', 'true', 'boolean', '통계 기능 활성화'),
+('stats_retention_days', '365', 'number', '통계 데이터 보관 기간 (일)'),
+('daily_stats_enabled', 'true', 'boolean', '일간 통계 수집 활성화'),
+
+-- 개발/디버그 설정
+('debug_mode', 'false', 'boolean', '디버그 모드 활성화'),
+('log_level', 'INFO', 'string', '로그 레벨'),
+('performance_monitoring', 'false', 'boolean', '성능 모니터링 활성화');
+
+-- ========================================
+-- 2. 기본 태그 삽입
+-- ========================================
+
+INSERT OR REPLACE INTO tags (name, color, description) VALUES
+('일상', '#007bff', '일상적인 생활에 대한 기록'),
+('감정', '#dc3545', '감정적인 경험이나 느낌'),
+('성찰', '#6f42c1', '자기 반성이나 깊은 생각'),
+('목표', '#28a745', '목표 설정이나 계획에 관한 내용'),
+('관계', '#fd7e14', '인간관계나 소통에 관한 이야기'),
+('성장', '#20c997', '개인적 성장이나 발전'),
+('여행', '#6610f2', '여행이나 새로운 경험'),
+('학습', '#e83e8c', '공부나 새로운 지식 습득'),
+('건강', '#17a2b8', '건강이나 운동에 관한 내용'),
+('취미', '#ffc107', '취미 활동이나 여가 시간'),
+('업무', '#6c757d', '직장이나 업무 관련 내용'),
+('가족', '#fd7e14', '가족과의 시간이나 추억'),
+('친구', '#20c997', '친구들과의 만남이나 우정'),
+('도전', '#dc3545', '새로운 도전이나 모험'),
+('감사', '#28a745', '감사한 일들에 대한 기록');
+
+-- ========================================
+-- 3. 샘플 일기 데이터 삽입 (개발/테스트용)
+-- ========================================
+
+-- 최근 며칠간의 샘플 일기들
+INSERT OR REPLACE INTO diary (content, emotion_summary, created_at, updated_at) VALUES
+(
+ '오늘은 새로운 프로젝트를 시작했다. mindiary 애플리케이션을 개발하는 것인데, 일기를 디지털로 관리할 수 있는 시스템을 만드는 것이다. 처음에는 복잡해 보였지만, 하나씩 단계별로 접근하니 생각보다 재미있다. 데이터베이스 설계부터 시작해서 UI까지 모든 것을 고려해야 하지만, 그만큼 배우는 것도 많을 것 같다.',
+ 'positive',
+ datetime('now', '-2 days', 'localtime'),
+ datetime('now', '-2 days', 'localtime')
+),
+(
+ '코딩을 하면서 여러 번 막혔지만, 하나씩 해결해나가는 과정이 즐겁다. 특히 SQLite 데이터베이스를 설계할 때 정규화와 성능을 동시에 고려해야 하는 부분이 흥미로웠다. 인덱스를 적절히 설정하고, 트리거를 활용해서 자동화할 수 있는 부분들을 찾아내는 것도 재미있는 작업이었다.',
+ 'positive',
+ datetime('now', '-1 days', 'localtime'),
+ datetime('now', '-1 days', 'localtime')
+),
+(
+ '오늘은 좀 피곤했다. 밤늦게까지 코딩을 하다 보니 수면 부족이 심하다. 그래도 프로젝트가 조금씩 형태를 갖춰가는 것을 보니 뿌듯하다. 내일은 좀 더 체계적으로 시간을 관리해서 건강도 챙기면서 개발을 진행해야겠다.',
+ 'neutral',
+ datetime('now', 'localtime'),
+ datetime('now', 'localtime')
+);
+
+-- ========================================
+-- 4. 샘플 감정 분석 데이터 삽입
+-- ========================================
+
+INSERT OR REPLACE INTO emotion_analysis (diary_id, emotion_type, confidence_score, keywords, analysis_method) VALUES
+(1, 'positive', 0.85, '["새로운", "프로젝트", "재미있다", "배우는"]', 'rule_based'),
+(2, 'positive', 0.90, '["즐겁다", "흥미로웠다", "재미있는"]', 'rule_based'),
+(3, 'neutral', 0.75, '["피곤했다", "뿌듯하다", "체계적으로"]', 'rule_based');
+
+-- ========================================
+-- 5. 샘플 일기-태그 연결
+-- ========================================
+
+INSERT OR REPLACE INTO diary_tags (diary_id, tag_id) VALUES
+(1, 1), -- 일상
+(1, 4), -- 목표
+(1, 8), -- 학습
+(2, 8), -- 학습
+(2, 6), -- 성장
+(2, 14), -- 도전
+(3, 1), -- 일상
+(3, 3), -- 성찰
+(3, 9); -- 건강
+
+-- ========================================
+-- 6. 초기 통계 데이터 생성
+-- ========================================
+
+INSERT OR REPLACE INTO usage_stats (stat_date, diaries_created, total_characters, emotions_analyzed, tags_used)
+SELECT
+ date(created_at) as stat_date,
+ COUNT(*) as diaries_created,
+ SUM(length(content)) as total_characters,
+ COUNT(CASE WHEN emotion_summary IS NOT NULL THEN 1 END) as emotions_analyzed,
+ (SELECT COUNT(*) FROM diary_tags WHERE diary_id IN (SELECT id FROM diary WHERE date(created_at) = date(d.created_at))) as tags_used
+FROM diary d
+GROUP BY date(created_at);
+
+-- ========================================
+-- 7. 백업 로그 초기화
+-- ========================================
+
+INSERT INTO backup_log (backup_path, backup_type, status, created_at) VALUES
+('mindiary_initial_backup.db', 'manual', 'SUCCESS', datetime('now', 'localtime'));
+
+-- ========================================
+-- 8. 데이터 초기화 완료 표시
+-- ========================================
+
+UPDATE user_settings
+SET setting_value = datetime('now', 'localtime'), updated_at = datetime('now', 'localtime')
+WHERE setting_key = 'data_initialized_at'
+ OR setting_key = 'sample_data_loaded';
+
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description) VALUES
+('data_initialized_at', datetime('now', 'localtime'), 'string', '데이터 초기화 완료 시간'),
+('sample_data_loaded', 'true', 'boolean', '샘플 데이터 로드 여부'),
+('total_diaries', (SELECT COUNT(*) FROM diary), 'number', '전체 일기 개수'),
+('total_tags', (SELECT COUNT(*) FROM tags), 'number', '전체 태그 개수');
diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql
new file mode 100644
index 0000000..77d3b02
--- /dev/null
+++ b/src/main/resources/sql/schema.sql
@@ -0,0 +1,245 @@
+-- ========================================
+-- mindiary 데이터베이스 스키마 초기화 스크립트
+-- ========================================
+
+-- SQLite 설정
+PRAGMA foreign_keys = ON;
+PRAGMA journal_mode = WAL;
+PRAGMA synchronous = NORMAL;
+PRAGMA cache_size = 1000;
+PRAGMA temp_store = MEMORY;
+
+-- ========================================
+-- 1. 메인 테이블들
+-- ========================================
+
+-- 일기 테이블 (핵심 테이블)
+CREATE TABLE IF NOT EXISTS diary (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ content TEXT NOT NULL CHECK(length(content) > 0),
+ emotion_summary TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ -- 검증 제약조건
+ CONSTRAINT content_length_check CHECK(length(content) <= 10000),
+ CONSTRAINT emotion_format_check CHECK(emotion_summary IS NULL OR length(emotion_summary) <= 100),
+ CONSTRAINT date_format_check CHECK(
+ created_at GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]'
+ AND updated_at GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]'
+ )
+);
+
+-- 사용자 설정 테이블
+CREATE TABLE IF NOT EXISTS user_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ setting_key TEXT UNIQUE NOT NULL CHECK(length(setting_key) > 0),
+ setting_value TEXT,
+ setting_type TEXT DEFAULT 'string' CHECK(setting_type IN ('string', 'number', 'boolean', 'json')),
+ description TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ -- 제약조건
+ CONSTRAINT key_format_check CHECK(setting_key NOT GLOB '* *' AND setting_key GLOB '[a-z_]*')
+);
+
+-- 백업 로그 테이블
+CREATE TABLE IF NOT EXISTS backup_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ backup_path TEXT NOT NULL,
+ backup_size INTEGER DEFAULT 0 CHECK(backup_size >= 0),
+ backup_type TEXT DEFAULT 'manual' CHECK(backup_type IN ('manual', 'auto', 'scheduled')),
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ status TEXT DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'SUCCESS', 'FAILED', 'PARTIAL')),
+ error_message TEXT,
+
+ -- 제약조건
+ CONSTRAINT backup_path_check CHECK(length(backup_path) > 0)
+);
+
+-- ========================================
+-- 2. 확장 테이블들 (향후 기능용)
+-- ========================================
+
+-- 감정 분석 상세 정보 테이블
+CREATE TABLE IF NOT EXISTS emotion_analysis (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ diary_id INTEGER NOT NULL,
+ emotion_type TEXT NOT NULL CHECK(emotion_type IN ('positive', 'negative', 'neutral', 'mixed')),
+ confidence_score REAL CHECK(confidence_score >= 0.0 AND confidence_score <= 1.0),
+ keywords TEXT, -- JSON 형태로 저장
+ analysis_method TEXT DEFAULT 'rule_based',
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ -- 외래키
+ FOREIGN KEY (diary_id) REFERENCES diary(id) ON DELETE CASCADE
+);
+
+-- 태그 테이블 (일기 분류용)
+CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL CHECK(length(name) > 0 AND length(name) <= 50),
+ color TEXT DEFAULT '#007bff' CHECK(color GLOB '#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]'),
+ description TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ usage_count INTEGER DEFAULT 0 CHECK(usage_count >= 0)
+);
+
+-- 일기-태그 연결 테이블 (다대다 관계)
+CREATE TABLE IF NOT EXISTS diary_tags (
+ diary_id INTEGER NOT NULL,
+ tag_id INTEGER NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+
+ PRIMARY KEY (diary_id, tag_id),
+ FOREIGN KEY (diary_id) REFERENCES diary(id) ON DELETE CASCADE,
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+);
+
+-- 사용 통계 테이블
+CREATE TABLE IF NOT EXISTS usage_stats (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ stat_date TEXT NOT NULL, -- YYYY-MM-DD 형식
+ diaries_created INTEGER DEFAULT 0 CHECK(diaries_created >= 0),
+ total_characters INTEGER DEFAULT 0 CHECK(total_characters >= 0),
+ emotions_analyzed INTEGER DEFAULT 0 CHECK(emotions_analyzed >= 0),
+ tags_used INTEGER DEFAULT 0 CHECK(tags_used >= 0),
+
+ UNIQUE(stat_date),
+ CONSTRAINT date_format_check CHECK(stat_date GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]')
+);
+
+-- ========================================
+-- 3. 인덱스 생성 (성능 최적화)
+-- ========================================
+
+-- diary 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_diary_created_at ON diary(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_diary_emotion ON diary(emotion_summary) WHERE emotion_summary IS NOT NULL;
+CREATE INDEX IF NOT EXISTS idx_diary_content_fts ON diary(content); -- Full Text Search 준비
+CREATE INDEX IF NOT EXISTS idx_diary_date_only ON diary(date(created_at));
+
+-- user_settings 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_settings_key ON user_settings(setting_key);
+CREATE INDEX IF NOT EXISTS idx_settings_type ON user_settings(setting_type);
+
+-- backup_log 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_backup_created_at ON backup_log(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_backup_status ON backup_log(status);
+
+-- emotion_analysis 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_emotion_diary_id ON emotion_analysis(diary_id);
+CREATE INDEX IF NOT EXISTS idx_emotion_type ON emotion_analysis(emotion_type);
+CREATE INDEX IF NOT EXISTS idx_emotion_confidence ON emotion_analysis(confidence_score DESC);
+
+-- tags 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
+CREATE INDEX IF NOT EXISTS idx_tags_usage ON tags(usage_count DESC);
+
+-- diary_tags 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_diary_tags_tag ON diary_tags(tag_id);
+CREATE INDEX IF NOT EXISTS idx_diary_tags_diary ON diary_tags(diary_id);
+
+-- usage_stats 테이블 인덱스
+CREATE INDEX IF NOT EXISTS idx_stats_date ON usage_stats(stat_date DESC);
+
+-- ========================================
+-- 4. 트리거 생성 (자동화)
+-- ========================================
+
+-- diary 테이블 updated_at 자동 업데이트
+CREATE TRIGGER IF NOT EXISTS update_diary_timestamp
+ AFTER UPDATE ON diary
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+BEGIN
+ UPDATE diary SET updated_at = datetime('now', 'localtime') WHERE id = NEW.id;
+END;
+
+-- user_settings 테이블 updated_at 자동 업데이트
+CREATE TRIGGER IF NOT EXISTS update_settings_timestamp
+ AFTER UPDATE ON user_settings
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+BEGIN
+ UPDATE user_settings SET updated_at = datetime('now', 'localtime') WHERE id = NEW.id;
+END;
+
+-- tags 사용 횟수 자동 증가
+CREATE TRIGGER IF NOT EXISTS increment_tag_usage
+ AFTER INSERT ON diary_tags
+ FOR EACH ROW
+BEGIN
+ UPDATE tags SET usage_count = usage_count + 1 WHERE id = NEW.tag_id;
+END;
+
+-- tags 사용 횟수 자동 감소
+CREATE TRIGGER IF NOT EXISTS decrement_tag_usage
+ AFTER DELETE ON diary_tags
+ FOR EACH ROW
+BEGIN
+ UPDATE tags SET usage_count = usage_count - 1 WHERE id = OLD.tag_id;
+END;
+
+-- 일기 삭제 시 감정 분석 데이터도 함께 삭제 (CASCADE 보완)
+CREATE TRIGGER IF NOT EXISTS cleanup_emotion_analysis
+ AFTER DELETE ON diary
+ FOR EACH ROW
+BEGIN
+ DELETE FROM emotion_analysis WHERE diary_id = OLD.id;
+ DELETE FROM diary_tags WHERE diary_id = OLD.id;
+END;
+
+-- ========================================
+-- 5. 뷰 생성 (편의성)
+-- ========================================
+
+-- 최근 일기 뷰 (30일)
+CREATE VIEW IF NOT EXISTS recent_diaries AS
+SELECT
+ id,
+ content,
+ emotion_summary,
+ created_at,
+ updated_at,
+ date(created_at) as diary_date,
+ length(content) as content_length
+FROM diary
+WHERE date(created_at) >= date('now', '-30 days')
+ORDER BY created_at DESC;
+
+-- 감정별 통계 뷰
+CREATE VIEW IF NOT EXISTS emotion_stats AS
+SELECT
+ emotion_summary,
+ COUNT(*) as count,
+ ROUND(AVG(length(content)), 2) as avg_content_length,
+ MIN(created_at) as first_entry,
+ MAX(created_at) as last_entry
+FROM diary
+WHERE emotion_summary IS NOT NULL
+GROUP BY emotion_summary
+ORDER BY count DESC;
+
+-- 월별 통계 뷰
+CREATE VIEW IF NOT EXISTS monthly_stats AS
+SELECT
+ strftime('%Y-%m', created_at) as month,
+ COUNT(*) as diary_count,
+ SUM(length(content)) as total_characters,
+ ROUND(AVG(length(content)), 2) as avg_content_length,
+ COUNT(DISTINCT emotion_summary) as unique_emotions
+FROM diary
+GROUP BY strftime('%Y-%m', created_at)
+ORDER BY month DESC;
+
+-- ========================================
+-- 스키마 버전 정보
+-- ========================================
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description, created_at, updated_at)
+VALUES ('schema_version', '1.0.0', 'string', '데이터베이스 스키마 버전', datetime('now', 'localtime'), datetime('now', 'localtime'));
+
+-- 스키마 초기화 완료 로그
+INSERT OR REPLACE INTO user_settings (setting_key, setting_value, setting_type, description, created_at, updated_at)
+VALUES ('schema_initialized_at', datetime('now', 'localtime'), 'string', '스키마 초기화 완료 시간', datetime('now', 'localtime'), datetime('now', 'localtime'));
diff --git a/src/test/java/util/DatabaseUtilTest.java b/src/test/java/util/DatabaseUtilTest.java
new file mode 100644
index 0000000..004c572
--- /dev/null
+++ b/src/test/java/util/DatabaseUtilTest.java
@@ -0,0 +1,128 @@
+package util;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.ResultSet;
+
+/**
+ * DatabaseUtil 테스트 클래스
+ * SQLite 연결 및 기본 기능을 검증합니다.
+ */
+public class DatabaseUtilTest {
+
+ private DatabaseUtil dbUtil;
+
+ @BeforeEach
+ void setUp() {
+ dbUtil = DatabaseUtil.getInstance();
+ }
+
+ @Test
+ @DisplayName("데이터베이스 연결 테스트")
+ void testConnection() {
+ // 연결 테스트
+ assertTrue(dbUtil.testConnection(), "데이터베이스 연결이 실패했습니다.");
+
+ // 실제 연결 생성 및 검증
+ assertDoesNotThrow(() -> {
+ try (Connection conn = dbUtil.getConnection()) {
+ assertNotNull(conn, "연결 객체가 null입니다.");
+ assertTrue(conn.isValid(10), "연결이 유효하지 않습니다.");
+ }
+ });
+ }
+
+ @Test
+ @DisplayName("테이블 존재 여부 확인")
+ void testTablesExist() {
+ assertDoesNotThrow(() -> {
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement()) {
+
+ // diary 테이블 확인
+ ResultSet rs1 = stmt.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='diary'");
+ assertTrue(rs1.next(), "diary 테이블이 존재하지 않습니다.");
+
+ // user_settings 테이블 확인
+ ResultSet rs2 = stmt.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='user_settings'");
+ assertTrue(rs2.next(), "user_settings 테이블이 존재하지 않습니다.");
+
+ // backup_log 테이블 확인
+ ResultSet rs3 = stmt.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='backup_log'");
+ assertTrue(rs3.next(), "backup_log 테이블이 존재하지 않습니다.");
+ }
+ });
+ }
+
+ @Test
+ @DisplayName("기본 설정값 확인")
+ void testDefaultSettings() {
+ // 기본 설정값들이 제대로 삽입되었는지 확인
+ assertEquals("1.0.0", dbUtil.getSetting("app_version", ""), "app_version 설정이 올바르지 않습니다.");
+ assertEquals("true", dbUtil.getSetting("emotion_analysis_enabled", ""), "emotion_analysis_enabled 설정이 올바르지 않습니다.");
+ assertEquals("true", dbUtil.getSetting("backup_enabled", ""), "backup_enabled 설정이 올바르지 않습니다.");
+ assertEquals("5000", dbUtil.getSetting("max_diary_length", ""), "max_diary_length 설정이 올바르지 않습니다.");
+ }
+
+ @Test
+ @DisplayName("설정값 저장 및 조회 테스트")
+ void testSettingsOperations() {
+ String testKey = "test_setting";
+ String testValue = "test_value";
+
+ // 설정값 저장
+ assertTrue(dbUtil.setSetting(testKey, testValue), "설정값 저장이 실패했습니다.");
+
+ // 설정값 조회
+ assertEquals(testValue, dbUtil.getSetting(testKey, ""), "저장된 설정값과 조회된 값이 다릅니다.");
+
+ // 기본값 테스트
+ assertEquals("default", dbUtil.getSetting("nonexistent_key", "default"), "기본값 반환이 올바르지 않습니다.");
+ }
+
+ @Test
+ @DisplayName("데이터베이스 통계 조회")
+ void testDatabaseStats() {
+ DatabaseUtil.DatabaseStats stats = dbUtil.getDatabaseStats();
+
+ assertNotNull(stats, "데이터베이스 통계가 null입니다.");
+ assertTrue(stats.settingsCount >= 4, "기본 설정값들이 충분히 삽입되지 않았습니다.");
+ assertTrue(stats.databaseSize > 0, "데이터베이스 크기가 0입니다.");
+
+ System.out.println("데이터베이스 통계: " + stats.toString());
+ }
+
+ @Test
+ @DisplayName("샘플 일기 데이터 삽입 및 조회")
+ void testSampleDiaryOperations() {
+ assertDoesNotThrow(() -> {
+ try (Connection conn = dbUtil.getConnection();
+ Statement stmt = conn.createStatement()) {
+
+ // 샘플 일기 데이터 삽입
+ String insertSQL = """
+ INSERT INTO diary (content, emotion_summary, created_at, updated_at)
+ VALUES ('테스트 일기 내용입니다.', 'positive', '2024-06-24 10:00:00', '2024-06-24 10:00:00')
+ """;
+
+ int result = stmt.executeUpdate(insertSQL);
+ assertEquals(1, result, "일기 데이터 삽입이 실패했습니다.");
+
+ // 삽입된 데이터 조회
+ ResultSet rs = stmt.executeQuery("SELECT * FROM diary WHERE content = '테스트 일기 내용입니다.'");
+ assertTrue(rs.next(), "삽입된 일기 데이터를 찾을 수 없습니다.");
+
+ assertEquals("테스트 일기 내용입니다.", rs.getString("content"));
+ assertEquals("positive", rs.getString("emotion_summary"));
+
+ System.out.println("테스트 일기 데이터 검증 완료: ID=" + rs.getInt("id"));
+ }
+ });
+ }
+}