/** * DatabaseImpl: 실제 데이터베이스 작업의 실행자 * 모든 Repository(계정, 게시물, 통계 등)를 총괄 관리합니다. */ class DatabaseImpl implements Database { groups : GroupRepo ; accounts : AccountRepo ; posts : PostRepo ; dailyStats : DailyStatRepo ; settings : SettingsRepo ; constructor ( private driver : Driver ) { this.settings = new SettingsRepo ( driver ); this.groups = new GroupRepo ( driver ); this.accounts = new AccountRepo ( driver ); this.posts = new PostRepo ( driver , this.settings ); this.dailyStats = new DailyStatRepo ( driver ); } /** 데이터베이스 연결을 안전하게 종료합니다. */ close (): void { this.driver . close (); } } /** * 데이터베이스를 열고 초기 설정을 수행하는 진입점. * 앱이 시작될 때 스키마를 적용하고 기본 그룹을 생성합니다. */ export function openDatabase ( path : string , driverFactory : DriverFactory = openNodeSqlite ): Database { const driver = driverFactory ( path ); driver . exec ( SCHEMA ); // 1. 테이블 생성 migrate ( driver ); // 2. 버전 업데이트(마이그레이션) // 3. '미분류' 그룹이 없으면 기본 생성 driver . prepare ( 'INSERT OR IGNORE INTO groups(name) VALUES (?)' ). run ( DEFAULT_GROUP ); return new DatabaseImpl ( driver ); } /** * 마이그레이션(이주) 로직: * 이전 버전의 DB를 사용하는 사용자들을 위해, 새로운 컬럼이 없으면 자동으로 추가합니다. */ function migrate ( driver : Driver ): void { // 1. daily_stats 테이블에 통계 관련 컬럼들이 있는지 확인 후 추가 const cols = new Set ( driver . prepare ( 'PRAGMA table_info(daily_stats)' ). all < { name : string } > (). map (( r ) => r . name ), ); for ( const c of [ 'like_count' , 'like_delta' , 'view_count' , 'view_delta' ]) { if ( ! cols . has ( c )) driver . exec ( `ALTER TABLE daily_stats ADD COLUMN ${ c } INTEGER` ); } // 2. 게시물(posts) 테이블에 '알림 시간' 컬럼 추가 const postCols = new Set ( driver . prepare ( 'PRAGMA table_info(posts)' ). all < { name : string } > (). map (( r ) => r . name ), ); if ( ! postCols . has ( 'notified_at' )) driver . exec ( 'ALTER TABLE posts ADD COLUMN notified_at TEXT' ); // 3. 계정(accounts) 테이블에 수집 상태 및 실패 횟수 등 관리용 컬럼들 대거 추가 const acc = new Set ( driver . prepare ( 'PRAGMA table_info(accounts)' ). all < { name : string } > (). map (( r ) => r . name ), ); const addAcc : Array < [ string , string ] > = [ [ 'backfill_state' , "TEXT NOT NULL DEFAULT 'pending'" ], [ 'last_refresh_at' , 'TEXT' ], [ 'last_success_at' , 'TEXT' ], [ 'consecutive_failures' , 'INTEGER NOT NULL DEFAULT 0' ], [ 'last_error' , 'TEXT' ], [ 'priority' , 'INTEGER NOT NULL DEFAULT 0' ], ]; for ( const [ name , def ] of addAcc ) { if ( ! acc . has ( name )) driver . exec ( `ALTER TABLE accounts ADD COLUMN ${ name } ${ def }` ); } // 4. 이전에 이미 데이터를 수집 중이던 계정은 상태를 '완료(done)'로 자동 변경 if ( ! acc . has ( 'backfill_state' )) { driver . exec ( "UPDATE accounts SET backfill_state = 'done' WHERE last_collected_at IS NOT NULL" ); } }