# Phase 1: 即座実装可能な修正 ## 1. エラーハンドリング強化(30分で実装) ### ファイル作成: `src/utils/errorHandler.js` ```javascript 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) return '不明なエラー' if (error.status === 400) { return 'アカウントまたはレコードが見つかりません' } else if (error.status === 401) { return '認証が必要です。ログインしてください' } else if (error.status === 403) { return 'アクセス権限がありません' } else if (error.status === 429) { return 'アクセスが集中しています。しばらく待ってから再試行してください' } else if (error.status === 500) { return 'サーバーでエラーが発生しました' } else if (error.message?.includes('fetch')) { return 'ネットワーク接続を確認してください' } else if (error.message?.includes('timeout')) { return 'タイムアウトしました。再試行してください' } return `エラーが発生しました: ${error.message || '不明'}` } export function logError(error, context = 'Unknown') { const errorInfo = { context, message: error.message, status: error.status, timestamp: new Date().toISOString(), url: window.location.href } console.error(`[ATProto Error] ${context}:`, errorInfo) // 本番環境では外部ログサービスに送信することも可能 // if (import.meta.env.PROD) { // sendToLogService(errorInfo) // } } ``` ### 修正: `src/api/atproto.js` のrequest関数 ```javascript import { ATProtoError, logError } from '../utils/errorHandler.js' async function request(url, options = {}) { try { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 15000) // 15秒タイムアウト const response = await fetch(url, { ...options, signal: controller.signal }) clearTimeout(timeoutId) if (!response.ok) { throw new ATProtoError( `Request failed: ${response.statusText}`, response.status, { url, method: options.method || 'GET' } ) } return await response.json() } catch (error) { if (error.name === 'AbortError') { const timeoutError = new ATProtoError( 'リクエストがタイムアウトしました', 408, { url } ) logError(timeoutError, 'Request Timeout') throw timeoutError } if (error instanceof ATProtoError) { logError(error, 'API Request') throw error } // ネットワークエラーなど const networkError = new ATProtoError( 'ネットワークエラーが発生しました', 0, { url, originalError: error.message } ) logError(networkError, 'Network Error') throw networkError } } ``` ### 修正: `src/hooks/useAdminData.js` ```javascript import { getErrorMessage, logError } from '../utils/errorHandler.js' export function useAdminData() { // 既存のstate... const [error, setError] = useState(null) const [retryCount, setRetryCount] = useState(0) const loadAdminData = async () => { try { setLoading(true) setError(null) const apiConfig = getApiConfig(`https://${env.pds}`) const did = await atproto.getDid(env.pds, env.admin) const profile = await atproto.getProfile(apiConfig.bsky, did) // Load all data in parallel const [records, lang, comment] = await Promise.all([ collections.getBase(apiConfig.pds, did, env.collection), collections.getLang(apiConfig.pds, did, env.collection), collections.getComment(apiConfig.pds, did, env.collection) ]) setAdminData({ did, profile, records, apiConfig }) setLangRecords(lang) setCommentRecords(comment) setRetryCount(0) // 成功時はリトライカウントをリセット } catch (err) { logError(err, 'useAdminData.loadAdminData') setError(getErrorMessage(err)) // 自動リトライ(最大3回) if (retryCount < 3) { setTimeout(() => { setRetryCount(prev => prev + 1) loadAdminData() }, Math.pow(2, retryCount) * 1000) // 1s, 2s, 4s } } finally { setLoading(false) } } return { adminData, langRecords, commentRecords, loading, error, retryCount, refresh: loadAdminData } } ``` ## 2. シンプルキャッシュ(15分で実装) ### ファイル作成: `src/utils/cache.js` ```javascript class SimpleCache { constructor(ttl = 30000) { // 30秒TTL this.cache = new Map() this.ttl = ttl } generateKey(...parts) { return parts.filter(Boolean).join(':') } 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 } console.log(`Cache hit: ${key}`) return item.data } set(key, data) { this.cache.set(key, { data, timestamp: Date.now() }) console.log(`Cache set: ${key}`) } clear() { this.cache.clear() console.log('Cache cleared') } invalidatePattern(pattern) { let deletedCount = 0 for (const key of this.cache.keys()) { if (key.includes(pattern)) { this.cache.delete(key) deletedCount++ } } console.log(`Cache invalidated: ${pattern} (${deletedCount} items)`) } getStats() { return { size: this.cache.size, keys: Array.from(this.cache.keys()) } } } export const dataCache = new SimpleCache() // デバッグ用:グローバルからアクセス可能にする if (import.meta.env.DEV) { window.dataCache = dataCache } ``` ### 修正: `src/api/atproto.js` のcollections ```javascript import { dataCache } from '../utils/cache.js' export const collections = { async getBase(pds, repo, collection, limit = 10) { const cacheKey = dataCache.generateKey('base', pds, repo, collection, limit) const cached = dataCache.get(cacheKey) if (cached) return cached const data = await atproto.getRecords(pds, repo, collection, limit) dataCache.set(cacheKey, data) return data }, async getLang(pds, repo, collection, limit = 10) { const cacheKey = dataCache.generateKey('lang', pds, repo, collection, limit) const cached = dataCache.get(cacheKey) if (cached) return cached const data = await atproto.getRecords(pds, repo, `${collection}.chat.lang`, limit) dataCache.set(cacheKey, data) return data }, async getComment(pds, repo, collection, limit = 10) { const cacheKey = dataCache.generateKey('comment', pds, repo, collection, limit) const cached = dataCache.get(cacheKey) if (cached) return cached const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit) dataCache.set(cacheKey, data) return data }, async getChat(pds, repo, collection, limit = 10) { const cacheKey = dataCache.generateKey('chat', pds, repo, collection, limit) const cached = dataCache.get(cacheKey) if (cached) return cached const data = await atproto.getRecords(pds, repo, `${collection}.chat`, limit) dataCache.set(cacheKey, data) return data }, async getUserList(pds, repo, collection, limit = 100) { const cacheKey = dataCache.generateKey('userlist', pds, repo, collection, limit) const cached = dataCache.get(cacheKey) if (cached) return cached const data = await atproto.getRecords(pds, repo, `${collection}.user`, limit) dataCache.set(cacheKey, data) return data }, async getUserComments(pds, repo, collection, limit = 10) { const cacheKey = dataCache.generateKey('usercomments', pds, repo, collection, limit) const cached = dataCache.get(cacheKey) if (cached) return cached const data = await atproto.getRecords(pds, repo, collection, limit) dataCache.set(cacheKey, data) return data }, // 投稿後にキャッシュを無効化 invalidateCache(collection) { dataCache.invalidatePattern(collection) } } ``` ### 修正: `src/components/CommentForm.jsx` にキャッシュクリア追加 ```javascript // handleSubmit内の成功時処理に追加 try { await atproto.putRecord(null, record, agent) // キャッシュを無効化 collections.invalidateCache(env.collection) // Clear form setText('') setUrl('') // Notify parent component if (onCommentPosted) { onCommentPosted() } } catch (err) { setError(err.message) } ``` ## 3. ローディング改善(20分で実装) ### ファイル作成: `src/components/LoadingSkeleton.jsx` ```javascript import React from 'react' export default function LoadingSkeleton({ count = 3, showTitle = false }) { return (
{showTitle && (
)} {Array(count).fill(0).map((_, i) => (
))}
) } ``` ### 修正: `src/components/RecordTabs.jsx` ```javascript import LoadingSkeleton from './LoadingSkeleton.jsx' export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, apiConfig, pageContext }) { const [activeTab, setActiveTab] = useState('lang') // ... 既存のロジック return (
{activeTab === 'lang' && ( !langRecords ? ( ) : ( ) )} {activeTab === 'comment' && ( !commentRecords ? ( ) : ( ) )} {activeTab === 'collection' && ( !chatRecords ? ( ) : ( ) )} {activeTab === 'users' && ( !userComments ? ( ) : ( ) )}
{/* 既存のstyle... */}
) } ``` ### 修正: `src/App.jsx` にエラー表示改善 ```javascript import { getErrorMessage } from './utils/errorHandler.js' export default function App() { const { user, agent, loading: authLoading, login, logout } = useAuth() const { adminData, langRecords, commentRecords, loading: dataLoading, error, retryCount, refresh: refreshAdminData } = useAdminData() const { userComments, chatRecords, loading: userLoading, refresh: refreshUserData } = useUserData(adminData) const pageContext = usePageContext() // ... 既存のロジック if (error) { return (

ATProto OAuth Demo

エラー: {error}

{retryCount > 0 && (

自動リトライ中... ({retryCount}/3)

)}
) } // ... 既存のレンダリング } ``` ## 実装チェックリスト ### ✅ Phase 1A: エラーハンドリング(30分) - [ ] `src/utils/errorHandler.js` 作成 - [ ] `src/api/atproto.js` の `request` 関数修正 - [ ] `src/hooks/useAdminData.js` エラーハンドリング追加 - [ ] `src/App.jsx` エラー表示改善 ### ✅ Phase 1B: キャッシュ(15分) - [ ] `src/utils/cache.js` 作成 - [ ] `src/api/atproto.js` の `collections` にキャッシュ追加 - [ ] `src/components/CommentForm.jsx` にキャッシュクリア追加 ### ✅ Phase 1C: ローディングUI(20分) - [ ] `src/components/LoadingSkeleton.jsx` 作成 - [ ] `src/components/RecordTabs.jsx` にローディング表示追加 ### テスト - [ ] エラー状態でも適切にメッセージが表示される - [ ] キャッシュがコンソールログで確認できる - [ ] ローディング中にスケルトンが表示される - [ ] 投稿後にキャッシュがクリアされる **実装時間目安**: 65分(エラーハンドリング30分 + キャッシュ15分 + ローディング20分) これらの修正により、oauth_newは./oauthで頻発している問題を回避し、 より安定したユーザー体験を提供できます。