From f88fd5bd5fd1e573046e11ed3f791ad9306f0bdd Mon Sep 17 00:00:00 2001 From: TLSRUF Date: Tue, 14 Oct 2025 20:24:46 +0900 Subject: [PATCH] Initial commit --- mindiary.db | Bin 0 -> 106496 bytes pom.xml | 2 +- src/main/java/dao/DAOIntegrationTest.class | Bin 0 -> 12902 bytes src/main/java/dao/DAOIntegrationTest.java | 320 ++++++++++ src/main/java/dao/DiaryDAO.class | Bin 0 -> 16831 bytes src/main/java/dao/DiaryDAO.java | 530 ++++++++++++---- src/main/java/dao/DirectDAOTest.class | Bin 0 -> 11473 bytes src/main/java/dao/DirectDAOTest.java | 284 +++++++++ .../EmotionAnalysisDAO$EmotionAnalysis.class | Bin 0 -> 2348 bytes src/main/java/dao/EmotionAnalysisDAO.class | Bin 0 -> 16503 bytes src/main/java/dao/EmotionAnalysisDAO.java | 569 ++++++++++++++++++ .../java/dao/StatisticsDAO$DailyStats.class | Bin 0 -> 1523 bytes .../java/dao/StatisticsDAO$EmotionStats.class | Bin 0 -> 1543 bytes .../java/dao/StatisticsDAO$MonthlyStats.class | Bin 0 -> 1604 bytes src/main/java/dao/StatisticsDAO.class | Bin 0 -> 15649 bytes src/main/java/dao/StatisticsDAO.java | 528 ++++++++++++++++ src/main/java/dao/TagDAO$Tag.class | Bin 0 -> 1945 bytes src/main/java/dao/TagDAO.class | Bin 0 -> 14272 bytes src/main/java/dao/TagDAO.java | 531 ++++++++++++++++ src/main/java/mindiary.db | Bin 0 -> 106496 bytes src/main/java/model/Diary.class | Bin 0 -> 3233 bytes src/main/java/model/Diary.java | 117 +++- src/main/java/sql/initial_data.sql | 169 ++++++ src/main/java/sql/schema.sql | 245 ++++++++ .../java/util/DatabaseConnectionTest.java | 99 +++ src/main/java/util/DatabaseInitializer.class | Bin 0 -> 9069 bytes src/main/java/util/DatabaseInitializer.java | 239 ++++++++ .../util/DatabaseUtil$DatabaseStats.class | Bin 0 -> 865 bytes src/main/java/util/DatabaseUtil.class | Bin 0 -> 17356 bytes src/main/java/util/DatabaseUtil.java | 126 +++- src/main/java/util/SchemaTest.java | 136 +++++ .../util/SchemaValidator$ColumnInfo.class | Bin 0 -> 812 bytes .../util/SchemaValidator$SchemaInfo.class | Bin 0 -> 1084 bytes ...hemaValidator$SchemaValidationResult.class | Bin 0 -> 1252 bytes .../java/util/SchemaValidator$TableInfo.class | Bin 0 -> 1043 bytes src/main/java/util/SchemaValidator.class | Bin 0 -> 10448 bytes src/main/java/util/SchemaValidator.java | 374 ++++++++++++ .../java/util/SimpleSchemaInitializer.class | Bin 0 -> 15356 bytes .../java/util/SimpleSchemaInitializer.java | 431 +++++++++++++ src/main/resources/sql/initial_data.sql | 169 ++++++ src/main/resources/sql/schema.sql | 245 ++++++++ src/test/java/util/DatabaseUtilTest.java | 128 ++++ 42 files changed, 5104 insertions(+), 138 deletions(-) create mode 100644 mindiary.db create mode 100644 src/main/java/dao/DAOIntegrationTest.class create mode 100644 src/main/java/dao/DAOIntegrationTest.java create mode 100644 src/main/java/dao/DiaryDAO.class create mode 100644 src/main/java/dao/DirectDAOTest.class create mode 100644 src/main/java/dao/DirectDAOTest.java create mode 100644 src/main/java/dao/EmotionAnalysisDAO$EmotionAnalysis.class create mode 100644 src/main/java/dao/EmotionAnalysisDAO.class create mode 100644 src/main/java/dao/EmotionAnalysisDAO.java create mode 100644 src/main/java/dao/StatisticsDAO$DailyStats.class create mode 100644 src/main/java/dao/StatisticsDAO$EmotionStats.class create mode 100644 src/main/java/dao/StatisticsDAO$MonthlyStats.class create mode 100644 src/main/java/dao/StatisticsDAO.class create mode 100644 src/main/java/dao/StatisticsDAO.java create mode 100644 src/main/java/dao/TagDAO$Tag.class create mode 100644 src/main/java/dao/TagDAO.class create mode 100644 src/main/java/dao/TagDAO.java create mode 100644 src/main/java/mindiary.db create mode 100644 src/main/java/model/Diary.class create mode 100644 src/main/java/sql/initial_data.sql create mode 100644 src/main/java/sql/schema.sql create mode 100644 src/main/java/util/DatabaseConnectionTest.java create mode 100644 src/main/java/util/DatabaseInitializer.class create mode 100644 src/main/java/util/DatabaseInitializer.java create mode 100644 src/main/java/util/DatabaseUtil$DatabaseStats.class create mode 100644 src/main/java/util/DatabaseUtil.class create mode 100644 src/main/java/util/SchemaTest.java create mode 100644 src/main/java/util/SchemaValidator$ColumnInfo.class create mode 100644 src/main/java/util/SchemaValidator$SchemaInfo.class create mode 100644 src/main/java/util/SchemaValidator$SchemaValidationResult.class create mode 100644 src/main/java/util/SchemaValidator$TableInfo.class create mode 100644 src/main/java/util/SchemaValidator.class create mode 100644 src/main/java/util/SchemaValidator.java create mode 100644 src/main/java/util/SimpleSchemaInitializer.class create mode 100644 src/main/java/util/SimpleSchemaInitializer.java create mode 100644 src/main/resources/sql/initial_data.sql create mode 100644 src/main/resources/sql/schema.sql create mode 100644 src/test/java/util/DatabaseUtilTest.java diff --git a/mindiary.db b/mindiary.db new file mode 100644 index 0000000000000000000000000000000000000000..38db2f865ee544f31c5b6da0a00d2d86225680d5 GIT binary patch literal 106496 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..47e30176d1252314298c4869b16e5b49484edebd GIT binary patch literal 12902 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 HP5J)|>-e^@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d1b7399bf01e5e2bb4a2affeb7aa4bb9569f1281 GIT binary patch literal 16831 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 Uycz!<=0|bo!_`*a21m*Nf2jWyF#rGn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a072165f435095efa73a72f7c91b594f98d46ab3 GIT binary patch literal 11473 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 X>j=KY3)fELE7}BKiSzILYxMmG=ZNN^ literal 0 HcmV?d00001 diff --git a/src/main/java/dao/EmotionAnalysisDAO.class b/src/main/java/dao/EmotionAnalysisDAO.class new file mode 100644 index 0000000000000000000000000000000000000000..12964b5a4ac5b4c18282cb160506853df34f76db GIT binary patch literal 16503 zcmd^G34B!5x&OY&EH{%2#7q(xP#o9nqNu20v4I4MX3->|SS=38B@8B+n3(`UabK#{ z7H!;6L8%%S>RJO4m8vZ+wVQUa`m9!;b?IySY;CL7n)m8%;)+Mia(D+%V;J8p-;eL^9gbZZH*$C?IFco)1cOF0V>vM zB28jCF#90;XN=1f!~$z$(Y}HD4)s#?GSFn5rqG^Dg;wxhvMn_~>yf2c7 zuEg5%sEQ5>&@`P6rbDo_%psY4MqghX(-oDDeX+7d8Xd}XX?;^`T}xY}zNxL*o^?rT z*3u4<-jY~HW$IZHvZ{=<$9tAxIoS9miS~FOYECx>R>k`|5|ybnFX=Xt%j4+U-e*LU zM#qw9vMji$reR@SYozq(%1GM3vY-I&aGhq*5lrK-H}m2>J%A;E!Zgv|n#37h)0_u0 z19TLGsWwfXv?ei6x6zZ#qnR`-K(lq4Lq{`(QZ1am5Lj=Y(HreEQoWcC$s(wF3l%cK z@n(;XR2Sf*hUNxno=&w?hlQ${lCf?B``RAug2EVWxC0h?;;WdZ*e^1+iBgqgbXq|5 zSX*0tV_j=oP2=%P<~O%A*0eG0Z|+8!FN!NusEus^da`DL4mg3U- zY*-Yi0msO-zjnzZ!LdN&5P`j3q*S9WrT5rxn6P%fle0+>*#1Y&(!alq(&Dr+4P>`0ii1pSE9eY z-AE*s^>=j*L{`O;%Omx*)sZvT%EVvQ=~B836U%F1VzDJ-UILP;qRVx_e0>)V9iqeMNGvqVNPS)rirrTQU8tUe?MZ_Ha ziw#q7eoJ$sy%#C_7d&x6T}xeRuE^}j(ZS}H+Pap=+{G@d7OAakofmAVZ>(>N91;{v z-6w|Uex`%7Y@oFX+0BVi$b&k4M<_(M$mN87qi;aOIjqwb+RC(7ceFP}4sG!6dn03TXz z%Q}U7<=CiQwRk}k;R#WMCz+0N&omZv*4!>&+N%K!AExi?w1b|4;v1%0mLlTJRX)RX z_P+^Z@c9H?8vOur{S-+p9k4{C(GQt!`8TcDa#5FvL%3_r2+)shf>jc)>FbLQG{h3g zApMAP^5}VbAwWOV>F4wdxaEquR6A(`#|g}v;lkRx3tD&)EZJhf1vkXToVhjwS4KkN zO4ca0M&^1+rhlpnP! z+6Twv<22SWrx`O+YNk(^%4%VYA*rl(Gval)9<=8HRY=Opb1S@N;HSCNw+~!DQyod{lSr=u|O?tl|Q>VofZqBTX%xhlQ)K*$v z7KtVhQ1|yFcjAQ{)6%@~cyT%0-Cd4|bT!pDz`E`JD+XN0*VV(?nf%P|4 z9+{jhcHQlyTGXI1A~T;7k&HAIK@jhqVH2#TW%YTy5APe`{dC@+5l@YC> z^&vV>XQYW_(`=iNHJenc@!?E+)~rOt+iAG+LlKo95@~3B1a>_A?GYDbZLcvBJ9lQ) zn%xw|PISp+rd^35_$lg^E+kX$G@i*c!$C!s*MjTd#)<Mnv(AD`fgdG!I*=$S3u;^g@}I9v zN;cYz^+dX&iKKvEdT2b4>G1TIv&CRrMw9!LpO)UWj_J`aih~9DxMRQF^QtsH25;}c z0zz3H>s;>E4pRuQ8>PwR$So|7cXbFf$}~n6Fhf~()>mTq9E}?gmc=?WZh{|S+d4er z<8?lPTad#-s;1V?hD+W^5@Ot@^Fm4FjK|YDld{|3Eo+R#U4EvX2qi4m`DB&mUKLF= zA(v#p;)xoyLh*!1Vcp{dJt;BmFvetTx&Zv0@ixB8B`jdUp zE{%~Y_jSis8y!gJb5iGi2}u0L8U4|&1QIN~O6S##ynbGX%0DVkO=bCOgM22R72vfx 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..37947bd1c3629ddd4a21477562b45cd906d49bee GIT binary patch literal 1523 zcmZuxYflqF6g^WOTgvh(FO`R&AZ6Yhq z506z*3$(5|uA?^uno{ZgHpJ0pAz`B(w!m;ndUFLWwG(Kk7;J6sPHai1a&C?Sfp|c> zCAAVrZr7O$+IQS?)^K&&=t8$ZXUUPiqk^2T2rmhUya^|5^qO#&_Oz_zK1yE}wekZK z>$fprV%_S*(~jo`o35;!e^MnAyl3OS3C6W72k!$_iepG%v;MfxCFWV5nhd69#%j6O zCrdM9Mdk>q-ZgD?V`%7nNniw{79QFd!?-|Cy{NpaRayB0)|srFs@)@jfmA--P<9QG zP2e#mEKJ&Xf~i|AziJFye&qQllD%I^HQcYY?L))Sl}6ddEH#usbIvQV@m<@Ft9DLL z4wb(r4=X%rFDq>Xx)P=@6?wZoXaw~~yd%BO`bo7FQjTMiXN32|k!JRTmy*1+cUw;$ z5`X8gm&i9Ebp?6}y?bh^+N+7p8V%jeF0Q@n)+P$xsb5rY9n*=VX~WIv&KfZ}GCL~J z@4P@;SZ5t~CS1VIyRPzc6&VDIQ=X_#6~;MxF@9MsT=|92ir{Q>J+2%08n22X3yQNF^wsx>ox0V_|GoCK9dk_n2jktK6+{AETG8+BIy@CYUacHM~OGe*yC*q5uE@ literal 0 HcmV?d00001 diff --git a/src/main/java/dao/StatisticsDAO$EmotionStats.class b/src/main/java/dao/StatisticsDAO$EmotionStats.class new file mode 100644 index 0000000000000000000000000000000000000000..109d1fa98d873e3379eed7115dcdc304970a57b5 GIT binary patch literal 1543 zcmZux?M@Rx6g^W)x2?-Zfg*|^DwP(Hir-kOVq2sMfgd#_#Q0+=+p^g1#@%fVF}{(B znm{Bb`T#zZ@l4r5*)~n)&fGb3?%i|m?caaD{s72gBZ>g}6a-cDBP1|r0^`YIs^jZCL?((^Boy40 z9CMcx&RdPWnl2De<~tvrx%n=Ic?An97IE*=yEENbnTloCHLA6e?1<~B`mV!uR?19g z5Dz5~%vt-)VQAAZ^{qyIPq*J{t*l~I-E9ljuHIO(Cr8ZbhQ@p3l-dyplQwr&WHwZh z4nFoAK`KbdC?RWI6(>5+JEFVqf>e+X2l#OPyc2jPxbv*?ezV;Y0xGXNj$zkoD;q6Y z4ZEzb8?rnjml}LY_5icq(j9lha$E%kHb&PRkiBS{x}9SSIyxsoZEGn9gRKze;DJLI zkdwq+Kn_65vgKQro!zo*Wm*-S4urTa@iav%X#3}XLj2%HAk9@?1Mj)s$1>N;&|@gy zeNdgC2YBc~rw{TA?gtx_n;m4>Z3g%=n0;7(N3OVz)x!s^$aWLv;pqq zhaDsK%}4uzSI?30pwsB%Pi8xJf|#GSM$~Fg5#oLkOhU80MTq-pEn;8vq@D27ekAl| zZ`uhztxfE!bJ~Je$i&EG=r;npeH23}?FMNEPBB&5ZK-pLJEc=3FTxJC literal 0 HcmV?d00001 diff --git a/src/main/java/dao/StatisticsDAO$MonthlyStats.class b/src/main/java/dao/StatisticsDAO$MonthlyStats.class new file mode 100644 index 0000000000000000000000000000000000000000..8f9c0834881b1f134fe8e98df5a095bed03273bc GIT binary patch literal 1604 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 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 0000000000000000000000000000000000000000..e2304455bd70a57c19ef44ce1334876748526040 GIT binary patch literal 1945 zcma)7+fv(B6kW$Q$Tl(%2&4%OrR8P|f^$y@kj60~aSJ$&9cGw5bcC&Vlrd6AlGBGy z|ELdbCvKU}v>(uq>U14Rj%39TebGLbb=FyD@3rlJ{{8cB0DCyeBZahrjEXUg3rxS& z-|6M9ZgtA_v$sae6Byq$Ez^4}kS>-^a>yd5Ag^Kqs=#Dhx64hvvtOyNFbHHU{oD|k zIgEH3o?}{_9eHw6#gsfY*0Q^{BUdh|n2{?JZNqIj=J%dyTX|f@oPv23SMZ6z6`#s8 z&yDh--O{`Jx@R;wqae%vL13X+3U2!t!b`7VLBXdgu47RkAIarYo3*27jb`QO<#z}5 zS4Wkmz)FnhjqRN4UU_iafj{*OXGh9gQt>&yU~U4Gz{BGIIT{M}(;G|6ii%a4gj~xp z__%h(lc`x(QACNNd#>Iws&>y}F+@#nZK&A94S|%|7MP9Yx@LJshZ?ge3v93=Kbq~m z)%NC+EX&^ds=L0q->(s9;;>=FyOqg0~86hgpgz;M^h@h9Mk@Q_>}{JU4G>@*bx%C$FGE*AdL*; znuKZ_eJI*y9|i43AJf_eW`D!g-|<-=%UZgRwTNpG3QuV~yMY|VPEzhHwy=aUJMkvj zALGmLj!4dXXut4ST+SWNo}dy!7x`j815%0>T^C5dOVm?TLu{N@Nz&dYXdll;(JD#W z2Sgo=q%9O&}7V$Hp_pi*U0?`;+A#J literal 0 HcmV?d00001 diff --git a/src/main/java/dao/TagDAO.class b/src/main/java/dao/TagDAO.class new file mode 100644 index 0000000000000000000000000000000000000000..32904a25fb62a50876252e2f8f1f13eaa823a8e6 GIT binary patch literal 14272 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..38db2f865ee544f31c5b6da0a00d2d86225680d5 GIT binary patch literal 106496 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 literal 0 HcmV?d00001 diff --git a/src/main/java/model/Diary.class b/src/main/java/model/Diary.class new file mode 100644 index 0000000000000000000000000000000000000000..79452ed75fae533281256cbb4a4426cc5146e1db GIT binary patch literal 3233 zcmbVOZBr9h6n<`A*syMp7mb2d0kuE~pjNGlC{#hCM9@TPv}%`RfnZ)R*=W)Fr9Z>z z*VZq7=!;UOGwt+~o#}7t)IN7#k_|+s(+uq0d-plddG0yqp54Fx`So`I6L_Y=2fu=V zh9Fu5+E?|*`dD5sE|1MEt{NFrpygbym^04{_y>m;S`k940#(BfXaYM5rL2)3yOh(* z>jFxqR5XnuZ8~S`JV~>hD=v>q%Qg)=rCFO%D4DrZF5C%v`}3n=NJZ z{3TMQIOCRzr6&UW28V2t?GQ_LpN4)MVsKLN#B?$>m6(4pGj}sFl@d7YFsk(~)yT!^aq4G;Nf?>B0YE>IkzE;sjD;hT|GO!3pLvSD7xXnanEFen;}2((oxxa|d~&xNNRi58@%5 z6}V>Q|5+|OaU>h-b&6nO;7Db3baWur>m*w)a8}E&Gp}XVnm<=@4igH_Yq)?(R!Y5& z61wHIkShuV2jlUfxK#C-hKsnwx~WtbD^{fmMC4OXSvY|UVTSuJR`Ypjcts{8E^x-l zgEtU&J;wGi7;nNSkIpp>vq%VpmJRb_UawSGkPV@=7Fm#Um{)K^!%ZXwI_kS}N|s?Y z9#!=`?RR?xx!#$9Io#5)ATtnJ(JLz#dGbQIO@HhBjU*By*=+BXD`yLZvz3a9JGiUh z3k~;hU!cE!0Nz)mj9_VLUN>2iF;C1jpDM4mB&c3s|ATSQHks^yF^lan=}TAgE5 zmM8ky*L-{_wV+3~Ti*F(o3}>3Y^;%Q4{PMB!W#K<;1~hpMmvEAobBf*I0mD?K>WxX zfiHQLkAW^;gV1@E&}q5|aO{@Q==Y;>ux*J>#$^ z9l?iOc3bc!dVWIh1`c0uCf#;0KnH_#FhmDY${uE*VstY?>MRV$K|Oe{5&40-yumxh z*%FR6fht>}Cka|^3Jq_Co*{IlDYShnbb`=aQ)t&#=oF!=c*v_vbU))})s{?hq@GpT z!hsp)?=q2j6dZii!K-#rY+$f%(*{3nuF~cj!#PWfVspE2w29pu?dEBBgLb8wU4y>% zx-VN_iZ-{1Uu%x{RH==>O?;X=;3qPjlEVguH*o9)l)y^_{IB@b=Q&`DD03wU-`+`STES^vWeQGlv`(N+6}G8PHse@w zIXWh#joDOA*p?Iiu$)zw%9c~D709 zF!d6_&go=2$i;9n-NKR1uMJ0pH+%FW tvqvAPX4(734Y|x-2w&BTu#a5w*V_5{CelBnw_fLVb5;phC%}iN{{p~fi#`AV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2cab73ddd1744ea5921ee9523ff6d04c6e46e502 GIT binary patch literal 9069 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 KcwBr2p8o-G*h`uK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..765a7c80c827b610b290f3e4b71d85affaedc88c GIT binary patch literal 865 zcmZuvZBNrs6n<{^(yf-k0Pz(80qw@z1oeZN^MfBOFB#wQ>ZX3#nKzKvWolUtYuD8p21KJK6HcFCpN5SUYw}J%etVSi~&` zbC|7w<|09&w_5lpd=++2S;dA>R###BsXs`C=zNc0{ zg)FA7l4!a?qG{ozHA3i%lB|=rNScu@)W3s$P0}_Tr$V@%rzd9p4iR)MUmiB%-)#u!WX7;9h7>Q<1Wx*IUH2AZUEXcDHOh5L!#!~@a> Lbu_SvhbaFAK+43P literal 0 HcmV?d00001 diff --git a/src/main/java/util/DatabaseUtil.class b/src/main/java/util/DatabaseUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..791fa9c3e2f1c1f98338f7431280fdfedeabd881 GIT binary patch literal 17356 zcmc&*3w&H?G(Xyymfr1ujd8AFdv>YB9yDkIZRCoq*w?j(IGOqwZu0@5WGj>oQ!O7Sd{W=rv`crqMu zmb)gE9Eqkfv1nSF1Wh_gnv|rYnb8rajjVRANhi}OSVug$X;U;M>rJLMEl9@)&ly_K zuF94eWHFuT^b$c>*%d^r^yt7qG@TwCjmLKs)9G}EpXQl#CY^nxnzp;FEFW*mN1!-joDggQm=1PFW{z(mYH-RT?8O*ie_5k z;dC1Es;yP#EfR%pFzIr-0^o~m>`j3$a;@L)X;44YJQqY+ zaayuBz(H{(`J)mGM+ZFeEDJ71My@!>ZdJE14Ub|i3DhfhD}Nk47em5iG)T+Mp7{S zXpTzqdnlv2${9_j>&mevfcNZ{v zU2ZpN2VD(aLQek|UD_0F(6vlOz)>U>+Zs(7^br^l$ZWnw()?A+n_CRJ9!>&nhQr%i zl8FSU9W(&Gy@5XNryEVWiEhqgat#0`|JHDPG}<*NEZCN7yDma*1#w_tD9|3Y2Q-u% z%?Kj?!lc_pgTVn9QYPvEsco<`nQ7e~OJ@}0-(k|73OBk{k1$+1+eQZZq)DGr1Hq@k zRV>%=k@3(j8U+;n^l8VevF`<;OWSjlcMq1(Dimu$2yan%9B zMiyw1L>jGYt`jVMUYyk)rmFWhd>a`xYm4b2v80C`ROe4^&|W}gffS|(Hb;lU0vQqa zVQ?%8}t-3r&eLR>i!56D{C?6878YuhY(674)Ns9VbB17&p zh-t3gO?JuoE^#NFBOYm$L4U>RDZ=5RVTppo5_USanTo_o=xzoxIit-?YgP4Rg1j!YC>4)?$0IZfM_MV9jM-yl=D|cA8T&?=YCjEr| z6{8?*gD0Qqv{ck@bC?L^-%a`t!Kgo$ZXF)U>@es*nLG^*QunG!Kc`=QaBpUQKQz+Eh-rCaRFkZhRa%)9*R|kAlG}Ui2VcJ@^vbDR_ zZMG%4!&+ulX?y9mK3vx z3;gUg*~f)WI6G}q7>F&1OGl$Agg$vXN63ay1Dt{p4KDI?z~o|4XI24j6JQ|`-JZeJ zw8`L7=IWO2)~23TtEZ{Cz13=4VRd%(Sgn_}g?d7k3c;-EfQ3J?h}G8F(|Sp3x3#9b zt)r>C&syEuXEpWqbhUM2jE>gMo_f_1zAyt|T0O0o^{CN0d)wQsmX)n7tE=PD#HP&V zYP(5|b)mJeM)!&iC&elEr$>i}5%{Uz_IS#BiUuYPxx95nQ*V2ZRSm+7O88%0l}K)@ zs<*1*$$@ZON~&t?;YUZL(Vsj>peA5&8S~}uBdvT~{jpNB0ogAIRFdoMY`e6#)fJ$Y zh0{vIS=!~7XLgK4b$3TNRcUo(tJ3WvQO)IY3d;7j-R%Frgv*)dy^n;&&y0@r;{g0e zgVOFg63%SSN0F9`U7f~3-G#X$!G(EErjhV)I58TI+Y$P!eW8BI@8WT>~Lr8N|) zQdpdc$Uu2j3rPQ!Xe2f!j`Vy&YtSpnh(l>7a4pa)5f;WP-}OO z$fRo$aaKE61Y+x50#(4QcUA*LAx>~juf|&2)ZW_~vZ^ntx9q<)0X~W6_<63$Co|F; zvz5~j+eK0XR*X<9Dj^f1B^Wm%3de^{Z6Zk>woE>aPX|Hwv~{$GdYU@c^sngZ?r7>k zkfh02bsS8lhQk@j8L81%sK-n+wM3RP&*U>10brqSl|5;`f7fXa2!56{$ux0+!AO%7 zhet;G5$vX6jt18wx>z)S;rxXL&qqk^5F$eC_>Oce-5*VerXvO~R1SSKYA})ytiWd1bT?hn z(PTm1(b%ShkYE~qxU19Pl{mcEwL?fuKp6eQ$w(Bn>zdjPUIj$h)#)9H0i=i$Nm$M@ z@FtXw2JP9=2f_&C`<0C^vlc-Z26w{k+T9UOjr6B8SOI!>w03lL_Zhq2x=|zeK=-|l-3)(7GWk(rZSUq&bRwmh;uV;BFe*;ncOFlTKT4E zB8ogfv{`Z88t_#)5pOX0az?gd_HcMhv?ZAs2xlY^!yTw1kwNHt)_X@E`O%(-`%S)5 z;*ni-0?ln!s?~Hf9F8S)-IBn2^vw6J(~pqX<~>KUS0i3z`eCvSmIMUm z$i%!s`w_|WkjYy(4$DF3W~Cqr#ikk#Vs?WQxd%l(ro)SQheU~H4IYtGI5g3)QB+}z z(-k|?elvF9Pe(^2(!X-$@NhaE;0%xYd8^6Wcsooj%bV}TZCH155QqZJVQtf8&uj*J zf0`>N<_!j4!_=4S9kNohs_lF6w)GA&#P0GPgZ6UMIt3B?btZp=KMIoqd9)`Jn?R4% zZL37GA48$-4GsP{m}x~*TYKwrgKq*lvnOHEuq}58f4~R)lQR4Es>QYDRfd+k^`~_Y(T9ZWaj7H zpf#2Cc8H=EFB{xkshn{D&|PF2`~dW(b}R<;S(87<55hx%N>x6fJGl*cMe!<5_J7jh zIA?kGfwiWsh9lb`1oL^aWkqcE-NX|R$TwzxSpgI`1vDGQA}pQyG_4pD7DDu9p6!S<`u zk8LkX1m>Pi<@hS6(yt*@vz_t?(u@5A0+1y*cB&Xb6$zOvG5G7Sjof0@(%^3ZV7bN8 z+J`rX_~Rvmzs(N;wOy8-aFClfRFb zV0@3k|I9RI(`i??)!n+Ly{QG~@jp-89mtQnTe_OsTSG0a)z!8bvXu9Mj|Vk~;MG4I zjFFbgpfcu6Z?dNxq9Nds= z?>5(BKN6^7k9ORO0=)-^woqVuhpxDKr>)7|sm7!dot`n>YV+L%)*cYU(F85|#+-*g zknwjs!+N9v9em(+=|EgkcsRQRLh7Za1X!R9XK{kQ`NEhGZYQ?c5fyo1xn<4CWG0 z_8KSmdF{z0vKmXKI=Y;aypBCc@=Vh_SA7m$PaUEK$$=1Fu`V$@GXV;XRtu;5Dy}j- zNEpnV1lE2ATn!JB0kft}B``c&8{m;r<|Nts5=XvD`F_t_e3LLmSkuYC?*YtDdH<$} zvb4bHG^Ezc#Axuv1ip;G-AVi!U4iKIb=-TX2hYFCKJy!RMruH{`!zr0wBz5P{I@yJ z|BVsxS&6ih??cpBc=xZacx9~1`G5FLjPMqouEAU$Tx*{q{wkgE6#4sBe_7p^93fBN zK`MTlxc0@pWVF}5Sa%FH`_bOSicdqY(`cT$diZy0WSRF&%zhSHR8cuq(_E^>Q$7D4 zH8ZH-ZJO(+vgLjn`W?KPM?Oxm7gB?~G^0^b?LnGRJ4WRP@W5)^QW6q58y!AG2AzX$ z_&&w%wp9&7)POFP_;=z{G^hO3aXQUId+{bko@(mGsjh&I(1O15MRf=1L&pxNnaZgW zi_}X$9Rm}@gKdv7IiUYi!MUCr|)g;4gSXsi(wN&qKND;kU6o zFZ6-^%3FPV;dk3&YpyMSWQ-maWPV8@6XJV(H&xV@AFzv`&NI7b9f|JjD?bQ{j@xEe z1x9epu2wPB4PcCqLy9-UrfvZn-irD=(du@p1Hcy39Wchb)MgASr~-q}W8w0_> z82zZRI9NPJ|8_sk4i=aHbc}v>Ka~UvgT>?YOAoEPv55Chd|k#uL?!F_E!G3I2Mdo= zZLn~h{znvVVFnb;atq8xBj^tru=JoGral9!zz}aDEH6A3^hsAdD$H0?C0(5YQ^pGV zrE8&V*^(^-Tb;Ns*Z0MZ-iDwTEgGO2e9}aZ(m`PAD1C)q!gUqBOy2<*n(1HZzwrDP z2)Y2*BAUx5;d&;|kSm|cXYg5g)>zyM1rptZR`-J4?}Ih&0<#6+VZCty07`D9*_@75F>dUN{drhX8jLUAFw>=aU#FD-13 zvI9@zOShK)zFpwhQ4hGh z(zg#9K2DXoVlSD1wKwP!On8q85N7IzdnpGB9o~HeA5ArI0z+^D?bL;<0k^OL*NgEh z*DJBR7_J3ymT9d2V*JWggFxH$A=Dm5&m$P^IRN1(#(W;_kD~Qqw0<5+|FTltZB#q~ zBlbB`ddSZnKhD#1F+^0%MIu&Lgi>kqSm~rLSdda#o*J^GmotRj(mw&d^XO@%t5Dwy z4)yUTKqnGH?Jax`R8dgDC80iV1;u=7^26L=;x zd+0MCo@CQc}&1Zk!rJdf~J-r+)Wkb;2Z?*ZYrK^gDDcTCWUF!UiVq8mAYQHp6d zoA`pRln#RVpXW0AI-fw_<#PH7SKyV>0&4+gp*?_eO>67Blz!_OqYR&Dz=iA#Q@PB&%HsXS=wSAOC<&Vz&v3oE{ZCwFIgIO5}FKg(0fZ`&A ztZle{7|!)ET=(MyaTwQ9`aZpi>l^eYyo~V8PgT74D&lAr_o-Bq@ z3ey{;(|8lr@Mbz6&%L~bKFo1^x<5>}aDwi}{r!9uJ%;k*oPk%`3a_*cAH;6Q9NXws z9>5$s@Q&qbo{2ZlC-JokbT3oc1eUCVDI0VOUv6|;M($Aq6?UBqF|a#4WS$dKP23~S znFy&IKQm<@w1N<@b9KIX8`dEZ%mum(W}`MA=z12rv-I+4Cnk-BzVIoCopD(xm5}sI zgku$a)gewJA`^1iA;bevE1Bc%ByMj+TyPV5;}Zq@5cCqrQnGT#D)?#_^28P5B|Bksi=al-%EuRe1mHwDsuXxj6@6ymZH#T3eEbfmEU81|VYs>#%{7?VJl62VIkf!G_S@Jvbx(=3&0j*6J6rS#dNERPfEV zgRz+moaUV~&bK0BM!X9pE%qrvexlNMi0_<(f8+dVFI>%H1jO1238UpWp~4&|qdfVC(7?oSM7v! z{PG}FxQB}P3slYz(;R+;=JBK8kjH2#e~G$r?M3|^xWAM4fkPfwYP(gO&PpZutk@+g zb$wB`IL|<{#jcVZKV((37Vu_N_IkT9uE?-A{~VR|71TY$pNB?q zsKVV|b?3v~K6O`!yQdHFcxarD7Dz(l51)PaQknJ)U)G+XZa@5H3C!&y*xtwKHU#vN z#JCs7U?;@7P@Nl}1l`Yaz!WOhq zr&;_1{O80U(gpl4_)n`pMo{t-9EN_3|5o{m5_=~Z?^0ck*xx}MqvVex1LL)TlT_0( zbi!KaIBA!ACHpPSZ6kgTZ6f^f?JFwXxdZ|c1DKqy?Y z!ZS$S#WOr?(7!(i&nkQIUU;7Bk8NlC-8%6puXw3`KkzyeM+-?}ZUmm=xCVgj?YM5h zuUtQhO!RHIdhv$q9`MBm{K{1$UM0`?XHdh>;huj1_xu{%^XqWWZ_vg3Yq|{A4XC>Z z_xIri?ZbFMD_n6vlAOyNkLo}^o9aXf>;k_zmI0g4e{+(au2asfbG0N|)s~U%JIOM# z6?wh9I*ik!N*8eeK2M?6v!?>79ntg475sxKdUhi((Uf~0@zvWgSD|x4k!+~SiTMoo zPWk%JI+^VEgh8c0m)8C?82k{4TyZ{dJM~ES1->vH3yMb-D7Mivkj>Tjm8%AYj@jNJug60HIL(ReSuj$YW5Wr-+t& z0@Ud-sozsd+i<-aZJ)vYm}drk+f$}M^0x@L6!gp&b$knz!8NCJSD(;h5D^jzq37(k}StUccBNqr|``2oa{N3g8vKO Cf`**{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5d22ce3c3529667af6b92ccc412bd8e06cb62892 GIT binary patch literal 812 zcmZuvU2hUW6g|TN3+vhs#H!U=szny0MITMY2T6>Lgg&fkLVX%ws9R@u$bK{-#{Z;= ziH(W=0DqM64r~QYU+%eg=H#5&bNAQp?>_;w@zg>FhKa0=9P$inXZ)PEBo9toM}spl zj2ZIJy}*lKFc?n#xPSsftD|J%2Ok6Oi*~IV)tZ%fI=O5E755S;+tsL9nS3DlRD^|V zCTttmvBI#O#GY)q!}r4H$6R_N9;1lGnBfE*SK-$1*ZpEYR$&F*2!v9 zeQ?7@88;cs)S7{vSh$7TChpkS#9fBO$mWlv$i^!4Ic4|ES&~gZ;oZ{FJmZ69^OVJ4kHnZK zGNyZs4@CHu4tsl!C!+YZ zieeM{M7nB4^rXWe5Mf91C=wB6ve+4RXb&0srE*|;%hXZmDbjLgb2XhQP?|Hupns0) zBbv4d8DXyR4eZN|UZbk7!91!C>QptgJ2L^ACDZ(YOy6jHMbYhN30B>Hj-Ygb4Y!}C p?w;;0u>JY3;tC8>46(@~ID`fobeF+nG-tSn7NJF67WVK2#Xn_Bw0Hmj literal 0 HcmV?d00001 diff --git a/src/main/java/util/SchemaValidator$SchemaInfo.class b/src/main/java/util/SchemaValidator$SchemaInfo.class new file mode 100644 index 0000000000000000000000000000000000000000..f309e67940d8dca1650b77831e5ac8097633ffa0 GIT binary patch literal 1084 zcmZuwTTc@~6#h;x3@rY!JE-t%fcglmtCx48u znwW?t`sjl{N<6bIZ81%D=j@)#cfNC(KYxGx0U^p6}z*vya*}Vse4j40;XQNSny^SM9Y7t=PX{D(U~C^w~O3| zl3!!6%#P!!-Nx~O3L-n9U;F3O$MQYN4BA>&-*B&^2!T6<(H?~m(zf3Ss_Kd3a;CBW zc4Nh~o73&8P(SrUEwIi`y_gfMvgfIwzp3xA`=#A34mp zu4YBwPsH})i(h$w`w12qdq2q{`x_Y}zGIy535obAZrG=|`I)0)Ft|fZ5cGoWHda`! zux<&lGv-BtOGpUJ3KaOEGd4xKWJkvfj=K@qD(*#c4fk1Q89t3QJU|K$Sw2GQA0Ija AkN^Mx literal 0 HcmV?d00001 diff --git a/src/main/java/util/SchemaValidator$SchemaValidationResult.class b/src/main/java/util/SchemaValidator$SchemaValidationResult.class new file mode 100644 index 0000000000000000000000000000000000000000..018189c4e655dd1d43933cb5bd1d5b9d7b267eb4 GIT binary patch literal 1252 zcmZux>rN9v6#k|y-F8`^T*SKqf|gRL7rbBr!GcOcAhCuB{u;_qmbANZb`~YZchW=y z(dYyCP{uQ}wQLQ4_Iz{hXTCH0`_K1Z0Oqk`BY~ual!Gp$8FB~wke3?VtCu!*4n$2c zq~~1ERSOKs{N$#E48x#t3YFU^ElJ6b*4#kZu#n868;*q@2fgTHm@+Qa+P-M=P2O;K zx$@0NFDgD z$FP`>QkXd^`&?GVyHI#FQI6?Xm5_X=AZ{cJKlE7qpUMeIJl`ZcSk*%i`V+RgGsG-S`D`iQx1yS(DSeR zwq@EuNn30)N?U!`!9A_c#pW_bP0);k2Y5(y?)kFGiPzaU8Lj($8@uvo&(30jAzO9p z9#^3h4C7IZ*(tpibFmz)vE+wm=>5`4?;|nX(W+nBynXVF=HfAVI(~{@_s}(fyYP^rWZ}$S<)2E z8ORlm!3sZMZ5<=~4fYok`GmguKL9p^6f09q8FQ`3G14<_AIKHH!al*k=XQ+F4z@tw z`L=(Zs%rli0~d1XpGa&aDeTe-u2r{EgvJf(B6Q23G@*h)7NOe)WeDBT=mhsqi}lh7 fU={SRWmHHeu|Q=Kc#1_Mmrx;TQ}J#rVHw%KBl9Sp literal 0 HcmV?d00001 diff --git a/src/main/java/util/SchemaValidator$TableInfo.class b/src/main/java/util/SchemaValidator$TableInfo.class new file mode 100644 index 0000000000000000000000000000000000000000..f154c8d63f5b927b7bcd10572417ec54f367245e GIT binary patch literal 1043 zcmZuwOHUI~6#hfm(7%mWjRTmOn% zO-w`+UApi`iRTWL7Sd$qo;&CHo$s9a^Y_Ou099TbLK zoWN#aR8J#|3j(tSF5(hldJtK+ebrSiK^#geP&@mC*eS5WJ@Xx-XO0+E4tarF1_~&iq4cC5rgYzNdy-q& zC=Ma&cl&Zkbuxp>Fz&t=A=Nayw)6v+%dVY+yV)4Yb}gKarn|CLVazqB!@-jc(^lI- zuch2@989wB{D+z?jx0Xj^kw_?Gx;Wr5V%X283CM-ZaRV6R!>ZeD`n4(e^s-{<+hdQ zKXp7`-~l_0f=)2%wyoTnB|T4h-2Lh12s|b%j0(yP`eh)44<4ajdmswZ%gHxh9%Mqv zi@;wczDKacFXXf+S|B#T-kNS+|VHF&jy6 s7cqgHz#V?*m`$NBKG5+(;$E0+4fjL2j)yEWoIZ_pJVFW^EGtO;16)AyWdHyG literal 0 HcmV?d00001 diff --git a/src/main/java/util/SchemaValidator.class b/src/main/java/util/SchemaValidator.class new file mode 100644 index 0000000000000000000000000000000000000000..ae1a7d9fcbad2aa2e36a833d0c7ab21db03abc1f GIT binary patch literal 10448 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 I+=amZ11M<}(*OVf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c9c7d4c6fadbcf21640ceddd85500f289419ab5c GIT binary patch literal 15356 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 C-E{Q; literal 0 HcmV?d00001 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")); + } + }); + } +}