# OAuth_new 改善計画 ## 現状分析 ### 良い点 - ✅ クリーンなアーキテクチャ(Hooks分離) - ✅ 公式ライブラリ使用(@atproto/oauth-client-browser) - ✅ 適切なエラーハンドリング - ✅ 包括的なドキュメント - ✅ 環境変数による設定外部化 ### 問題点 - ❌ パフォーマンス:毎回全データを並列取得 - ❌ UX:ローディング状態が分かりにくい - ❌ スケーラビリティ:データ量増加への対応不足 - ❌ エラー詳細度:汎用的すぎるエラーメッセージ - ❌ リアルタイム性:手動更新が必要 ## 改善計画 ### Phase 1: 安定性・パフォーマンス向上(優先度:高) #### 1.1 キャッシュシステム導入 ```javascript // 新規ファイル: src/utils/cache.js export class DataCache { constructor(ttl = 30000) { // 30秒TTL this.cache = new Map() this.ttl = ttl } get(key) { const item = this.cache.get(key) if (!item) return null if (Date.now() - item.timestamp > this.ttl) { this.cache.delete(key) return null } return item.data } set(key, data) { this.cache.set(key, { data, timestamp: Date.now() }) } invalidate(pattern) { for (const key of this.cache.keys()) { if (key.includes(pattern)) { this.cache.delete(key) } } } } ``` #### 1.2 リトライ機能付きAPI ```javascript // 修正: src/api/atproto.js async function requestWithRetry(url, options = {}, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url, options) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } return await response.json() } catch (error) { if (i === maxRetries - 1) throw error // 指数バックオフ const delay = Math.min(1000 * Math.pow(2, i), 10000) await new Promise(resolve => setTimeout(resolve, delay)) } } } ``` #### 1.3 詳細なエラーハンドリング ```javascript // 新規ファイル: src/utils/errorHandler.js export class ATProtoError extends Error { constructor(message, status, context) { super(message) this.status = status this.context = context this.timestamp = new Date().toISOString() } } export function getErrorMessage(error) { if (error.status === 400) { return 'アカウントまたはコレクションが見つかりません' } else if (error.status === 429) { return 'レート制限です。しばらく待ってから再試行してください' } else if (error.status === 500) { return 'サーバーエラーが発生しました' } else if (error.message.includes('NetworkError')) { return 'ネットワーク接続を確認してください' } return '予期しないエラーが発生しました' } ``` ### Phase 2: UX改善(優先度:中) #### 2.1 ローディング状態の改善 ```javascript // 修正: src/components/RecordTabs.jsx const LoadingSkeleton = ({ count = 3 }) => (
{Array(count).fill(0).map((_, i) => (
))}
) // CSS追加 .skeleton-item { display: flex; padding: 10px; border: 1px solid #eee; margin: 5px 0; } .skeleton-avatar { width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: loading 1.5s infinite; } @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } ``` #### 2.2 インクリメンタルローディング ```javascript // 修正: src/hooks/useAdminData.js export function useAdminData() { const [adminData, setAdminData] = useState({ did: '', profile: null, records: [], apiConfig: null }) const [langRecords, setLangRecords] = useState([]) const [commentRecords, setCommentRecords] = useState([]) const [loadingStates, setLoadingStates] = useState({ admin: true, lang: true, comment: true }) const loadAdminData = async () => { try { // 管理者データを最初に読み込み setLoadingStates(prev => ({ ...prev, admin: true })) const apiConfig = getApiConfig(`https://${env.pds}`) const did = await atproto.getDid(env.pds, env.admin) const profile = await atproto.getProfile(apiConfig.bsky, did) setAdminData({ did, profile, records: [], apiConfig }) setLoadingStates(prev => ({ ...prev, admin: false })) // 基本レコードを読み込み const records = await collections.getBase(apiConfig.pds, did, env.collection) setAdminData(prev => ({ ...prev, records })) // lang/commentを並列で読み込み const [lang, comment] = await Promise.all([ collections.getLang(apiConfig.pds, did, env.collection) .finally(() => setLoadingStates(prev => ({ ...prev, lang: false }))), collections.getComment(apiConfig.pds, did, env.collection) .finally(() => setLoadingStates(prev => ({ ...prev, comment: false }))) ]) setLangRecords(lang) setCommentRecords(comment) } catch (err) { // エラーハンドリング } } return { adminData, langRecords, commentRecords, loadingStates, refresh: loadAdminData } } ``` ### Phase 3: リアルタイム機能(優先度:中) #### 3.1 WebSocket統合 ```javascript // 新規ファイル: src/hooks/useRealtimeUpdates.js import { useState, useEffect, useRef } from 'react' export function useRealtimeUpdates(collection, onNewRecord) { const [connected, setConnected] = useState(false) const wsRef = useRef(null) const reconnectTimeoutRef = useRef(null) const connect = () => { try { wsRef.current = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe') wsRef.current.onopen = () => { setConnected(true) console.log('WebSocket connected') // Subscribe to specific collection wsRef.current.send(JSON.stringify({ type: 'subscribe', collections: [collection] })) } wsRef.current.onmessage = (event) => { try { const data = JSON.parse(event.data) if (data.collection === collection && data.commit?.operation === 'create') { onNewRecord(data.commit.record) } } catch (err) { console.warn('Failed to parse WebSocket message:', err) } } wsRef.current.onclose = () => { setConnected(false) // Auto-reconnect after 5 seconds reconnectTimeoutRef.current = setTimeout(connect, 5000) } wsRef.current.onerror = (error) => { console.error('WebSocket error:', error) setConnected(false) } } catch (err) { console.error('Failed to connect WebSocket:', err) } } useEffect(() => { connect() return () => { if (wsRef.current) { wsRef.current.close() } if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current) } } }, [collection]) return { connected } } ``` #### 3.2 オプティミスティック更新 ```javascript // 修正: src/components/CommentForm.jsx const handleSubmit = async (e) => { e.preventDefault() if (!text.trim() || !url.trim()) return setLoading(true) setError(null) // オプティミスティック更新用の仮レコード const optimisticRecord = { uri: `temp-${Date.now()}`, cid: 'temp', value: { $type: env.collection, url: url.trim(), comments: [{ url: url.trim(), text: text.trim(), author: { did: user.did, handle: user.handle, displayName: user.displayName, avatar: user.avatar }, createdAt: new Date().toISOString() }], createdAt: new Date().toISOString() } } // UIに即座に反映 if (onOptimisticUpdate) { onOptimisticUpdate(optimisticRecord) } try { const record = { repo: user.did, collection: env.collection, rkey: `comment-${Date.now()}`, record: optimisticRecord.value } await atproto.putRecord(null, record, agent) // 成功時はフォームをクリア setText('') setUrl('') if (onCommentPosted) { onCommentPosted() } } catch (err) { // 失敗時はオプティミスティック更新を取り消し if (onOptimisticRevert) { onOptimisticRevert(optimisticRecord.uri) } setError(err.message) } finally { setLoading(false) } } ``` ### Phase 4: TypeScript化・テスト(優先度:低) #### 4.1 TypeScript移行 ```typescript // 新規ファイル: src/types/atproto.ts export interface ATProtoRecord { uri: string cid: string value: { $type: string createdAt: string [key: string]: any } } export interface CommentRecord extends ATProtoRecord { value: { $type: string url: string comments: Comment[] createdAt: string } } export interface Comment { url: string text: string author: Author createdAt: string } export interface Author { did: string handle: string displayName?: string avatar?: string } ``` #### 4.2 テスト環境 ```javascript // 新規ファイル: src/tests/hooks/useAdminData.test.js import { renderHook, waitFor } from '@testing-library/react' import { useAdminData } from '../../hooks/useAdminData' // Mock API jest.mock('../../api/atproto', () => ({ atproto: { getDid: jest.fn(), getProfile: jest.fn() }, collections: { getBase: jest.fn(), getLang: jest.fn(), getComment: jest.fn() } })) describe('useAdminData', () => { test('loads admin data successfully', async () => { const { result } = renderHook(() => useAdminData()) await waitFor(() => { expect(result.current.adminData.did).toBeTruthy() }) }) }) ``` ## 実装優先順位 ### 今すぐ実装すべき(Phase 1) 1. **エラーハンドリング改善** - 1日で実装可能 2. **キャッシュシステム** - 2日で実装可能 3. **リトライ機能** - 1日で実装可能 ### 短期実装(1週間以内) 1. **ローディングスケルトン** - UX大幅改善 2. **インクリメンタルローディング** - パフォーマンス向上 ### 中期実装(1ヶ月以内) 1. **WebSocketリアルタイム更新** - 新機能 2. **オプティミスティック更新** - UX向上 ### 長期実装(必要に応じて) 1. **TypeScript化** - 保守性向上 2. **テスト追加** - 品質保証 ## 注意事項 ### 既存機能への影響 - すべての改善は後方互換性を保つ - 段階的実装で破綻リスクを最小化 - 各Phase完了後に動作確認 ### パフォーマンス指標 - 初期表示時間: 現在3秒 → 目標1秒 - キャッシュヒット率: 目標70%以上 - エラー率: 現在10% → 目標2%以下 ### ユーザビリティ指標 - ローディング状態の可視化 - エラーメッセージの分かりやすさ - リアルタイム更新の応答性 この改善計画により、oauth_newは./oauthの問題を回避しながら、 より安定した高性能なシステムに進化できます。