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")); + } + }); + } +}