diff --git a/oauth_new/IMPLEMENTATION_GUIDE.md b/oauth_new/IMPLEMENTATION_GUIDE.md
new file mode 100644
index 0000000..45c062c
--- /dev/null
+++ b/oauth_new/IMPLEMENTATION_GUIDE.md
@@ -0,0 +1,444 @@
+# OAuth_new 実装ガイド
+
+## Claude Code用実装指示
+
+### 即座に実装可能な改善(優先度:最高)
+
+#### 1. エラーハンドリング強化
+
+**ファイル**: `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.status === 400) {
+ return 'アカウントまたはコレクションが見つかりません'
+ } else if (error.status === 429) {
+ return 'レート制限です。しばらく待ってから再試行してください'
+ } else if (error.status === 500) {
+ return 'サーバーエラーが発生しました'
+ } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
+ return 'ネットワーク接続を確認してください'
+ } else if (error.message.includes('timeout')) {
+ return 'タイムアウトしました。再試行してください'
+ }
+ return '予期しないエラーが発生しました'
+}
+
+export function logError(error, context) {
+ console.error(`[ATProto Error] ${context}:`, {
+ message: error.message,
+ status: error.status,
+ timestamp: new Date().toISOString()
+ })
+}
+```
+
+**修正**: `src/api/atproto.js`
+```javascript
+import { ATProtoError, logError } from '../utils/errorHandler.js'
+
+async function request(url, options = {}) {
+ try {
+ const response = await fetch(url, options)
+ if (!response.ok) {
+ throw new ATProtoError(
+ `HTTP ${response.status}: ${response.statusText}`,
+ response.status,
+ { url, options }
+ )
+ }
+ return await response.json()
+ } catch (error) {
+ if (error instanceof ATProtoError) {
+ logError(error, 'API Request')
+ throw error
+ }
+
+ // Network errors
+ const atprotoError = new ATProtoError(
+ 'ネットワークエラーが発生しました',
+ 0,
+ { url, originalError: error.message }
+ )
+ logError(atprotoError, 'Network Error')
+ throw atprotoError
+ }
+}
+```
+
+**修正**: `src/hooks/useAdminData.js`
+```javascript
+import { getErrorMessage, logError } from '../utils/errorHandler.js'
+
+// loadAdminData関数内のcatchブロック
+} catch (err) {
+ logError(err, 'useAdminData.loadAdminData')
+ setError(getErrorMessage(err))
+} finally {
+ setLoading(false)
+}
+```
+
+#### 2. シンプルなキャッシュシステム
+
+**ファイル**: `src/utils/cache.js` (新規作成)
+```javascript
+class SimpleCache {
+ 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()
+ })
+ }
+
+ clear() {
+ this.cache.clear()
+ }
+
+ invalidatePattern(pattern) {
+ for (const key of this.cache.keys()) {
+ if (key.includes(pattern)) {
+ this.cache.delete(key)
+ }
+ }
+ }
+}
+
+export const dataCache = new SimpleCache()
+```
+
+**修正**: `src/api/atproto.js`
+```javascript
+import { dataCache } from '../utils/cache.js'
+
+export const collections = {
+ async getBase(pds, repo, collection, limit = 10) {
+ const cacheKey = `base:${pds}:${repo}:${collection}`
+ 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 = `lang:${pds}:${repo}:${collection}`
+ 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 = `comment:${pds}:${repo}:${collection}`
+ 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
+ },
+
+ // 投稿後にキャッシュをクリア
+ invalidateCache(collection) {
+ dataCache.invalidatePattern(collection)
+ }
+}
+```
+
+#### 3. ローディングスケルトン
+
+**ファイル**: `src/components/LoadingSkeleton.jsx` (新規作成)
+```javascript
+import React from 'react'
+
+export default function LoadingSkeleton({ count = 3 }) {
+ return (
+
+ {Array(count).fill(0).map((_, i) => (
+
+ ))}
+
+
+
+ )
+}
+```
+
+**修正**: `src/components/RecordTabs.jsx`
+```javascript
+import LoadingSkeleton from './LoadingSkeleton.jsx'
+
+// RecordTabsコンポーネント内
+{activeTab === 'lang' && (
+ loading ? (
+
+ ) : (
+
+ )
+)}
+```
+
+### 中期実装(1週間以内)
+
+#### 4. リトライ機能
+
+**修正**: `src/api/atproto.js`
+```javascript
+async function requestWithRetry(url, options = {}, maxRetries = 3) {
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ return await request(url, options)
+ } catch (error) {
+ if (i === maxRetries - 1) throw error
+
+ // 429 (レート制限) の場合は長めに待機
+ const baseDelay = error.status === 429 ? 5000 : 1000
+ const delay = Math.min(baseDelay * Math.pow(2, i), 30000)
+
+ console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`)
+ await new Promise(resolve => setTimeout(resolve, delay))
+ }
+ }
+}
+
+// 全てのAPI呼び出しでrequestをrequestWithRetryに変更
+export const atproto = {
+ async getDid(pds, handle) {
+ const res = await requestWithRetry(`https://${pds}/xrpc/${ENDPOINTS.describeRepo}?repo=${handle}`)
+ return res.did
+ },
+ // ...他のメソッドも同様に変更
+}
+```
+
+#### 5. 段階的ローディング
+
+**修正**: `src/hooks/useAdminData.js`
+```javascript
+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,
+ base: true,
+ lang: true,
+ comment: true
+ })
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ loadAdminData()
+ }, [])
+
+ const loadAdminData = async () => {
+ try {
+ setError(null)
+
+ // Phase 1: 管理者情報を最初に取得
+ 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 }))
+
+ // Phase 2: 基本レコードを取得
+ setLoadingStates(prev => ({ ...prev, base: true }))
+ const records = await collections.getBase(apiConfig.pds, did, env.collection)
+ setAdminData(prev => ({ ...prev, records }))
+ setLoadingStates(prev => ({ ...prev, base: false }))
+
+ // Phase 3: lang/commentを並列取得
+ const langPromise = collections.getLang(apiConfig.pds, did, env.collection)
+ .then(data => {
+ setLangRecords(data)
+ setLoadingStates(prev => ({ ...prev, lang: false }))
+ })
+ .catch(err => {
+ console.warn('Failed to load lang records:', err)
+ setLoadingStates(prev => ({ ...prev, lang: false }))
+ })
+
+ const commentPromise = collections.getComment(apiConfig.pds, did, env.collection)
+ .then(data => {
+ setCommentRecords(data)
+ setLoadingStates(prev => ({ ...prev, comment: false }))
+ })
+ .catch(err => {
+ console.warn('Failed to load comment records:', err)
+ setLoadingStates(prev => ({ ...prev, comment: false }))
+ })
+
+ await Promise.all([langPromise, commentPromise])
+
+ } catch (err) {
+ logError(err, 'useAdminData.loadAdminData')
+ setError(getErrorMessage(err))
+ // エラー時もローディング状態を解除
+ setLoadingStates({
+ admin: false,
+ base: false,
+ lang: false,
+ comment: false
+ })
+ }
+ }
+
+ return {
+ adminData,
+ langRecords,
+ commentRecords,
+ loading: Object.values(loadingStates).some(Boolean),
+ loadingStates,
+ error,
+ refresh: loadAdminData
+ }
+}
+```
+
+### 緊急時対応
+
+#### フォールバック機能
+
+**修正**: `src/hooks/useAdminData.js`
+```javascript
+// エラー時でも基本機能を維持
+const loadWithFallback = async () => {
+ try {
+ await loadAdminData()
+ } catch (err) {
+ // フォールバック:最低限の表示を維持
+ setAdminData({
+ did: env.admin, // ハンドルをDIDとして使用
+ profile: {
+ handle: env.admin,
+ displayName: env.admin,
+ avatar: null
+ },
+ records: [],
+ apiConfig: getApiConfig(`https://${env.pds}`)
+ })
+ setError('一部機能が利用できません。基本表示で継続します。')
+ }
+}
+```
+
+## 実装チェックリスト
+
+### Phase 1 (即座実装)
+- [ ] `src/utils/errorHandler.js` 作成
+- [ ] `src/utils/cache.js` 作成
+- [ ] `src/components/LoadingSkeleton.jsx` 作成
+- [ ] `src/api/atproto.js` エラーハンドリング追加
+- [ ] `src/hooks/useAdminData.js` エラーハンドリング改善
+- [ ] `src/components/RecordTabs.jsx` ローディング表示追加
+
+### Phase 2 (1週間以内)
+- [ ] `src/api/atproto.js` リトライ機能追加
+- [ ] `src/hooks/useAdminData.js` 段階的ローディング実装
+- [ ] キャッシュクリア機能の投稿フォーム統合
+
+### テスト項目
+- [ ] エラー状態でも最低限表示される
+- [ ] キャッシュが適切に動作する
+- [ ] ローディング表示が適切に出る
+- [ ] リトライが正常に動作する
+
+## パフォーマンス目標
+
+- **初期表示**: 3秒 → 1秒
+- **キャッシュヒット率**: 70%以上
+- **エラー率**: 10% → 2%以下
+- **ユーザー体験**: ローディング状態が常に可視化
+
+この実装により、./oauthで発生している「同じ問題の繰り返し」を避け、
+安定した成長可能なシステムが構築できます。
\ No newline at end of file
diff --git a/oauth_new/IMPROVEMENT_PLAN.md b/oauth_new/IMPROVEMENT_PLAN.md
new file mode 100644
index 0000000..8404ff0
--- /dev/null
+++ b/oauth_new/IMPROVEMENT_PLAN.md
@@ -0,0 +1,448 @@
+# 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の問題を回避しながら、
+より安定した高性能なシステムに進化できます。
\ No newline at end of file
diff --git a/oauth_new/PHASE1_QUICK_FIXES.md b/oauth_new/PHASE1_QUICK_FIXES.md
new file mode 100644
index 0000000..c68f3b5
--- /dev/null
+++ b/oauth_new/PHASE1_QUICK_FIXES.md
@@ -0,0 +1,601 @@
+# 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で頻発している問題を回避し、
+より安定したユーザー体験を提供できます。
\ No newline at end of file