package notion import ( "encoding/json" "log/slog" "os" "path/filepath" "sync" ) // snapshot is the last-known state of a page, used to diff the next event. // 페이지당 1개만 유지(덮어쓰기) — 과거 이력을 누적하지 않는다. type snapshot struct { Title string `json:"title"` Props map[string]string `json:"props"` // 속성명 → 렌더된 현재 값 Blocks map[string]string `json:"blocks,omitempty"` // 블록 ID → 텍스트 (편집된 블록만 점진 저장) } // SnapshotStore persists page snapshots to a JSON file (thread-safe). // 비활성화하려면 path를 비우면 된다(메모리에만 유지, diff는 동작). type SnapshotStore struct { mu sync.Mutex path string data map[string]snapshot // pageID → snapshot } // NewSnapshotStore loads snapshots from path (missing file is fine). func NewSnapshotStore(path string) (*SnapshotStore, error) { s := &SnapshotStore{path: path, data: make(map[string]snapshot)} if path == "" { return s, nil } b, err := os.ReadFile(path) switch { case err == nil: if e := json.Unmarshal(b, &s.data); e != nil { return nil, e } case os.IsNotExist(err): // 최초 실행: 빈 스토어로 시작 default: return nil, err } return s, nil } func (s *SnapshotStore) Get(pageID string) (snapshot, bool) { s.mu.Lock() defer s.mu.Unlock() snap, ok := s.data[pageID] return snap, ok } func (s *SnapshotStore) Put(pageID string, snap snapshot) { s.mu.Lock() defer s.mu.Unlock() s.data[pageID] = snap s.persist() } func (s *SnapshotStore) Delete(pageID string) { s.mu.Lock() defer s.mu.Unlock() delete(s.data, pageID) s.persist() } // persist writes the whole map atomically (tmp + rename). 호출자가 lock 보유. func (s *SnapshotStore) persist() { if s.path == "" { return } if dir := filepath.Dir(s.path); dir != "" && dir != "." { _ = os.MkdirAll(dir, 0o755) } b, err := json.MarshalIndent(s.data, "", " ") if err != nil { slog.Warn("notion snapshot marshal failed", "err", err) return } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { slog.Warn("notion snapshot write failed", "err", err) return } if err := os.Rename(tmp, s.path); err != nil { slog.Warn("notion snapshot rename failed", "err", err) } }