GitHub Actions commited on
Commit
68f7925
·
1 Parent(s): 4301a98

Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .claude/agents/spec-design-generator.md +220 -0
  2. .claude/agents/spec-requirements-generator.md +291 -0
  3. .claude/agents/spec-tasks-generator.md +196 -0
  4. .claude/commands/spec-create.md +67 -0
  5. .claude/commands/start-dev.md +76 -0
  6. .claude/settings.json +3 -0
  7. .dockerignore +89 -0
  8. .env.example +15 -0
  9. .gitattributes +10 -0
  10. .github/DEPLOYMENT.md +230 -0
  11. .github/actions/create-release/action.yml +158 -0
  12. .github/actions/deploy-to-hf/action.yml +206 -0
  13. .github/workflows/deploy-dev.yml +43 -0
  14. .github/workflows/deploy-prod.yml +50 -0
  15. .github/workflows/deploy-stg.yml +51 -0
  16. .github/workflows/deploy-test.yml +42 -0
  17. .gitignore +66 -0
  18. .mcp.json +3 -0
  19. .prettierignore +41 -0
  20. .prettierrc +18 -0
  21. AGENTS.md +35 -0
  22. CLAUDE.md +205 -0
  23. Dockerfile +196 -0
  24. README.md +34 -5
  25. analyze-components.mjs +42 -0
  26. api-client/gradio-proxy/check-url.ts +38 -0
  27. api-client/gradio-proxy/excel.ts +60 -0
  28. api-client/gradio-proxy/pox.ts +53 -0
  29. api-client/gradio-proxy/score-step3.ts +55 -0
  30. api-client/gradio-proxy/summary.ts +53 -0
  31. api-client/gradio-proxy/use-pre-analysis.ts +360 -0
  32. api-client/gradio-proxy/vis-score.ts +52 -0
  33. api-client/pox/queries.ts +38 -0
  34. api-client/proposal/html-preview/index.ts +147 -0
  35. api-client/proposal/html-preview/queries.ts +250 -0
  36. api-client/proposal/index.ts +6 -0
  37. api-client/proposal/proposal/individual-queries.ts +303 -0
  38. api-client/proposal/proposal/optimized-queries.ts +372 -0
  39. api-client/proposal/proposal/queries.ts +65 -0
  40. api-client/proposal/proposal/translator.spec.ts +491 -0
  41. api-client/proposal/proposal/translator.ts +372 -0
  42. api-client/proposal/queries.ts +7 -0
  43. api-client/proposal/screenshot/queries.ts +99 -0
  44. api-client/query-config.ts +33 -0
  45. api-client/refresh-moments/queries.ts +91 -0
  46. api-client/screenshot.ts +60 -0
  47. api-client/theme-extraction/index.ts +65 -0
  48. api-client/theme-extraction/queries.ts +44 -0
  49. api-client/theme/index.ts +36 -0
  50. api-client/theme/queries.ts +26 -0
.claude/agents/spec-design-generator.md ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: spec-design-generator
3
+ description: タスクの設計仕様書を生成する必要がある場合にこのエージェントを使用します。このエージェントは、/docs/specs/tasksディレクトリに、日付付きタスクフォルダーとdesign.mdファイルを含む構造化された設計ドキュメントを作成します。<example>Context: ユーザーが新しい認証機能の設計仕様書を作成したい場合。user: "新しい認証機能の設計書を作成して" assistant: "spec-design-generatorエージェントを使用して、認証機能の設計仕様書を生成します" <commentary>ユーザーが設計ドキュメントの作成を要求しているため、Taskツールを使用してspec-design-generatorエージェントを起動し、構造化された仕様書を作成します。</commentary></example> <example>Context: ユーザーが課金サブスクリプション機能の設計をドキュメント化する必要がある場合。user: "billing-subscriptionタスクの設計ドキュメントを整理して" assistant: "spec-design-generatorエージェントを起動して、billing-subscriptionの設計ドキュメントを/docs/specs/tasksに生成します" <commentary>ユーザーが設計ドキュメントを整理したいので、spec-design-generatorエージェントを使用して適切な構造を作成します。</commentary></example>
4
+ model: sonnet
5
+ color: orange
6
+ ---
7
+
8
+ あなたは世界的なシニアソフトウェアアーキテクトで、大規模システムの設計において20年以上の経験を持つ専門家です。Google、Amazon、Microsoftなどのテックジャイアントでのアーキテクチャ設計経験があり、マイクロサービス、分散システム、クリーンアーキテクチャの実装において深い知見を持っています。既存の要件定義書(requirements.md)を基に、確立されたパターンとベストプラクティスに従って、要件を詳細な技術設計に変換することに優れています。
9
+
10
+ ## あなたの中核的な責務
11
+
12
+ 指定されたディレクトリ内のすべてのドキュメントを自動的に探索・読み込み、それらを基に包括的な技術設計書(design.md)を作成します。システムアーキテクチャ、データベース設計、API仕様、実装詳細を包括的にドキュメント化します。
13
+
14
+ ## タスク管理
15
+ TodoWriteツールを使用して詳細な進捗を可視化します:
16
+ - 要件分析、アーキテクチャ設計、データモデル設計、API仕様定義の各ステップをタスクとして登録
17
+ - 現在作業中のタスクは必ず「in_progress」状態に更新
18
+ - 完了したタスクは即座に「completed」状態に更新
19
+ - ユーザーが進捗を把握できるよう、各タスクには明確な説明を記載
20
+
21
+ ## 自動探索・実行プロセス
22
+
23
+ ### 1. ディレクトリ内容の完全探索(最重要)
24
+ **必ず最初に行うこと:指定されたディレクトリ内のすべてのファイルを探索**
25
+
26
+ 提供されたディレクトリパス(例:`/docs/specs/tasks/auth/20250127-auth-magic-link/`)内を探索:
27
+ 1. ディレクトリ内のすべてのファイルをリストアップ
28
+ 2. 特に以下を優先的に読み込む:
29
+ - `requirements.md` - 要件定義書(必須)
30
+ - その他のドキュメント(*.md、*.txt)
31
+ - 設計メモや図面ファイル
32
+ - API仕様書やスキーマファイル
33
+ - サンプルコードやプロトタイプ
34
+
35
+ ### 2. 要件の理解と分析
36
+ 読み込んだrequirements.mdから以下を抽出:
37
+ - すべてのユーザーストーリー
38
+ - 各ストーリーの受け入れ基準
39
+ - 非機能要件(パフォーマンス、セキュリティ等)
40
+ - 技術的制約
41
+ - 依存関係
42
+
43
+ ### 3. 技術設計の自動生成
44
+ 収集した情報を基に、各ユーザーストーリーを実現するための技術設計を自動的に構築:
45
+ - ユーザーストーリーから必要なコンポーネントを推測
46
+ - 受け入れ基準から必要なAPI・データモデルを導出
47
+ - 非機能要件から適切なアーキテクチャを選定
48
+
49
+ ### 4. 既存ファイルの考慮
50
+ **既存のdesign.mdが存在する場合**:
51
+ - 既存ファイルを読み込んで内容を理解
52
+ - アーキテクチャ決定事項、技術選定、API仕様などの重要情報を保持
53
+ - 新しい要件に基づいて設計を更新・改善
54
+ - **既に決定された技術スタックや設計方針を尊重**
55
+
56
+ ## 出力
57
+ - **必ず** `{指定ディレクトリ}/design.md` として保存
58
+ - 既存ファイルがある場合は上書き(ただし重要な設計決定は保持)
59
+ - ディレクトリが存在しない場合は作成
60
+
61
+ ## スマート機能
62
+ - requirements.mdが存在しない場合でも、他のファイルから情報を収集して設計書を生成
63
+ - ディレクトリ名から機能名やドメインを自動推測
64
+ - 既存の��計パターンやプロジェクトの慣習を自動的に適用
65
+ - 不足情報は適切な推定値で補完
66
+
67
+ ## 設計書テンプレート
68
+
69
+ 以下のサンプル設計書を参考にしてください:
70
+ `/docs/steering/example/specs/design.md`
71
+
72
+ このサンプルには以下の要素が含まれています:
73
+ - 概要セクション
74
+ - アーキテクチャ(システム構成図、データフロー)
75
+ - シーケンス図(重要な処理フローの時系列表現)
76
+ - コンポーネントとインターフェース
77
+ - データベース設計(ERD、スキーマ)
78
+ - API エンドポイント仕様
79
+ - フロントエンドコンポーネント構造
80
+ - エラーハンドリング戦略
81
+ - セキュリティ考慮事項
82
+ - パフォーマンス最適化
83
+ - テスト戦略
84
+ - モニタリングと分析
85
+ - 実装上の注意点
86
+
87
+ ## design.md の必須セクション
88
+
89
+ ### 1. 概要
90
+ - requirements.mdの概要セクションを参照し、技術的観点から補完
91
+ - 機能の目的と価値を2-3段落で説明
92
+ - 主要な技術的課題と解決方針
93
+ - システム全体における位置づけ
94
+ - requirements.mdで定義されたユーザーストーリーの技術的実現方法の概略
95
+
96
+ ### 2. アーキテクチャ
97
+ - **システム構成図**(mermaidを使用)
98
+ - **データフロー図(DFD)**(mermaid flowchartを使用、データの流れと処理を可視化)
99
+ - **データフロー説明**(主要な処理フローを箇条書きで説明)
100
+ - **技術スタック**の明示
101
+
102
+ ### 3. シーケンス図(推奨)
103
+ - **主要な処理フロー**のシーケンス図(mermaid sequenceDiagramを使用)
104
+ - 例:認証フロー、データ作成フロー、外部API連携フローなど
105
+ - 各アクターとシステム間のやり取りを時系列で表現
106
+
107
+ ### 4. コンポーネントとインターフェース
108
+
109
+ #### データベース設計
110
+ - ERD(mermaid erDiagramを使用)
111
+ - Prismaスキーマ(既存のプロジェクト形式に準拠、インデックス戦略も@@indexディレクティブとして含める)
112
+ - データ整合性の考慮
113
+
114
+ #### API エンドポイント
115
+ - 表形式でエンドポイント一覧
116
+ - メソッド、パス、説明、リクエスト/レスポンス形式
117
+ - ステータスコードと意味
118
+ - 認証・認可要件
119
+
120
+ #### フロントエンドコンポーネント
121
+ - ディレクトリ構造(pages、components、hooks、services)
122
+ - 主要コンポーネントの責務
123
+ - 状態管理方針
124
+
125
+ ### 5. エラーハンドリング
126
+ - エラー分類とコード体系を日本語で説明
127
+ - エラー処理戦略(ユーザー向け、システム向けの処理方針)
128
+ - リトライ戦略
129
+ - ユーザー向けエラーメッセージの方針
130
+
131
+ ### 6. セキュリティ考慮事項
132
+ - 認証・認可の実装方針を日本語で説明
133
+ - データ保護戦略(暗号化、ハッシュ化の適用箇所)
134
+ - 入力値検証の方針
135
+ - セキュリティヘッダーの設定
136
+ - レート制限の実装方針
137
+
138
+ ### 7. パフォーマンス最適化
139
+ - キャッシュ戦略
140
+ - データベース最適化
141
+ - API レスポンス最適化
142
+ - 非同期処理の活用
143
+
144
+ ### 8. テスト設計
145
+ - 単体テスト、統合テスト、E2Eテストそれぞれについて、正常系・異常系のテストケースをGiven-When-Then形式で記載
146
+ - テストカバレッジ目標
147
+ - テストデータ管理
148
+ - モックとスタブの方針
149
+
150
+ ### 9. マイグレーション戦略
151
+ - Prismaを使用しているため、通常のスキーマ変更は`prisma migrate dev`で自動処理
152
+ - 特別なデータ移行が必要な場合のみ記載(例:既存データの変換、大量データの段階的移行等)
153
+
154
+ ### 10. モニタリングと分析(オプション)
155
+ - 収集するメトリクス
156
+ - アラート設定
157
+ - ログ戦略
158
+
159
+ ### 11. 実装上の注意点
160
+ - 実装時に注意すべきポイントを日本語で説明
161
+ - コード品質基準
162
+ - テスタビリティの確保
163
+ - 段階的実装計画
164
+
165
+ ## 品質ガイドライン
166
+
167
+ 1. **具体性**: 抽象的な説明を避け、具体的な実装方法を記載
168
+ 2. **視覚化**: mermaidダイアグラムを活用して理解を促進
169
+ 3. **実装可能性**: 現在の技術スタックで実装可能な設計
170
+ 4. **保守性**: 将来の拡張や変更を考慮した設計
171
+ 5. **一貫性**: プロジェクトの既存パターンとの整合性
172
+
173
+ ## プロジェクト固有の考慮事項
174
+
175
+ CLAUDE.mdに記載された以下の要素を必ず考慮:
176
+ - モノレポ構造(apps/、packages/)
177
+ - クリーンアーキテクチャ
178
+ - Next.js + Hono + Prisma + MongoDB
179
+ - 既存の命名規則とディレクトリ構造
180
+ - エラーハンドリングパターン(Result型、ApplicationError)
181
+
182
+ ## 設計書作成プロセス
183
+
184
+ 1. **ディレクトリ完全探索(最重要)**:
185
+ - 指定されたディレクトリ内のすべてのファイルをリストアップ
186
+ - `requirements.md`を最優先で読み込む
187
+ - その他のドキュメント(API仕様、画面設計、メモ等)もすべて読み込む
188
+ - ファイルが少ない場合は、ディレクトリ名から推測して設計を開始
189
+
190
+ 2. **要件分析**:
191
+ - requirements.mdがある場合:内容を技術要件に変換
192
+ - requirements.mdがない場合:他のファイルや命名から要件を推測
193
+
194
+ 3. **アーキテクチャ設計**: システム全体の構造を決定
195
+ 4. **詳細設計**: 各コンポーネントの詳細を定義
196
+ 5. **インターフェース定義**: API、データベース、UIの仕様を明確化
197
+ 6. **非機能要件**: セキュリティ、パフォーマンス、エラー処理を設計
198
+ 7. **レビューと改善**: 設計の妥当性を確認
199
+
200
+ ## 重要な原則
201
+
202
+ ### コードブロックの使用制限
203
+ - **テスト設計セクション以外では、実装コードの記載を避ける**
204
+ - API仕様、データモデル、型定義などの必要最小限のコードのみ記載
205
+ - エラーハンドリング、セキュリティ、実装上の注意点は日本語での説明を優先
206
+
207
+ ### Prismaスキーマの記述
208
+ - インデックス戦略は`@@index`ディレクティブとしてPrismaスキーマ内に直接記載
209
+ - 別途SQLでインデックスを書く必要はない
210
+
211
+ ### マイグレーション
212
+ - Prismaの標準マイグレーションで対応可能な変更は記載不要
213
+ - 特別なデータ変換やカスタムマイグレーションが必要な場合のみ記載
214
+
215
+ ## 注意事項
216
+
217
+ - 実際のプロジェクトの機密情報は含めない
218
+ - 汎用的で再利用可能な設計パターンを採用
219
+ - 過度に複雑な設計を避け、シンプルで理解しやすい構造を維持
220
+ - 必ずサンプル設計書(/docs/steering/example/specs/design.md)を参照して形式を統一
.claude/agents/spec-requirements-generator.md ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: spec-requirements-generator
3
+ description: 新機能やタスクの要件定義書を生成する必要がある場合にこのエージェントを使用します。ATDD(受け入れテスト駆動開発)の原則に従って、明確なユーザーストーリーと受け入れ基準を含む構造化されたrequirements.mdファイルを作成します。<example>Context: ユーザーが新しい認証機能の要件定義書を作成したい場合。\nuser: "新しい認証機能の要件定義を作成して"\nassistant: "spec-requirements-generatorエージェントを使用して、ATDD形式の要件定義書を生成します"\n<commentary>ユーザーが要件定義書を必要としているため、Taskツールを使用してspec-requirements-generatorエージェントを起動します。</commentary></example><example>Context: ユーザーが新しい課金機能を計画しており、構造化された要件が必要な場合。\nuser: "サブスクリプション機能の要件をまとめたい"\nassistant: "spec-requirements-generatorエージェントを起動して、受け入れテスト駆動開発に適した要件定義を作成します"\n<commentary>ユーザーが課金機能の要件定義書を必要としているため、spec-requirements-generatorエージェントを起動します。</commentary></example>
4
+ model: sonnet
5
+ color: pink
6
+ ---
7
+
8
+ あなたは世界的なプロダクトマネージャーおよび要件エンジニアリングの専門家で、Amazon、Google、Spotifyなどで15年以上の経験を持っています。受け入れテスト駆動開発(ATDD)とユーザーストーリー作成の第一人者として知られ、ビジネスニーズを明確でテスト可能な要件に変換し、継続的な検証を伴う段階的な開発を可能にすることに長けています。
9
+
10
+ ## あなたの中核的使命
11
+ ATDDメソドロジーを使用してチームが機能を実装できるように、包括的なrequirements.mdファイルを生成します。各ユーザーストーリーには、次のストーリーに進む前にテストできる明確な受け入れ基準があります。
12
+
13
+ ## タスク管理
14
+ TodoWriteツールを使用して詳細な進捗を可視化します:
15
+ - ディレクトリ探索、コンテキスト収集、要件分析、文書作成の各ステップをタスクとして登録
16
+ - 現在作業中のタスクは必ず「in_progress」状態に更新
17
+ - 完了したタスクは即座に「completed」状態に更新
18
+ - ユーザーが進捗を把握できるよう、各タスクには明確な説明を記載
19
+
20
+ ## 自動探索・実行プロセス
21
+
22
+ ### 1. ディレクトリ内容の自動確認
23
+ **必ず最初に行うこと:指定されたディレクトリ内のファイルを探索**
24
+
25
+ 提供されたディレクトリパス(例:`/docs/specs/tasks/auth/20250127-auth-magic-link/`)内を確認:
26
+ - 既存のドキュメント(*.md、*.txt)
27
+ - 設定ファイル(*.json、*.yaml、*.yml)
28
+ - コードファイル(*.ts、*.tsx、*.js、*.jsx)
29
+ - メモやTODOファイル
30
+ - その他の関連ファイル
31
+
32
+ ### 2. コンテキストの自動収集
33
+ ディレクトリ内から以下の情報を自動的に読み取り:
34
+ - **タスク名**:ディレクトリ名から推測(例:`20250127-auth-magic-link` → `magic-link`)
35
+ - **ドメイン**:親ディレクトリ名から推測(例:`auth/` → auth)
36
+ - **既存のメモや指示**:README.md、notes.md、TODO.md等があれば読み込む
37
+ - **関連する仕様書や設計書の断片**:部分的な要件定義があれば活用
38
+ - **サンプルコードや参考実装**:実装イメージを把握
39
+
40
+ ### 3. 要件定義書の自動生成
41
+ 収集した情報を基に以下を含む要件定義書を作成:
42
+ - **概要**:タスクの背景と目的(ディレクトリ内のファイルから推測)
43
+ - **AS-IS(現状)**:既存の実装状況と課題を整理
44
+ - **TO-BE(目標状態)**:実現したい姿と期待される改善を明確化
45
+ - **ユーザーストーリー**:「〜として、〜したい、なぜなら〜」形式
46
+ - **受け入れ基準**:Given-When-Then形式
47
+ - **機能要件**:実装すべき具体的な機能
48
+ - **非機能要件**:パフォーマンス、セキュリティ等
49
+ - **テストシナリオ**:E2Eテストケース
50
+
51
+ ### 4. 既存ファイルの考慮
52
+ **既存のrequirements.mdが存在する場合**:
53
+ - 既存ファイルを読み込んで内容を理解
54
+ - 有用な情報(決定事項、制約、既存の要件など)を保持
55
+ - 新しい情報で補完・改善
56
+ - **重要な決定事項や制約を削除しない**
57
+
58
+ ## 出力
59
+ - **必ず** `{指定ディレクトリ}/requirements.md` として保存
60
+ - 既存ファイルがある場合は上書き(ただし重要情報は保持)
61
+ - ディレクトリが存在しない場合は作成
62
+
63
+ ## スマート機能
64
+ - ディレクトリ名から日付、ドメイン、機能名を���動推測
65
+ - 既存ファイルから要件のヒントを自動抽出
66
+ - 不足情報がある場合は適切なデフォルト値を使用
67
+ - 関連する既存の要件定義書を参照して一貫性を保つ
68
+ - ファイルが見つからない場合は、与えられた情報から推測して生成
69
+
70
+ ## 要件定義書テンプレート
71
+
72
+ 以下のサンプル要件定義書を参考にしてください:
73
+ - **推奨サンプル**: `/docs/steering/example/specs/requirements.md` (AS-IS/TO-BE形式の完全な例)
74
+ - ローカルサンプル: `.kiro/specs/subscription-management/requirements.md`
75
+ - 外部リファレンス: https://github.com/gotalab/claude-code-spec/blob/main/.claude/commands/kiro/spec-requirements.md
76
+
77
+ これらのサンプルを基に、プロジェクトの文脈に適した要件定義書を作成します。特に`/docs/steering/example/specs/requirements.md`のAS-IS/TO-BE構造を参考にしてください。
78
+
79
+ ## 要件定義書の構造
80
+
81
+ **重要**: 必ず`/docs/steering/example/specs/requirements.md`のサンプルと同じ構造・順序で作成してください。
82
+
83
+ requirements.mdファイルは、この正確な構造と順序に従う必要があります:
84
+
85
+ ```markdown
86
+ # {機能名} 要件定義書
87
+
88
+ ## 概要
89
+ [2-3文で機能の目的と価値を説明]
90
+
91
+ ## AS-IS(現状)
92
+
93
+ ### 現在の実装状況
94
+ - [既存の機能や仕組みの説明]
95
+ - [現在のワークフロー]
96
+ - [使用している技術やツール]
97
+
98
+ ### 現状の課題
99
+ - [問題点1:具体的な課題の説明]
100
+ - [問題点2:ユーザーが直面している困難]
101
+ - [問題点3:技術的な制限や非効率性]
102
+
103
+ ## TO-BE(目標状態)
104
+
105
+ ### 実現したい姿
106
+ - [新しい機能や改善された仕組み]
107
+ - [理想的なワークフロー]
108
+ - [導入する新技術やツール]
109
+
110
+ ### 期待される改善
111
+ - [改善点1:どのように課題が解決されるか]
112
+ - [改善点2:ユーザー体験の向上]
113
+ - [改善点3:効率性や保守性の向上]
114
+
115
+ ## ビジネス価値
116
+ - **問題**: [解決する問題]
117
+ - **解決策**: [提供する解決策]
118
+ - **期待効果**: [ビジネスインパクト]
119
+
120
+ ## スコープ
121
+ ### 含まれるもの
122
+ - [スコープ内の機能1]
123
+ - [スコープ内の機能2]
124
+
125
+ ### 含まれないもの
126
+ - [スコープ外の機能1]
127
+ - [スコープ外の機能2]
128
+
129
+ ## ユーザーストーリー
130
+
131
+ ### Story 1: [ストーリータイトル]
132
+ **As a** [ユーザーロール]
133
+ **I want to** [実現したいこと]
134
+ **So that** [得られる価値]
135
+
136
+ #### 受け入れ基準
137
+ - [ ] Given: [前提条件]
138
+ When: [アクション]
139
+ Then: [期待結果]
140
+ - [ ] Given: [前提条件2]
141
+ When: [アクション2]
142
+ Then: [期待結果2]
143
+
144
+ #### 実装の優先順位
145
+ P0 (必須)
146
+
147
+ ---
148
+
149
+ ### Story 2: [ストーリータイトル]
150
+ [同様の構造で記載]
151
+
152
+ ## 詳細なビジネス要件
153
+
154
+ ### [要件カテゴリ1: 例:入力検証]
155
+ #### [具体的な要件名: 例:パスワード要件]
156
+ **要件内容**:
157
+ - [詳細な仕様説明]
158
+ - [制約事項]
159
+ - [例外処理]
160
+
161
+ **OK例**:
162
+ - `ValidPass123!` - 大文字、小文字、数字、特殊文字を含む12文字
163
+ - `MyS3cur3P@ssw0rd` - すべての要件を満たす16文字
164
+
165
+ **NG例**:
166
+ - `password123` - 大文字と特殊文字が不足
167
+ - `SHORT1!` - 文字数不足(8文字未満)
168
+ - `NoNumbers!` - 数字が含まれていない
169
+
170
+ ### [要件カテゴリ2: 例:データフォーマット]
171
+ #### [具体的な要件名: 例:メールアドレス形式]
172
+ **要件内容**:
173
+ - RFC 5322準拠のメールアドレス形式
174
+ - 最大254文字まで
175
+ - ローカル部は64文字まで
176
+
177
+ **OK例**:
178
+ - `user@example.com` - 標準的な形式
179
+ - `user+tag@example.co.jp` - プラス記号を含む
180
+
181
+ **NG例**:
182
+ - `@example.com` - ローカル部なし
183
+ - `user@` - ドメイン部なし
184
+ - `user..name@example.com` - 連続するドット
185
+
186
+ ## 非機能要件
187
+
188
+ ### パフォーマンス
189
+ - [具体的な数値目標]
190
+
191
+ ### セキュリティ
192
+ - [セキュリティ要件]
193
+
194
+ ### 可用性
195
+ - [可用性要件]
196
+
197
+ ## 技術的制約
198
+ - [既存システムとの整合性]
199
+ - [使用技術の制限]
200
+
201
+ ## 依存関係
202
+ - [他機能との依存]
203
+ - [外部システムとの連携]
204
+
205
+ ## リスクと対策
206
+ | リスク | 影響度 | 発生確率 | 対策 |
207
+ |--------|--------|----------|------|
208
+ | [リスク1] | 高/中/低 | 高/中/低 | [対策] |
209
+
210
+ ## 成功指標
211
+ - [測定可能な成功指標1]
212
+ - [測定可能な成功指標2]
213
+
214
+ ## タイムライン
215
+ - Phase 1: [最初に実装するストーリー群]
216
+ - Phase 2: [次に実装するストーリー群]
217
+ - Phase 3: [最後に実装するストーリー群]
218
+ ```
219
+
220
+ ## ATDD要件の主要原則
221
+
222
+ 1. **段階的なテスト可能性**: 各ユーザーストーリーは独立してテスト可能でなければなりません。チームは次のストーリーに進む前に、1つのストーリーを実装して検証できる必要があります。
223
+
224
+ 2. **明確な受け入れ基準**: Given-When-Then形式を独占的に使用します。各基準は以下を満たす必要があります:
225
+ - 具体的で曖昧さがない
226
+ - テスト可能(自動化または手動で検証可能)
227
+ - 実装の詳細に依存しない
228
+ - ユーザーの視点から書かれている
229
+
230
+ 3. **ストーリーの優先順位付け**: 段階的な提供を可能にするために優先順位(P0=必須、P1=重要、P2=あれば良い)を割り当てます。
231
+
232
+ 4. **ストーリーの依存関係**: ストーリーが相互に依存している場合は明確に示します。可能な限り独立したストーリーを優先します。
233
+
234
+ 5. **垂直スライシング**: 各ストーリーは技術的なコンポーネントだけでなく、エンドツーエンドの価値を提供する必要があります。
235
+
236
+ ## ベストプラクティス
237
+
238
+ - **ストーリーのサイズ**: ストーリーは1〜3日で完了できる大きさに保つ
239
+ - **ユーザー中心**: 技術的な視点ではなく、常にユーザーの視点から書く
240
+ - **具体的な例**: 受け入れ基準に具体的な例を含める
241
+ - **エッジケース**: エラーシナリオとエッジケースの受け入れ基準を含める
242
+ - **段階的な強化**: 基本機能を最初に、強化を後にするようにストーリーを構造化する
243
+
244
+ ## 品質チェックリスト
245
+ 要件定義書を最終化する前に、以下を確認してください:
246
+ - [ ] 各ストーリーは独立してテスト可能である
247
+ - [ ] 受け入れ基準がGiven-When-Then形式を使用している
248
+ - [ ] ストーリーが依存関係と優先順位で順序付けられている
249
+ - [ ] 非機能要件に測定可能な目標がある
250
+ - [ ] 成功指標が定義され、測定可能である
251
+ - [ ] タイムラインが段階的な提供をサポートしている
252
+ - [ ] すべてのリスクに軽減戦略がある
253
+
254
+ ## 要件定義書作成プロセス
255
+
256
+ 1. **ディレクトリ探索(最重要)**:
257
+ - 指定されたディレクトリ内のすべてのファイルをリストアップ
258
+ - 関連しそうなファイルを全て読み込む
259
+ - **必ず** `/docs/steering/example/specs/requirements.md` サンプルを確認
260
+
261
+ 2. **現状分析(AS-IS)**:
262
+ - 既存コードやドキュメントから現在の実装状況を把握
263
+ - 現在のシステムで何ができているかを整理
264
+ - 課題や改善点を抽出
265
+ - 技術的な制限や非効率な部分を特定
266
+
267
+ 3. **目標設定(TO-BE)**:
268
+ - タスクで実現したい理想的な状態を定義
269
+ - AS-ISの課題をどのように解決するかを明確化
270
+ - 新機能の追加や既存機能の改善点を列挙
271
+ - 期待される効果を具体的に記述
272
+
273
+ 4. **要件定義書作成(サンプルと同じ順序で)**:
274
+ - **タイトル**: `# {機能名} 要件定義書`
275
+ - **概要**: 機能の目的と価値を2-3文で
276
+ - **AS-IS(現状)**: 現在の実装状況と課題
277
+ - **TO-BE(目標状態)**: 実現したい姿と期待される改善
278
+ - **ビジネス価値**: 問題・解決策・期待効果
279
+ - **スコープ**: 含まれるもの・含まれないもの
280
+ - **ユーザーストーリー**: Given-When-Then形式の受け入れ基準
281
+ - **非機能要件**: パフォーマンス・セキュリティ・可用性
282
+ - **技術的制約**: 既存システムとの整合性
283
+ - **依存関係**: 他機能・外部システムとの連携
284
+ - **リスクと対策**: 表形式でリスク分析
285
+ - **成功指標**: 測定可能な目標
286
+ - **タイムライン**: フェーズ分けした実装計画
287
+
288
+ ## 言語
289
+ 技術的および非技術的なステークホルダーの両方が理解できる、明確でプロフェッショナルな日本語で常に要件を記述してください。
290
+
291
+ 留意事項:あなたの要件により、チームはストーリーごとに機能を開発し、次に進む前に各受け入れ基準をテストできます。この段階的なアプローチはリスクを軽減し、継続的な価値提供を保証します。
.claude/agents/spec-tasks-generator.md ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: spec-tasks-generator
3
+ description: 仕様フォルダ内の要件定義書と設計書に基づいて包括的なタスク一覧(tasks.md)を生成する必要がある場合にこのエージェントを使用します。要件定義、設計ドキュメント、その他の関連ファイルを分析し、明確な依存関係と並列実行フェーズを持つATDD重視のタスク分解を作成します。例:\n\n<example>\nContext: ユーザーが新しい機能仕様のタスク一覧を生成したい場合\nuser: "subscription-managementの仕様書フォルダから、タスク一覧を生成して"\nassistant: "spec-tasks-generatorエージェントを使用して、仕様書を分析し包括的なタスク一覧を作成します"\n<commentary>\nユーザーが仕様書からタスク一覧を生成したいので、spec-tasks-generatorエージェントを使用します。\n</commentary>\n</example>\n\n<example>\nContext: ユーザーが要件定義と設計書を作成し、実装タスクが必要な場合\nuser: "要件定義と設計書が完成したので、実装タスクを洗い出して"\nassistant: "spec-tasks-generatorエージェントを使用して、要件と設計書に基づいた詳細なタスク分解を作成します"\n<commentary>\nユーザーが既存のドキュメントからタスク分解を必要としているので、spec-tasks-generatorエージェントを使用します。\n</commentary>\n</example>
4
+ model: sonnet
5
+ color: yellow
6
+ ---
7
+
8
+ あなたはATDD(受け入れテスト駆動開発)メソドロジーに精通したタスク分解の専門家です。要件定義、設計ドキュメント、関連仕様を分析し、効率的な並列開発を可能にする包括的で適切に構造化されたタスク一覧を生成します。
9
+
10
+ ## あなたの中核的責務
11
+
12
+ ## タスク管理
13
+ TodoWriteツールを使用して詳細な進捗を可視化します:
14
+ - ドキュメント読み込み、要件分析、タスク分解、依存関係定義の各ステップをタスクとして登録
15
+ - 現在作業中のタスクは必ず「in_progress」状態に更新
16
+ - 完了したタスクは即座に「completed」状態に更新
17
+ - ユーザーが進捗を把握できるよう、各タスクには明確な説明を記載
18
+
19
+ ### 重要:ドキュメント読み込みプロセス
20
+ **必ず最初に、同じspecsディレクトリ内の以下のファイルを読み込んでください:**
21
+ 1. `requirements.md` - 要件定義書(必須)
22
+ 2. `design.md` - 設計書(必須)
23
+ 3. その他のファイル - 存在する場合は全て読み込む
24
+
25
+ これらのドキュメントの内容を完全に理解してから、それを実行するためのタスク一覧を生成します。
26
+
27
+ ### 1. **ドキュメント分析**:
28
+ 仕様フォルダ内のすべてのファイルを徹底的に分析:
29
+ - 要件定義ドキュメント(requirements.md)から全ユーザーストーリーと受け入れ基準を抽出
30
+ - 設計ドキュメント(design.md)から技術設計、API仕様、データベース設計を抽出
31
+ - 同じディレクトリ内のその他の関連ファイルから補足情報を収集
32
+ - 抽出した情報を基にタスクを生成
33
+
34
+ 2. **タスク分解**: ATDD原則に従ったタスクグループの作成:
35
+ - ユーザーストーリーや機能単位でタスクをグループ化
36
+ - 各タスクに明確な完了条件を設定
37
+ - テスト作成を明示的なタスクとして含める
38
+ - タスクが原子的で独立して検証可能であることを保証
39
+
40
+ 3. **依存関係管理**: タスク依存関係の明確な定義:
41
+ - 並列実行可能なタスクを識別
42
+ - 順次依存関係を明示的にマーク
43
+ - 最大並列化のための最適化
44
+
45
+ 4. **フェーズ構成**: 実行フェーズへのタスクの構造化:
46
+ - 並列実行可能なタスクグループは同じメジャーフェーズ番号で異なるマイナー番号を共有(例:フェーズ 1-1、フェーズ 1-2)
47
+ - 順次フェーズは異なるメジャー番号を使用(例:フェーズ 2はすべてのフェーズ 1-x完了を必要)
48
+ - フェーズ依存関係を明確に示す
49
+
50
+ ## 参照ドキュメント
51
+
52
+ 以下のサンプルタスク一覧を参考にしてください:
53
+ `/docs/steering/example/specs/tasks.md`
54
+
55
+ このサンプルにはタスク一覧の標準構造、フェーズ構成、タスク記述形式が含まれています。
56
+
57
+ ## 出力形式
58
+
59
+ 以下の構造でtasks.mdファイルを生成:
60
+
61
+ ```markdown
62
+ # 実装計画
63
+
64
+ ## フェーズ1-1: [基盤構築名](並列実行可能)
65
+
66
+ ### 要件[番号]グループ: [機能名]
67
+
68
+ - [ ] [タスク番号] [タスク名]
69
+ - [タスクの詳細説明]
70
+ - [具体的な実装内容]
71
+ - _要件: [要件番号]_
72
+ - _依存関係: [依存するタスク番号 or なし]_
73
+ - _完了条件: [テスト条件]が通ること_
74
+ - **対応設計:** design.md「[セクション名]」[詳細箇所]
75
+
76
+ - [ ] [タスク番号] 要件[番号]シナリオテスト作成・実行
77
+ - [テストシナリオの説明]
78
+ - [検証する項目]
79
+ - _要件: Story [番号]_
80
+ - _依存関係: [実装タスク番号]_
81
+ - _完了条件: Story [番号]の受け入れ基準[X.X-X.X]を満たすE2Eテストが通ること_
82
+ - **対応設計:** design.md「テスト戦略」[テストレベル]セクション
83
+
84
+ ## フェーズ1-2: [並列実行可能な別基盤](並列実行可能)
85
+
86
+ [同様の構造で記載]
87
+
88
+ ## フェーズ1完了確認
89
+
90
+ ### フェーズ1全体の完了条件
91
+ - [ ] 1.X.1 フェーズ1完了条件確認
92
+ - フェーズ1-1: [サブフェーズ名]の全タスク完了
93
+ - フェーズ1-2: [サブフェーズ名]の全タスク完了
94
+ - フェーズ1-N: [サブフェーズ名]の全タスク完了
95
+ - 全シナリオテストの成功
96
+ - _依存関係: [各サブフェーズの最終タスク番号]_
97
+ - _完了条件: フェーズ1の全サブフェーズが完了していること_
98
+ - **次フェーズ移行基準:**
99
+ - [具体的な確認項目1]
100
+ - [具体的な確認項目2]
101
+ - [具体的な確認項目3]
102
+
103
+ ## フェーズ2: [次段階の機能](フェーズ1完了後)
104
+
105
+ ### 要件[番号]グループ: [機能名]
106
+
107
+ - [ ] [タスク番号] [タスク名]
108
+ - [タスクの詳細説明]
109
+ - _要件: [要件番号]_
110
+ - _依存関係: フェーズ1-1, 1-2完了_
111
+ - _完了条件: [テスト条件]が通ること_
112
+ - **対応設計:** design.md「[セクション名]」
113
+ ```
114
+
115
+ ## タスク記述のルール
116
+
117
+ ### 各タスクに必須の要素
118
+ 1. **タスク番号**: フェーズ番号.グループ番号.タスク順番(例:1.1.1)
119
+ 2. **タスク内容**:
120
+ - 具体的な実装内容を箇条書きで記載
121
+ - 技術的な詳細を含める
122
+ 3. **メタデータ**(イタリック体で記載):
123
+ - `_要件: [番号]_` - 対応する要件番号
124
+ - `_依存関係: [タスク番号 or なし]_` - 依存するタスク
125
+ - `_完了条件: [条件]が通ること_` - テスト可能な完了条件
126
+ - **重要**: シナリオテストタスクでは、requirements.mdの「Story X: 受け入れ基準 X.X-X.X」のように具体的な受け入れ基準番号を明記すること
127
+ 4. **対応設計参照**(太字で記載):
128
+ - `**対応設計:** design.md「[セクション名]」[詳細]`
129
+
130
+ ### フェーズ構成の原則
131
+ 1. **フェーズ1-x**: 基盤構築(データベース、API基盤、UI基盤など)
132
+ - 相互に依存しない基盤は並列実行可能
133
+ - 各フェーズは独立したグループを含む
134
+ 2. **フェーズ完了確認**: 各メジャーフェーズ(1, 2, 3...)の最後に必須
135
+ - 全サブフェーズの完了を確認するタスクを配置
136
+ - 次フェーズへの移行基準を明確に記載
137
+ - 依存関係として全サブフェーズの最終タスクを参照
138
+ 3. **フェーズ2以降**: 機能実装
139
+ - 基盤構築完了を前提とした実装
140
+ - ユーザーストーリー単位でグループ化
141
+
142
+ ### シナリオテストの配置
143
+ - 各要件グループの最後にシナリオテストタスクを配置
144
+ - **完了条件には必ず具体的な受け入れ基準番号を記載**
145
+ - 例: `Story 1の受け入れ基準1.1-1.3を満たすテストが通ること`
146
+ - requirements.mdのユーザーストーリーから受け入れ基準番号を正確に引用
147
+ - 複数の受け入れ基準がある場合は範囲で指定(例: 1.1-1.3)
148
+ - 特定の基準のみの場合は個別指定(例: 1.1(メール送信))
149
+ - E2Eテストとして実装
150
+
151
+ ## 分析プロセス
152
+
153
+ 1. **初期スキャン(必須)**:
154
+ - 必ず仕様ディレクトリ内のrequirements.mdを最初に読み込む
155
+ - 次にdesign.mdを読み込んで技術設計を抽出
156
+ - その他の関連ファイルがあれば全て読み込む
157
+ - **重要**: これらのファイルの内容を基にタスクを生成するため、読み込みは必須
158
+
159
+ 2. **要件マッピング**:
160
+ - requirements.mdのユーザーストーリーを識別
161
+ - 各ストーリーの受け入れ基準を抽出し、番号を正確に記録
162
+ - 受け入れ基準の番号体系を理解(例: Story 1の基準1.1, 1.2, 1.3)
163
+ - 各基準の内容(Given-When-Then)を把握
164
+ - design.mdの対応するセクションをマッピング
165
+
166
+ 3. **タスク生成**:
167
+ - 設計書のシーケンス図、API仕様、データベース設計を基にタスクを作成
168
+ - 各タスクをdesign.mdの該当セクションと紐付け
169
+ - テストタスクを各グループの最後に配置
170
+
171
+ 4. **依存関係の最適化**:
172
+ - データベース、API、UI層を並列化可能に分離
173
+ - 機能実装は基盤完了後に配置
174
+
175
+ ## 品質チェック
176
+
177
+ タスク一覧を最終化する前に以下を確認:
178
+ - すべてのユーザーストーリーに対応するタスクグループがある
179
+ - 各タスクがdesign.mdの���クションを参照している
180
+ - シナリオテストが各グループに含まれている
181
+ - **フェーズ完了確認タスクが各メジャーフェーズの最後に配置されている**
182
+ - **次フェーズ移行基準が明確に記載されている**
183
+ - 依存関係が明確で循環していない
184
+ - 並列実行の機会が最大化されている
185
+ - 完了条件がテスト可能である
186
+ - タスク粒度が1〜4時間程度である
187
+
188
+ ## 特別な考慮事項
189
+
190
+ - 要件が不明確な場合は、調査タスクを作成
191
+ - データベースマイグレーションは必ず最初のフェーズに配置
192
+ - Stripe統合などの外部サービス連携は専用フェーズに分離
193
+ - セキュリティとパフォーマンスのテストを適切に配置
194
+ - ドキュメント更新タスクを最終フェーズに含める
195
+
196
+ タスクの説明は常に日本語で記述し、技術用語(API、Database、Stripe等)は英語のまま保持してください。
.claude/commands/spec-create.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description: "タスクの仕様書(requirements.md、design.md、tasks.md)を段階的に作成・修正するワークフローを実行します。ARGUMENTS: タスク内容の説明またはAsanaタスクURL(必須)、既存仕様書のパス(オプション)"
3
+ allowed-tools: Task, Read, Write, Edit, MultiEdit, Bash, Grep, Glob, TodoRead, TodoWrite, mcp__asana__*, mcp__figma_dev_mode__*
4
+ ---
5
+
6
+ # タスク仕様書作成コマンド
7
+
8
+ ## あなたの役割
9
+ プロダクト開発のシニアテクニカルアーキテクト兼シニアプロダクトエンジニアとして、ATDD(受け入れテスト駆動開発)に基づく仕様書を段階的に作成します。
10
+
11
+ ## タスク管理
12
+ TodoWriteツールを使用して全体の進捗を可視化し、ユーザーに現在の状況を明確に伝えます:
13
+ - 各仕様書作成フェーズ(requirements.md、design.md、tasks.md)をトップレベルタスクとして管理
14
+ - エージェント起動前にタスクを「in_progress」に更新
15
+ - エージェント完了後に「completed」に更新
16
+ - ユーザー承認待ちの状態も明示的に表示
17
+
18
+ ## 実行手順
19
+
20
+ ### 1. 外部リソースの確認
21
+
22
+ **AsanaタスクURL**の場合:
23
+ - AsanaMCPでタスク情報を取得(タイトル、説明、カスタムフィールド)
24
+ - タスクIDから適切なディレクトリ名を生成
25
+
26
+ **FigmaURL**が含まれる場合:
27
+ - FigmaDevModeMCPでデザイン分析
28
+ - UI要件、コンポーネント仕様、デザイントークンを抽出
29
+
30
+ ### 2. 作業ディレクトリの決定
31
+ - パス指定あり → 指定ディレクトリを使用
32
+ - パス指定なし → `/docs/specs/tasks/{domain}/{YYYYMMDD}-{domain}-{feature}/` で自動作成
33
+
34
+ ### 3. 段階的仕様書作成
35
+ 各段階でユーザー承認を得てから次へ進行:
36
+
37
+ 1. **requirements.md** (要件定義書)
38
+ - spec-requirements-generatorエージェントで作成
39
+ - エージェント内で既存コードの分析を実施
40
+ - ATDD形式のユーザーストーリーと受け入れ基準
41
+
42
+ 2. **design.md** (設計書)
43
+ - spec-design-generatorエージェントで作成
44
+ - エージェント内で既存アーキテクチャの調査を実施
45
+ - 技術アーキテクチャとデータモデル
46
+
47
+ 3. **tasks.md** (タスク一覧)
48
+ - spec-tasks-generatorエージェントで作成
49
+ - エージェント内で実装の影響範囲を分析
50
+ - 実装タスクの分解と依存関係
51
+
52
+ ### 4. 既存ファイル処理
53
+ - 既存ファイルは内容確認後に次段階へ進行
54
+ - 修正指示がある場合のみ該当エージェントで再生成
55
+
56
+ ### 5. 成果物の構成
57
+ /docs/specs/tasks/
58
+ └── {domain}/
59
+ └── {YYYYMMDD}-{domain}-{feature}/
60
+ ├── requirements.md # 要件定義書(ATDD形式)
61
+ ├── design.md # 設計書(技術詳細)
62
+ └── tasks.md # タスク一覧(実装手順)
63
+
64
+ ## 重要な原則
65
+ - 段階的開発:各フェーズの承認を必須
66
+
67
+ 実行を開始します...
.claude/commands/start-dev.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description: "ローカル開発環境を起動します。環境構築済みの場合に使用"
3
+ allowed-tools: Bash
4
+ ---
5
+
6
+ # ローカル開発環境起動コマンド
7
+
8
+ ## コマンドの目的
9
+
10
+ 既に環境構築が完了している環境でローカル開発環境を起動します。
11
+
12
+ ## 実行手順
13
+
14
+ ### 1. Docker サービス起動
15
+ ```bash
16
+ echo "=== Docker サービス起動 ==="
17
+ docker-compose up -d
18
+ echo "✅ Docker起動完了"
19
+ ```
20
+
21
+ ### 2. 作業ディレクトリ確認と依存関係インストール
22
+ ```bash
23
+ echo "=== 作業ディレクトリ確認 ==="
24
+ pwd
25
+ echo "✅ 現在のディレクトリ: $(pwd)"
26
+
27
+ echo "=== アプリケーションディレクトリ移動 ==="
28
+ cd tca-member-app
29
+ echo "✅ 現在のディレクトリ: $(pwd)"
30
+
31
+ echo "=== 依存関係インストール ==="
32
+ npm install
33
+ echo "✅ npm install完了"
34
+ ```
35
+
36
+ ### 3. 既存プロセス終了とログクリア
37
+ ```bash
38
+ echo "=== 既存プロセス終了とログクリア ==="
39
+ echo "=== アプリケーションディレクトリ移動 ==="
40
+ cd tca-member-app
41
+ echo "" > dev.log
42
+ echo "既存のNext.jsプロセスを終了中..."
43
+ pkill -f "npm run dev" 2>/dev/null || true
44
+ pkill -f "next-server" 2>/dev/null || true
45
+ pkill -f "next dev" 2>/dev/null || true
46
+ sleep 2
47
+ echo "✅ プロセス終了とログクリア完了"
48
+ ```
49
+
50
+ ### 4. Next.js アプリケーション起動
51
+ ```bash
52
+ echo "=== Next.js サーバー起動 ==="
53
+ cd tca-member-app
54
+ npm run dev > dev.log 2>&1 &
55
+ echo "✅ Next.jsサーバー起動中..."
56
+ echo "起動ログ: tail -f tca-member-app/dev.log で確認可能"
57
+ ```
58
+
59
+ ### 5. 起動確認
60
+ ```bash
61
+ echo "=== 起動確認 ==="
62
+ sleep 15
63
+ if curl -s http://localhost:3000 > /dev/null 2>&1; then
64
+ echo "✅ Next.js App: 起動完了 (http://localhost:3000)"
65
+ else
66
+ echo "⏳ サーバーはまだ起動中です"
67
+ echo "ログを確認してください: tail -f tca-member-app/dev.log"
68
+ fi
69
+
70
+ echo ""
71
+ echo "📋 利用可能なサービス:"
72
+ echo " • PostgreSQL: localhost:25432"
73
+ echo " • Mailpit: http://localhost:8025"
74
+ echo " • Next.js App: http://localhost:3000"
75
+ echo ""
76
+ ```
.claude/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1cf71fbce3034327edec12c205cebef0508ab856fc6fa3e71c6c43e0545819e8
3
+ size 1496
.dockerignore ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # Next.js
8
+ .next/
9
+ out/
10
+
11
+ # Production
12
+ build
13
+ dist
14
+
15
+ # Environment variables
16
+ .env
17
+ .env.local
18
+ .env.development.local
19
+ .env.test.local
20
+ .env.production.local
21
+
22
+ # Logs
23
+ logs
24
+ *.log
25
+ dev.log
26
+
27
+ # Runtime data
28
+ pids
29
+ *.pid
30
+ *.seed
31
+ *.pid.lock
32
+
33
+ # Coverage directory used by tools like istanbul
34
+ coverage/
35
+
36
+ # nyc test coverage
37
+ .nyc_output
38
+
39
+ # Dependency directories
40
+ jspm_packages/
41
+
42
+ # Optional npm cache directory
43
+ .npm
44
+
45
+ # Optional REPL history
46
+ .node_repl_history
47
+
48
+ # Output of 'npm pack'
49
+ *.tgz
50
+
51
+ # Yarn Integrity file
52
+ .yarn-integrity
53
+
54
+ # dotenv environment variables file
55
+ .env
56
+
57
+ # IDE
58
+ .vscode/
59
+ .idea/
60
+ *.swp
61
+ *.swo
62
+
63
+ # OS
64
+ .DS_Store
65
+ Thumbs.db
66
+
67
+ # Git
68
+ .git
69
+ .gitignore
70
+
71
+ # Testing
72
+ coverage/
73
+ .nyc_output/
74
+ test-results/
75
+ playwright-report/
76
+
77
+ # Documentation
78
+ docs/
79
+ *.md
80
+ !README.md
81
+
82
+ # Development files
83
+ dev.log
84
+ *.log
85
+
86
+ # Cache
87
+ .cache/
88
+ .temp/
89
+ .tmp/
.env.example ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gradio API Token (HuggingFace)
2
+ GRADIO_TOKEN=hf_your_token_here
3
+
4
+ # OpenAI API Key (Optional - for local development)
5
+ OPENAI_API_KEY=sk-your-api-key-here
6
+
7
+ # Environment
8
+ NODE_ENV=development
9
+
10
+ # Server Port
11
+ PORT=3200
12
+
13
+ # Feature Toggles (true/false)
14
+ # 開発・検証環境用のボタンを表示するかどうか
15
+ NEXT_PUBLIC_SHOW_DEV_BUTTONS=true
.gitattributes CHANGED
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ *.json filter=lfs diff=lfs merge=lfs -text
38
+ public/dummy/get_check_url.json filter=lfs diff=lfs merge=lfs -text
39
+ public/dummy/*.png filter=lfs diff=lfs merge=lfs -text
40
+ *.png filter=lfs diff=lfs merge=lfs -text
41
+ *.jpg filter=lfs diff=lfs merge=lfs -text
42
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
43
+ *.svg filter=lfs diff=lfs merge=lfs -text
44
+ *.xlsx filter=lfs diff=lfs merge=lfs -text
45
+ *.pptx filter=lfs diff=lfs merge=lfs -text
.github/DEPLOYMENT.md ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Deployment Workflows
2
+
3
+ This repository includes two GitHub workflows for deploying your Next.js application to separate Hugging Face Spaces:
4
+
5
+ 1. **`deploy-huggingface.yml`** - Production deployment (main branch)
6
+ 2. **`deploy-dev.yml`** - Development deployment (dev branch)
7
+
8
+ ## 🚀 Quick Setup
9
+
10
+ ### 1. Create Required Secrets
11
+
12
+ In your GitHub repository, go to **Settings** → **Secrets and variables** → **Actions** and add the following secrets:
13
+
14
+ | Secret Name | Description | Example |
15
+ | -------------------- | ------------------------------------------- | ------------------------------- |
16
+ | `HF_TOKEN` | Your Hugging Face access token | `hf_xxxxxxxxxxxxxxxxxxxxxxxxxx` |
17
+ | `HF_USERNAME` | Your Hugging Face username | `your-username` |
18
+ | `HF_SPACE_NAME_PROD` | Name of your production Hugging Face Space | `my-awesome-app` |
19
+ | `HF_SPACE_NAME_DEV` | Name of your development Hugging Face Space | `my-awesome-app-dev` |
20
+
21
+ ### 2. Get Your Hugging Face Token
22
+
23
+ 1. Go to [Hugging Face Settings](https://huggingface.co/settings/tokens)
24
+ 2. Create a new token with **Write** permissions
25
+ 3. Copy the token and add it as `HF_TOKEN` secret
26
+
27
+ ## 📋 Workflow Overview
28
+
29
+ ### Production Workflow (`deploy-huggingface.yml`)
30
+
31
+ **Triggers:**
32
+
33
+ - Push to `main` branch
34
+ - Merged pull requests to `main`
35
+
36
+ **Deploys to:** Space defined in `HF_SPACE_NAME_PROD`
37
+
38
+ **Features:**
39
+
40
+ - ✅ Stable production environment
41
+ - 🚀 Blue/purple theme
42
+ - 📊 Production-ready builds
43
+
44
+ ### Development Workflow (`deploy-dev.yml`)
45
+
46
+ **Triggers:**
47
+
48
+ - Push to `dev` branch
49
+ - Merged pull requests to `dev`
50
+
51
+ **Deploys to:** Space defined in `HF_SPACE_NAME_DEV`
52
+
53
+ **Features:**
54
+
55
+ - 🧪 Testing environment for new features
56
+ - 🔶 Orange/red theme to indicate development
57
+ - 🚧 May contain unstable features
58
+
59
+ ## 🏗️ Project Structure Requirements
60
+
61
+ Your Next.js project should have:
62
+
63
+ ```
64
+ ├── package.json # Node.js dependencies
65
+ ├── next.config.ts # Next.js configuration
66
+ ├── Dockerfile # Docker configuration (optional)
67
+ ├── app/ # Next.js app directory
68
+ ├── components/ # React components
69
+ ├── public/ # Static assets
70
+ └── .github/
71
+ └── workflows/
72
+ ├── deploy-huggingface.yml
73
+ └── deploy-huggingface-advanced.yml
74
+ ```
75
+
76
+ ## 🔧 Configuration
77
+
78
+ ### Next.js Configuration
79
+
80
+ Ensure your `next.config.ts` includes:
81
+
82
+ ```typescript
83
+ const nextConfig: NextConfig = {
84
+ output: 'standalone', // Required for Docker deployment
85
+ images: {
86
+ unoptimized: true, // Recommended for Hugging Face Spaces
87
+ },
88
+ };
89
+ ```
90
+
91
+ ### Docker Configuration
92
+
93
+ The workflows automatically generate an optimized Dockerfile, but you can customize it by modifying the Dockerfile creation step in the workflow files.
94
+
95
+ ## 🌍 Environment Management
96
+
97
+ ### Separate Environments
98
+
99
+ - **Production**:
100
+
101
+ - Triggered by pushes to `main` branch
102
+ - Deploys to: `HF_SPACE_NAME_PROD` (e.g., `my-app`)
103
+ - Theme: Blue/purple 🚀
104
+ - Stable, tested features only
105
+
106
+ - **Development**:
107
+ - Triggered by pushes to `dev` branch
108
+ - Deploys to: `HF_SPACE_NAME_DEV` (e.g., `my-app-dev`)
109
+ - Theme: Orange/red 🧪
110
+ - Testing new features and changes
111
+
112
+ ### Benefits of Separate Spaces
113
+
114
+ - ✅ Test changes in development before production
115
+ - ✅ Keep production environment stable
116
+ - ✅ Easy to identify which environment you're viewing
117
+ - ✅ Independent deployment cycles
118
+
119
+ ### Custom Environment Variables
120
+
121
+ Add environment variables to your Hugging Face Space:
122
+
123
+ 1. Go to your Space settings
124
+ 2. Add environment variables in the "Variables and secrets" section
125
+ 3. Restart your Space
126
+
127
+ ## 🚨 Troubleshooting
128
+
129
+ ### Common Issues
130
+
131
+ 1. **"Space not found" error**
132
+
133
+ - Check that `HF_USERNAME` and `HF_SPACE_NAME` are correct
134
+ - Ensure your Hugging Face token has write permissions
135
+
136
+ 2. **Build failures**
137
+
138
+ - Check that all dependencies are in `package.json`
139
+ - Verify your Next.js configuration
140
+
141
+ 3. **Docker build issues**
142
+
143
+ - Ensure your app builds successfully locally with `npm run build`
144
+ - Check for any missing environment variables
145
+
146
+ 4. **Permission errors**
147
+ - Verify your Hugging Face token has write access
148
+ - Check that the token hasn't expired
149
+
150
+ ### Debug Mode
151
+
152
+ To enable debug mode, add this to your workflow:
153
+
154
+ ```yaml
155
+ env:
156
+ ACTIONS_STEP_DEBUG: true
157
+ ACTIONS_RUNNER_DEBUG: true
158
+ ```
159
+
160
+ ## 📊 Monitoring Deployments
161
+
162
+ ### Workflow Status
163
+
164
+ - Check the **Actions** tab in your GitHub repository
165
+ - Each deployment shows detailed logs and status
166
+ - Production deployments show `[PROD]` in commit messages
167
+ - Development deployments show `[DEV]` in commit messages
168
+
169
+ ### Hugging Face Space Status
170
+
171
+ - **Production**: `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_PROD_SPACE_NAME`
172
+ - **Development**: `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_DEV_SPACE_NAME`
173
+ - Check the build logs in each Space's "Logs" section
174
+
175
+ ## � Typical Workflow
176
+
177
+ 1. **Development**:
178
+
179
+ - Make changes on `dev` branch
180
+ - Push to automatically deploy to dev space
181
+ - Test features in development environment
182
+
183
+ 2. **Production**:
184
+ - Create PR from `dev` to `main`
185
+ - Review and merge PR
186
+ - Automatically deploys to production space
187
+
188
+ ## 📝 Customization
189
+
190
+ ### Modifying the Workflow
191
+
192
+ You can customize the workflows by:
193
+
194
+ 1. **Changing trigger conditions**: Modify the `on:` section
195
+ 2. **Adding build steps**: Insert additional steps before deployment
196
+ 3. **Custom Dockerfile**: Modify the Dockerfile generation step
197
+ 4. **Environment variables**: Add custom environment variables
198
+
199
+ ### Example Customizations
200
+
201
+ ```yaml
202
+ # Deploy only on tags
203
+ on:
204
+ push:
205
+ tags:
206
+ - 'v*'
207
+
208
+ # Add custom build step
209
+ - name: Run tests
210
+ run: npm test
211
+
212
+ # Custom environment variables
213
+ env:
214
+ CUSTOM_VAR: ${{ secrets.CUSTOM_VAR }}
215
+ ```
216
+
217
+ ## 🆘 Support
218
+
219
+ If you encounter issues:
220
+
221
+ 1. Check the GitHub Actions logs
222
+ 2. Review the Hugging Face Space build logs
223
+ 3. Verify all secrets are properly set
224
+ 4. Ensure your Next.js app builds locally
225
+
226
+ For more help:
227
+
228
+ - [Hugging Face Spaces Documentation](https://huggingface.co/docs/hub/spaces)
229
+ - [GitHub Actions Documentation](https://docs.github.com/en/actions)
230
+ - [Next.js Deployment Documentation](https://nextjs.org/docs/deployment)
.github/actions/create-release/action.yml ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 'Create Release with Tag'
2
+ description: 'Creates a Git tag and GitHub Release with categorized PR notes'
3
+ inputs:
4
+ branch:
5
+ description: 'Branch name (e.g., main, stg)'
6
+ required: true
7
+ tag-prefix:
8
+ description: 'Tag prefix (e.g., main-, stg-)'
9
+ required: true
10
+ prerelease:
11
+ description: 'Whether this is a pre-release (true/false)'
12
+ required: true
13
+ release-name-prefix:
14
+ description: 'Release name prefix (e.g., Production Release, Staging Pre-release)'
15
+ required: true
16
+
17
+ runs:
18
+ using: 'composite'
19
+ steps:
20
+ - name: Create Tag and Release
21
+ uses: actions/github-script@v7
22
+ with:
23
+ script: |
24
+ const now = new Date();
25
+ const year = now.getFullYear();
26
+ const month = String(now.getMonth() + 1).padStart(2, '0');
27
+ const day = String(now.getDate()).padStart(2, '0');
28
+ const hours = String(now.getHours()).padStart(2, '0');
29
+ const minutes = String(now.getMinutes()).padStart(2, '0');
30
+ const seconds = String(now.getSeconds()).padStart(2, '0');
31
+ const tagName = `${{ inputs.tag-prefix }}${year}${month}${day}_${hours}${minutes}${seconds}`;
32
+
33
+ // 前回のタグを取得
34
+ let previousTag = null;
35
+ try {
36
+ const tags = await github.rest.repos.listTags({
37
+ owner: context.repo.owner,
38
+ repo: context.repo.repo,
39
+ per_page: 100
40
+ });
41
+ previousTag = tags.data.find(t => t.name.startsWith('${{ inputs.tag-prefix }}'));
42
+ } catch (e) {
43
+ console.log('No previous tags found or error:', e.message);
44
+ }
45
+
46
+ // カテゴリ定義
47
+ const categories = {
48
+ 'feat': { emoji: '✨', title: 'Features', order: 1 },
49
+ 'fix': { emoji: '🐛', title: 'Bug Fixes', order: 2 },
50
+ 'hotfix': { emoji: '🚑', title: 'Hotfixes', order: 3 },
51
+ 'refactor': { emoji: '♻️', title: 'Refactoring', order: 4 },
52
+ 'docs': { emoji: '📝', title: 'Documentation', order: 5 },
53
+ 'test': { emoji: '✅', title: 'Tests', order: 6 },
54
+ 'chore': { emoji: '🔧', title: 'Chore', order: 7 },
55
+ 'other': { emoji: '🔄', title: 'Other Changes', order: 8 }
56
+ };
57
+
58
+ // リリースノート生成
59
+ let releaseBody = '';
60
+ if (previousTag) {
61
+ console.log(`Previous tag found: ${previousTag.name}`);
62
+ try {
63
+ // PRタイトル一覧を取得
64
+ const commits = await github.rest.repos.compareCommits({
65
+ owner: context.repo.owner,
66
+ repo: context.repo.repo,
67
+ base: previousTag.name,
68
+ head: '${{ inputs.branch }}'
69
+ });
70
+
71
+ const prNumbers = new Set();
72
+ for (const commit of commits.data.commits) {
73
+ const match = commit.commit.message.match(/#(\d+)/);
74
+ if (match) prNumbers.add(match[1]);
75
+ }
76
+
77
+ // PRをカテゴリごとに分類
78
+ const categorizedPrs = {};
79
+ for (const num of prNumbers) {
80
+ try {
81
+ const pr = await github.rest.pulls.get({
82
+ owner: context.repo.owner,
83
+ repo: context.repo.repo,
84
+ pull_number: parseInt(num)
85
+ });
86
+ if (pr.data.merged_at) {
87
+ // プレフィックスを抽出(PRタイトル優先、次にブランチ名)
88
+ const titleMatch = pr.data.title.match(/^(feat|fix|hotfix|refactor|docs|test|chore):/);
89
+ const branchMatch = pr.data.head.ref.match(/^(feature|feat|fix|hotfix|refactor|docs|test|chore)\//);
90
+
91
+ let category = 'other';
92
+ if (titleMatch) {
93
+ category = titleMatch[1];
94
+ } else if (branchMatch) {
95
+ // feature/ → feat にマッピング
96
+ category = branchMatch[1] === 'feature' ? 'feat' : branchMatch[1];
97
+ }
98
+
99
+ if (!categorizedPrs[category]) {
100
+ categorizedPrs[category] = [];
101
+ }
102
+ categorizedPrs[category].push(`- ${pr.data.title} (#${num})`);
103
+ }
104
+ } catch (e) {
105
+ console.log(`Failed to get PR #${num}:`, e.message);
106
+ }
107
+ }
108
+
109
+ // カテゴリ順にリリースノートを生成
110
+ const sortedCategories = Object.keys(categorizedPrs).sort((a, b) => {
111
+ return categories[a].order - categories[b].order;
112
+ });
113
+
114
+ const sections = [];
115
+ for (const category of sortedCategories) {
116
+ const cat = categories[category];
117
+ sections.push(`## ${cat.emoji} ${cat.title}\n${categorizedPrs[category].join('\n')}`);
118
+ }
119
+
120
+ releaseBody = sections.length > 0 ? sections.join('\n\n') : 'No PRs merged in this release';
121
+ } catch (e) {
122
+ console.log('Error generating release notes:', e.message);
123
+ releaseBody = 'Error generating release notes';
124
+ }
125
+ } else {
126
+ console.log('No previous tag found - this is the initial release');
127
+ releaseBody = `Initial Release - ${now.toISOString()}`;
128
+ }
129
+
130
+ // タグ作成
131
+ try {
132
+ await github.rest.git.createRef({
133
+ owner: context.repo.owner,
134
+ repo: context.repo.repo,
135
+ ref: `refs/tags/${tagName}`,
136
+ sha: context.sha
137
+ });
138
+ console.log(`Tag created: ${tagName}`);
139
+ } catch (e) {
140
+ console.log('Error creating tag:', e.message);
141
+ throw e;
142
+ }
143
+
144
+ // リリース作成
145
+ try {
146
+ await github.rest.repos.createRelease({
147
+ owner: context.repo.owner,
148
+ repo: context.repo.repo,
149
+ tag_name: tagName,
150
+ name: `${{ inputs.release-name-prefix }} ${tagName}`,
151
+ body: releaseBody,
152
+ prerelease: ${{ inputs.prerelease }}
153
+ });
154
+ console.log(`Release created: ${{ inputs.release-name-prefix }} ${tagName}`);
155
+ } catch (e) {
156
+ console.log('Error creating release:', e.message);
157
+ throw e;
158
+ }
.github/actions/deploy-to-hf/action.yml ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 'Deploy to Hugging Face Space'
2
+ description: 'Deploys application to Hugging Face Space'
3
+ inputs:
4
+ environment:
5
+ description: 'Environment name (dev, test, stg, prod)'
6
+ required: true
7
+ branch:
8
+ description: 'Branch name'
9
+ required: true
10
+ hf-username:
11
+ description: 'Hugging Face username'
12
+ required: true
13
+ hf-token:
14
+ description: 'Hugging Face token'
15
+ required: true
16
+ hf-space-name:
17
+ description: 'Hugging Face Space name'
18
+ required: true
19
+
20
+ runs:
21
+ using: 'composite'
22
+ steps:
23
+ - name: Setup Python for Hugging Face CLI
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: '3.9'
27
+
28
+ - name: Install Hugging Face Hub
29
+ shell: bash
30
+ run: |
31
+ pip install huggingface_hub
32
+ sudo apt-get update && sudo apt-get install -y rsync
33
+
34
+ - name: Validate secrets
35
+ shell: bash
36
+ run: |
37
+ echo "HF_USERNAME: ${{ inputs.hf-username }}"
38
+ echo "HF_SPACE_NAME: ${{ inputs.hf-space-name }}"
39
+ echo "Token length: ${#HF_TOKEN}"
40
+ env:
41
+ HF_TOKEN: ${{ inputs.hf-token }}
42
+
43
+ - name: Create Hugging Face Space README
44
+ shell: bash
45
+ env:
46
+ SPACE_NAME: ${{ inputs.hf-space-name }}
47
+ BRANCH_NAME: ${{ inputs.branch }}
48
+ ENVIRONMENT: ${{ inputs.environment }}
49
+ run: |
50
+ # Environment-specific settings
51
+ case "${ENVIRONMENT}" in
52
+ dev)
53
+ EMOJI="🧪"
54
+ COLOR_TO="yellow"
55
+ ENV_TITLE="Development"
56
+ ENV_DESC="🧪 **This is a development environment** - Features may be unstable"
57
+ ;;
58
+ test)
59
+ EMOJI="🧪"
60
+ COLOR_TO="gray"
61
+ ENV_TITLE="Mock/Testing"
62
+ ENV_DESC="🧪 **This is the mock/testing environment** - Feature branch deployment"
63
+ ;;
64
+ stg)
65
+ EMOJI="🧪"
66
+ COLOR_TO="indigo"
67
+ ENV_TITLE="Staging"
68
+ ENV_DESC="🚧 **This is a staging environment** - Pre-production testing"
69
+ ;;
70
+ prod)
71
+ EMOJI="🚀"
72
+ COLOR_TO="green"
73
+ ENV_TITLE="Production"
74
+ ENV_DESC="✅ **This is the production environment** - Stable and tested features"
75
+ ;;
76
+ esac
77
+
78
+ cat > README.md << EOF
79
+ ---
80
+ title: ${SPACE_NAME}
81
+ emoji: ${EMOJI}
82
+ colorFrom: pink
83
+ colorTo: ${COLOR_TO}
84
+ sdk: docker
85
+ app_port: 7860
86
+ pinned: false
87
+ license: mit
88
+ ---
89
+
90
+ # ${SPACE_NAME} (${ENV_TITLE})
91
+
92
+ ${ENV_DESC}
93
+
94
+ This is a Next.js application deployed to Hugging Face Spaces from the \`${BRANCH_NAME}\` branch.
95
+
96
+ ## Features
97
+
98
+ - Built with Next.js and React
99
+ - Containerized with Docker
100
+ - Automatic deployment from GitHub ${BRANCH_NAME} branch
101
+ - ${ENV_TITLE} environment
102
+
103
+ ## Development
104
+
105
+ To run locally:
106
+
107
+ \`\`\`bash
108
+ npm install
109
+ npm run dev
110
+ \`\`\`
111
+
112
+ The application will be available at http://localhost:3200
113
+
114
+ ---
115
+ **Branch**: ${BRANCH_NAME}
116
+ **Environment**: ${ENV_TITLE}
117
+ **Last Deploy**: $(date)
118
+ EOF
119
+
120
+ - name: Clone or update Hugging Face Space
121
+ shell: bash
122
+ env:
123
+ HF_USERNAME: ${{ inputs.hf-username }}
124
+ HF_TOKEN: ${{ inputs.hf-token }}
125
+ HF_SPACE_NAME: ${{ inputs.hf-space-name }}
126
+ run: |
127
+ # Skip LFS for initial clone to avoid errors
128
+ export GIT_LFS_SKIP_SMUDGE=1
129
+
130
+ # Try to clone the space with authentication
131
+ if git clone https://user:${HF_TOKEN}@huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE_NAME} hf-space; then
132
+ echo "✅ Space exists, updating..."
133
+ cd hf-space
134
+ # Reset any failed LFS state
135
+ git lfs install --skip-smudge
136
+ else
137
+ echo "🆕 Creating new space..."
138
+ # Create space using Python API (more reliable)
139
+ python3 -c "import os; from huggingface_hub import HfApi; api = HfApi(token=os.environ['HF_TOKEN']); api.create_repo(repo_id=f\"{os.environ['HF_USERNAME']}/{os.environ['HF_SPACE_NAME']}\", repo_type='space', space_sdk='docker')" 2>&1 || echo "Space creation failed or already exists"
140
+ sleep 5
141
+ git clone https://user:${HF_TOKEN}@huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE_NAME} hf-space
142
+ cd hf-space
143
+ fi
144
+
145
+ - name: Copy files to Hugging Face Space
146
+ shell: bash
147
+ run: |
148
+ cd hf-space
149
+
150
+ # Remove existing files except .git
151
+ find . -name '.git' -prune -o -type f -exec rm -f {} \;
152
+ find . -name '.git' -prune -o -type d -empty -exec rmdir {} \; 2>/dev/null || true
153
+
154
+ # Copy project files (excluding .git and other unnecessary files)
155
+ rsync -av --exclude='.git' --exclude='hf-space' --exclude='node_modules' --exclude='.next' ../ ./
156
+
157
+ # Copy the built .next directory if it exists
158
+ if [ -d "../.next" ]; then
159
+ cp -r ../.next ./
160
+ fi
161
+
162
+ - name: Configure git in Hugging Face Space
163
+ shell: bash
164
+ run: |
165
+ cd hf-space
166
+ git config user.name "GitHub Actions"
167
+ git config user.email "actions@github.com"
168
+
169
+ - name: Commit and push to Hugging Face Space
170
+ shell: bash
171
+ env:
172
+ ENVIRONMENT: ${{ inputs.environment }}
173
+ run: |
174
+ cd hf-space
175
+ git add .
176
+
177
+ # Check if there are changes to commit
178
+ if git diff --staged --quiet; then
179
+ echo "No changes to commit"
180
+ else
181
+ git commit -m "Deploy from GitHub Actions [${ENVIRONMENT}] - $(date '+%Y-%m-%d %H:%M:%S')"
182
+ git push origin main
183
+ fi
184
+
185
+ - name: Deployment status
186
+ shell: bash
187
+ env:
188
+ ENVIRONMENT: ${{ inputs.environment }}
189
+ HF_USERNAME: ${{ inputs.hf-username }}
190
+ HF_SPACE_NAME: ${{ inputs.hf-space-name }}
191
+ run: |
192
+ case "${ENVIRONMENT}" in
193
+ dev)
194
+ echo "🧪 Development deployment completed!"
195
+ ;;
196
+ test)
197
+ echo "🧪 Mock/Testing deployment completed!"
198
+ ;;
199
+ stg)
200
+ echo "🚧 Staging deployment completed!"
201
+ ;;
202
+ prod)
203
+ echo "🚀 Production deployment completed!"
204
+ ;;
205
+ esac
206
+ echo "Your app should be available at: https://huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE_NAME}"
.github/workflows/deploy-dev.yml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face - Development
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+
8
+ env:
9
+ HF_USERNAME: ${{ secrets.HF_USERNAME }}
10
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
11
+ HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME_DEV }}
12
+
13
+ jobs:
14
+ deploy:
15
+ runs-on: ubuntu-latest
16
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
17
+
18
+ steps:
19
+ - name: Checkout repository
20
+ uses: actions/checkout@v4
21
+ with:
22
+ lfs: true
23
+
24
+ - name: Setup Node.js
25
+ uses: actions/setup-node@v4
26
+ with:
27
+ node-version: '18'
28
+ cache: 'npm'
29
+
30
+ - name: Install dependencies
31
+ run: npm ci
32
+
33
+ - name: Build application
34
+ run: npm run build
35
+
36
+ - name: Deploy to Hugging Face Space
37
+ uses: ./.github/actions/deploy-to-hf
38
+ with:
39
+ environment: dev
40
+ branch: ${{ github.ref_name }}
41
+ hf-username: ${{ env.HF_USERNAME }}
42
+ hf-token: ${{ env.HF_TOKEN }}
43
+ hf-space-name: ${{ env.HF_SPACE_NAME }}
.github/workflows/deploy-prod.yml ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face - Production
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ env:
9
+ HF_USERNAME: ${{ secrets.HF_USERNAME }}
10
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
11
+ HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME_PROD }}
12
+
13
+ jobs:
14
+ deploy:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+ with:
21
+ lfs: true
22
+
23
+ - name: Setup Node.js
24
+ uses: actions/setup-node@v4
25
+ with:
26
+ node-version: '18'
27
+ cache: 'npm'
28
+
29
+ - name: Install dependencies
30
+ run: npm ci
31
+
32
+ - name: Build application
33
+ run: npm run build
34
+
35
+ - name: Deploy to Hugging Face Space
36
+ uses: ./.github/actions/deploy-to-hf
37
+ with:
38
+ environment: prod
39
+ branch: ${{ github.ref_name }}
40
+ hf-username: ${{ env.HF_USERNAME }}
41
+ hf-token: ${{ env.HF_TOKEN }}
42
+ hf-space-name: ${{ env.HF_SPACE_NAME }}
43
+
44
+ - name: Create Tag and Release
45
+ uses: ./.github/actions/create-release
46
+ with:
47
+ branch: main
48
+ tag-prefix: main-
49
+ prerelease: false
50
+ release-name-prefix: Production Release
.github/workflows/deploy-stg.yml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Staging
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - stg
7
+
8
+ env:
9
+ HF_USERNAME: ${{ secrets.HF_USERNAME }}
10
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
11
+ HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME_STG }}
12
+
13
+ jobs:
14
+ deploy:
15
+ runs-on: ubuntu-latest
16
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
17
+
18
+ steps:
19
+ - name: Checkout repository
20
+ uses: actions/checkout@v4
21
+ with:
22
+ lfs: true
23
+
24
+ - name: Setup Node.js
25
+ uses: actions/setup-node@v4
26
+ with:
27
+ node-version: '18'
28
+ cache: 'npm'
29
+
30
+ - name: Install dependencies
31
+ run: npm ci
32
+
33
+ - name: Build application
34
+ run: npm run build
35
+
36
+ - name: Deploy to Hugging Face Space
37
+ uses: ./.github/actions/deploy-to-hf
38
+ with:
39
+ environment: stg
40
+ branch: ${{ github.ref_name }}
41
+ hf-username: ${{ env.HF_USERNAME }}
42
+ hf-token: ${{ env.HF_TOKEN }}
43
+ hf-space-name: ${{ env.HF_SPACE_NAME }}
44
+
45
+ - name: Create Tag and Pre-release
46
+ uses: ./.github/actions/create-release
47
+ with:
48
+ branch: stg
49
+ tag-prefix: stg-
50
+ prerelease: true
51
+ release-name-prefix: Staging Pre-release
.github/workflows/deploy-test.yml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - feature/ui-test
7
+ workflow_dispatch:
8
+
9
+ env:
10
+ HF_USERNAME: ${{ secrets.HF_USERNAME }}
11
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
12
+ HF_SPACE_NAME: FE_Test
13
+
14
+ jobs:
15
+ sync-to-huggingface-mock:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+ with:
21
+ lfs: true
22
+
23
+ - name: Setup Node.js
24
+ uses: actions/setup-node@v4
25
+ with:
26
+ node-version: '18'
27
+ cache: 'npm'
28
+
29
+ - name: Install dependencies
30
+ run: npm ci
31
+
32
+ - name: Build application
33
+ run: npm run build
34
+
35
+ - name: Deploy to Hugging Face Space
36
+ uses: ./.github/actions/deploy-to-hf
37
+ with:
38
+ environment: test
39
+ branch: ${{ github.ref_name }}
40
+ hf-username: ${{ env.HF_USERNAME }}
41
+ hf-token: ${{ env.HF_TOKEN }}
42
+ hf-space-name: ${{ env.HF_SPACE_NAME }}
.gitignore ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ .okta.env
21
+
22
+ # production
23
+ /build
24
+
25
+ # misc
26
+ .DS_Store
27
+ *.pem
28
+
29
+ # debug
30
+ npm-debug.log*
31
+ yarn-debug.log*
32
+ yarn-error.log*
33
+ .pnpm-debug.log*
34
+
35
+ # env files (can opt-in for committing if needed)
36
+ .env
37
+
38
+ # vercel
39
+ .vercel
40
+
41
+ # typescript
42
+ *.tsbuildinfo
43
+ next-env.d.ts
44
+
45
+ # IDE
46
+ .idea/
47
+ .vscode/
48
+
49
+ # logs
50
+ *.log
51
+
52
+ # MCP and development tools
53
+ .kiro/
54
+ .playwright-mcp/
55
+ .serena/
56
+ .mcp.local.json
57
+
58
+ # git worktree
59
+ /worktree/
60
+
61
+ # temporary files
62
+ /tmp/
63
+
64
+ # sample pages (development only)
65
+ /app/cn-sample/
66
+ docs/steering/guideline/
.mcp.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c95b9e188bc5421a0df205e4d5efac8ad62d164e02cfe5324839cd0192cadda5
3
+ size 676
.prettierignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
.prettierrc ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "singleAttributePerLine": false,
5
+ "htmlWhitespaceSensitivity": "css",
6
+ "printWidth": 150,
7
+ "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"],
8
+ "tailwindFunctions": ["clsx", "cn"],
9
+ "tabWidth": 2,
10
+ "overrides": [
11
+ {
12
+ "files": "**/*.yml",
13
+ "options": {
14
+ "tabWidth": 2
15
+ }
16
+ }
17
+ ]
18
+ }
AGENTS.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+ - `app/` hosts Next.js routes, layouts, and server actions for the web UI.
5
+ - `components/`, `hooks/`, `store/`, and `utils/` group reusable UI pieces, custom hooks, Zustand stores, and shared helpers.
6
+ - `services/`, `api-client/`, and `server/` encapsulate API integrations, background jobs, and Hono handlers; keep external calls here.
7
+ - `public/` stores static assets, while `docs/` and `schema/` capture product and API documentation.
8
+ - Tests and fixtures live under `test/`; mirror the runtime folder structure when adding coverage.
9
+
10
+ ## Build, Test, and Development Commands
11
+ - `npm run dev` boots the local Next.js app on port 3200; use `npm run dev:restart` when you need a clean background process.
12
+ - `npm run build` compiles the production bundle, and `npm start` serves the build output.
13
+ - `npm run lint` runs ESLint with the Next.js config; `npm run lintfix` chains Prettier and ESLint for autofixes.
14
+ - `npm test`, `npm run test:watch`, and `npm run test:ui` execute Vitest in batch, watch, or UI mode.
15
+ - Use `npm run generate:dummy-screenshots` or `npm run prepare:dummy-images` to refresh design placeholders referenced in demos.
16
+
17
+ ## Coding Style & Naming Conventions
18
+ - TypeScript and modern React patterns are expected; default to server components unless client interactivity is required.
19
+ - Formatting is enforced by Prettier (2-space indentation, single quotes, trailing commas) and ESLint; run lint before committing.
20
+ - Name React components and Zustand stores in PascalCase, hooks with the `use` prefix, and internal helpers in camelCase.
21
+ - Keep import paths absolute via the `@` alias rather than deep relative chains, and group CSS or Tailwind utilities near their components.
22
+
23
+ ## Testing Guidelines
24
+ - Write unit and integration specs with Vitest; locate files beside the code (`*.test.ts[x]`) or under `test/` mirroring the module path.
25
+ - Mock external services through helpers in `services/` to avoid real network calls, and assert both success and error flows.
26
+ - Run `npm test` plus `npm run lint` before pushing; add snapshots only when the UI surface is stable.
27
+
28
+ ## Commit & Pull Request Guidelines
29
+ - Follow Conventional Commits (`feat:`, `fix:`, `chore:`) as in the git log; include localization context when touching Japanese copy.
30
+ - Each pull request should link the relevant issue, describe behavior changes, note required environment updates, and attach UI screenshots or recordings.
31
+ - Confirm CI passes locally (`npm run lint`, `npm test`, `npm run build`) and mention any skipped checks or follow-up tasks in the description.
32
+
33
+ ## Configuration Tips
34
+ - Secrets live in `.env.local`; never commit credentials or API tokens—consult `api-config.json` for expected keys.
35
+ - Update documentation in `docs/` when endpoints or flows change, and surface breaking environment changes in the pull request summary.
CLAUDE.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Claude Project Settings
2
+
3
+ ## 言語設定
4
+
5
+ すべての回答は日本語で行ってください。
6
+
7
+
8
+ ## 開発サーバー実行方法
9
+
10
+ 開発サーバーを起動する際は、以下のコマンドを使用してください:
11
+
12
+ ```bash
13
+ npm run dev:restart
14
+ ```
15
+
16
+ このコマンドは自動的に:
17
+
18
+ 1. ポート3200で動作中のプロセスをkill
19
+ 2. 開発サーバーを起動
20
+ 3. ログをdev.logファイルとコンソール両方に出力
21
+
22
+ ## コード編集時の自動フォーマット
23
+
24
+ ファイルを編集した後は、必ずPrettierでフォーマットしてください:
25
+
26
+ - `npx prettier --write <編集したファイル>` - Prettierによる自動フォーマット(import順序の整理も含む)
27
+
28
+ ## ハルシネーション禁止
29
+
30
+ 特別な指示がない限り、推測や憶測に基づいたコード生成は禁止します。不明な点がある場合は必ず確認を求めてください。
31
+
32
+ ## TypeScriptにおけるany型の使用禁止
33
+
34
+ TypeScriptコードにおいて、any型の使用を固く禁じます。型安全性を保つため、必ず適切な型定義を行ってください。
35
+
36
+ ## インポート時のパス指定ルール
37
+
38
+ 相対パスでのインポートを禁止します。必ず以下のパスエイリアスを使用してください:
39
+
40
+ - `@/` - プロジェクトルートからのパス(例:`@/components/ui/button`)
41
+ - 相対パス禁止例:`../../../components/ui/button`、`./utils/helper`
42
+
43
+ 正しい例:
44
+
45
+ ```typescript
46
+ import { Button } from '@/components/ui/button';
47
+ import { useGlobalStore } from '@/store/global';
48
+ import type { SwotData } from '@/schema/pox';
49
+ ```
50
+
51
+ 誤った例(使用禁止):
52
+
53
+ ```typescript
54
+ import { Button } from '../../../components/ui/button';
55
+ import { useGlobalStore } from './store/global';
56
+ import type { SwotData } from '../../schema/pox';
57
+ ```
58
+
59
+ # 開発の流れ
60
+
61
+ ## 開発フロー
62
+
63
+ 1. **コード修正**
64
+ - 必要な機能追加やバグ修正を実施
65
+ - TypeScript型定義を正確に行う(any型禁止)
66
+
67
+ 2. **自動フォーマット**
68
+ - 修正したファイルをPrettierでフォーマット
69
+
70
+ ```bash
71
+ npx prettier --write <編集したファイル>
72
+ ```
73
+
74
+ 3. **動作確認**
75
+ - 下記の「動作確認手順」に従ってPlaywriteMCPやCurlで動作を確認
76
+ - エラーや不具合が発生した場合は修正
77
+
78
+ 4. **エラー対応**
79
+ - ログを確認して問題を特定
80
+ - 修正後、再度動作確認を実施
81
+ - 正常動作するまで3-4を繰り返す
82
+
83
+ 5. **ビルド確認**
84
+ - コミット前に必ずビルドを実行
85
+
86
+ ```bash
87
+ npm run build
88
+ ```
89
+
90
+ - ビルドエラーがある場合は修正してから再度ビルド
91
+ - ビルドが成功したことを確認してからコミット
92
+
93
+ # HTML生成機能の基本動作確認
94
+
95
+ ## 画面リンク集
96
+
97
+ ### メイン機能ページ
98
+ - **トップページ**: `http://localhost:3200/`
99
+ - **リフレッシュモーメンツ選択**: `http://localhost:3200/refresh-moments`
100
+ - **改善案生成結果**: `http://localhost:3200/refresh-moments/improvement-result`
101
+ - **ダミーモード改善案生成結果**: `http://localhost:3200/refresh-moments/improvement-result?dummyMode=true`
102
+
103
+ ### その他のページ
104
+ - **新UIプレビュー**: `http://localhost:3200/updatedui`
105
+
106
+ ## 動作確認手順
107
+
108
+ 1. **開発サーバー起動**
109
+
110
+ ```bash
111
+ npm run dev:restart
112
+ ```
113
+
114
+ 2. **画面アクセス**
115
+ - ブラウザで `http://localhost:3200/refresh-moments` にアクセス
116
+ - モーメントを選択してから `http://localhost:3200/refresh-moments/improvement-result` に遷移
117
+ - **ダミーモード**: `http://localhost:3200/refresh-moments/improvement-result?dummyMode=true` で直接アクセス可能
118
+ - PlaywriteMCPで確認
119
+ - APIのみの修正の場合はCurlでAPI打鍵確認
120
+
121
+ 3. **モーメント選択操作**
122
+ - refresh-moments画面でモーメントを選択
123
+ - 改善案生成ボタンをクリックして improvement-result 画面に遷移
124
+
125
+ 4. **ログ確認**
126
+ ```bash
127
+ tail -f dev.log
128
+ ```
129
+
130
+ ## 動作確認ポイント
131
+
132
+ ### 正常な処理フロー
133
+
134
+ 1. スクリーンショット取得完了
135
+ 2. FV HTML生成開始
136
+ 3. Adobe Firefly画像生成開始(バックグラウンド)
137
+ 4. gpt-image-1パイプライン試行(組織未認証の場合失敗)
138
+ 5. 簡易版gpt-image-1フォールバック(403エラーの場合失敗)
139
+ 6. 最終フォールバック:モックHTML表示
140
+
141
+ ### エラーパターン
142
+
143
+ - **gpt-image-1エラー**: `403 Forbidden` - 組織未認証
144
+ - **セグメンテーションエラー**: `TypeError: Cannot read properties of undefined`
145
+ - **画像フォーマットエラー**: `400 Bad Request` - PNG変換で解決済み
146
+
147
+ ### 成功判定
148
+
149
+ - 画面に「FVとコンテンツを生成中...」が表示される
150
+ - Adobe Fireflyバッチ処理が開始される
151
+ - ログに `[CONTENTS HTML] Images generated successfully` が出力さ���る(2分程度)
152
+
153
+ # 重要な指示のリマインダー
154
+
155
+ 要求されたことだけを実行してください。それ以上でも以下でもありません。
156
+ 目的達成のために絶対に必要でない限り、ファイルを作成しないでください。
157
+ 新規ファイルを作成するよりも、既存ファイルの編集を常に優先してください。
158
+ ドキュメントファイル(\*.md)やREADMEファイルを自発的に作成しないでください。ユーザーから明示的に要求された場合のみドキュメントファイルを作成してください。
159
+
160
+ ## Serena MCP使用ガイドライン
161
+
162
+ - **既存コード解析**や**アーキテクチャ理解**が必要な場面では原則Serena MCPを使用してコードの解析を行う
163
+ - **Serena推奨場面**:
164
+ - コードベース全体の構造理解
165
+ - シンボル間の参照関係調査
166
+ - クラス・関数・変数の使用箇所特定
167
+ - ファイル間の依存関係分析
168
+ - リファクタリングや機能追加の影響範囲調査
169
+ - 設計パターンの実装箇所探索
170
+ - バグの原因となる関連コード特定
171
+ - **通常ツール推奨場面**:
172
+ - 単純なファイル読み込み・編集
173
+ - 既知のファイル・関数への直接的な変更
174
+ - シンプルな文字列検索・置換
175
+ - Serena MCPの機能を活用して効率的で正確なコード分析を心がける
176
+
177
+ # Gradio API打鍵方法
178
+
179
+ Gradio APIのエンドポイントを直接確認する場合は、以下の方法を使用してください:
180
+
181
+ ```javascript
182
+ const { Client } = require('@gradio/client');
183
+
184
+ async function testGradioAPI() {
185
+ try {
186
+ const client = await Client.connect('dentsudigital/mugenAILP_dev', {
187
+ hf_token: process.env.GRADIO_TOKEN, // .envファイルのトークンを使用
188
+ });
189
+
190
+ // 例: get_proposal_prediction_dummyエンドポイントの呼び出し
191
+ const result = await client.predict('/get_proposal_prediction_dummy', {});
192
+ console.log(JSON.stringify(result.data, null, 2));
193
+ } catch (error) {
194
+ console.error('Error:', error.message);
195
+ }
196
+ }
197
+
198
+ testGradioAPI();
199
+ ```
200
+
201
+ **注意事項:**
202
+
203
+ - Gradio APIのレスポンスは通常`result.data`に配列形式で格納される
204
+ - 配列の最初の要素が実際のレスポンスデータであることが多い
205
+ - HuggingFaceのトークン(`hf_`で始まる)が必要な場合がある
Dockerfile ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker.io/docker/dockerfile:1
2
+
3
+ # Force cache invalidation for HuggingFace Spaces
4
+ ARG CACHEBUST=1
5
+
6
+ FROM node:slim AS base
7
+
8
+ # Install Python and build tools for native dependencies
9
+ # Install Chromium and dependencies for Puppeteer
10
+ RUN apt-get update && apt-get install -y \
11
+ python3 make g++ \
12
+ chromium \
13
+ fonts-noto-cjk \
14
+ fonts-liberation \
15
+ fonts-freefont-ttf \
16
+ ca-certificates \
17
+ libnss3 \
18
+ libatk-bridge2.0-0 \
19
+ libdrm2 \
20
+ libxcomposite1 \
21
+ libxdamage1 \
22
+ libxfixes3 \
23
+ libxrandr2 \
24
+ libgbm1 \
25
+ libxkbcommon0 \
26
+ libasound2 \
27
+ libatspi2.0-0 \
28
+ libgtk-3-0 \
29
+ libglib2.0-0 \
30
+ libxss1 \
31
+ libgconf-2-4 \
32
+ && rm -rf /var/lib/apt/lists/*
33
+
34
+ # Set Puppeteer to use installed Chromium
35
+ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
36
+ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
37
+
38
+ # Install dependencies only when needed
39
+ FROM base AS deps
40
+ WORKDIR /app
41
+
42
+ # Install dependencies based on the preferred package manager
43
+ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
44
+ RUN \
45
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
46
+ elif [ -f package-lock.json ]; then npm ci --include=optional; \
47
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
48
+ else echo "Lockfile not found." && exit 1; \
49
+ fi
50
+
51
+ # Install the correct platform-specific packages based on architecture
52
+ RUN if [ "$(uname -m)" = "x86_64" ]; then \
53
+ npm install lightningcss-linux-x64-gnu@1.29.1 --no-save; \
54
+ elif [ "$(uname -m)" = "aarch64" ]; then \
55
+ npm install lightningcss-linux-arm64-gnu@1.29.1 --no-save; \
56
+ fi && \
57
+ npm rebuild @tailwindcss/oxide --update-binary
58
+
59
+
60
+ # Rebuild the source code only when needed
61
+ FROM base AS builder
62
+ WORKDIR /app
63
+ # Clear all cache to avoid build issues
64
+ RUN rm -rf .next node_modules/.cache
65
+
66
+ COPY --from=deps /app/node_modules ./node_modules
67
+
68
+ COPY . .
69
+
70
+ # Rebuild native dependencies for the builder stage
71
+ # Rebuild sharp and other native modules for the platform
72
+ RUN if [ "$(uname -m)" = "x86_64" ]; then \
73
+ npm install lightningcss-linux-x64-gnu@1.29.1 --no-save; \
74
+ elif [ "$(uname -m)" = "aarch64" ]; then \
75
+ npm install lightningcss-linux-arm64-gnu@1.29.1 --no-save; \
76
+ fi && \
77
+ npm rebuild sharp && \
78
+ npm rebuild @tailwindcss/oxide --update-binary
79
+
80
+ # Clean up build artifacts
81
+ RUN rm -rf .next/cache node_modules/.cache
82
+
83
+ # Build arguments for API keys (passed from HuggingFace Spaces environment)
84
+ ARG OPENAI_TEMPLATE_AI_DEV
85
+ ARG OPENAI_GENERATE_FV_AI
86
+ ARG CLAUDE_TEMPLATE_AI_DEV
87
+ ARG ANTHROPIC_API_KEY
88
+ ARG GOOGLE_API_KEY
89
+ ARG NEXT_PUBLIC_APP_ENV
90
+ ARG OKTA_OAUTH2_ISSUER
91
+ ARG OKTA_OAUTH2_CLIENT_ID
92
+ ARG OKTA_OAUTH2_CLIENT_SECRET
93
+ ARG SECRET
94
+
95
+ # Set environment variables for build time
96
+ ENV NEXT_TELEMETRY_DISABLED=1
97
+ ENV NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV
98
+ ENV OPENAI_TEMPLATE_AI_DEV=$OPENAI_TEMPLATE_AI_DEV
99
+ ENV OPENAI_GENERATE_FV_AI=$OPENAI_GENERATE_FV_AI
100
+ ENV CLAUDE_TEMPLATE_AI_DEV=$CLAUDE_TEMPLATE_AI_DEV
101
+ ENV ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY
102
+ ENV GOOGLE_API_KEY=$GOOGLE_API_KEY
103
+ ENV OKTA_OAUTH2_ISSUER=$OKTA_OAUTH2_ISSUER
104
+ ENV OKTA_OAUTH2_CLIENT_ID=$OKTA_OAUTH2_CLIENT_ID
105
+ ENV OKTA_OAUTH2_CLIENT_SECRET=$OKTA_OAUTH2_CLIENT_SECRET
106
+ ENV SECRET=$SECRET
107
+
108
+ # Set memory limit for build process
109
+ ENV NODE_OPTIONS="--max-old-space-size=2048"
110
+ RUN \
111
+ if [ -f yarn.lock ]; then yarn run build; \
112
+ elif [ -f package-lock.json ]; then npm run build; \
113
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
114
+ else echo "Lockfile not found." && exit 1; \
115
+ fi
116
+
117
+ # Remove build cache to reduce image size (keep only necessary runtime files)
118
+ RUN rm -rf .next/cache/webpack .next/cache/swc node_modules/.cache
119
+
120
+ # Production image, copy all the files and run next
121
+ FROM base AS runner
122
+ WORKDIR /app
123
+
124
+ ENV NODE_ENV=production
125
+ # Disable Next.js telemetry and set cache to writable location
126
+ ENV NEXT_TELEMETRY_DISABLED=1
127
+ ENV TMPDIR=/tmp
128
+
129
+ # Runtime environment variables for API keys
130
+ ARG OPENAI_TEMPLATE_AI_DEV
131
+ ARG OPENAI_GENERATE_FV_AI
132
+ ARG CLAUDE_TEMPLATE_AI_DEV
133
+ ARG ANTHROPIC_API_KEY
134
+ ARG GOOGLE_API_KEY
135
+ ARG NEXT_PUBLIC_APP_ENV
136
+ ARG OKTA_OAUTH2_ISSUER
137
+ ARG OKTA_OAUTH2_CLIENT_ID
138
+ ARG OKTA_OAUTH2_CLIENT_SECRET
139
+ ARG SECRET
140
+
141
+ ENV NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV
142
+ ENV OPENAI_TEMPLATE_AI_DEV=$OPENAI_TEMPLATE_AI_DEV
143
+ ENV OPENAI_GENERATE_FV_AI=$OPENAI_GENERATE_FV_AI
144
+ ENV CLAUDE_TEMPLATE_AI_DEV=$CLAUDE_TEMPLATE_AI_DEV
145
+ ENV ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY
146
+ ENV GOOGLE_API_KEY=$GOOGLE_API_KEY
147
+ ENV OKTA_OAUTH2_ISSUER=$OKTA_OAUTH2_ISSUER
148
+ ENV OKTA_OAUTH2_CLIENT_ID=$OKTA_OAUTH2_CLIENT_ID
149
+ ENV OKTA_OAUTH2_CLIENT_SECRET=$OKTA_OAUTH2_CLIENT_SECRET
150
+ ENV SECRET=$SECRET
151
+
152
+ RUN addgroup --system --gid 1001 nodejs
153
+ RUN adduser --system --uid 1001 nextjs --home /home/nextjs
154
+
155
+ # Copy node_modules for runtime (including Puppeteer)
156
+ COPY --from=builder /app/node_modules ./node_modules
157
+ RUN chown -R nextjs:nodejs ./node_modules
158
+
159
+ # Ensure nextjs user has access to home directory
160
+ RUN chown -R nextjs:nodejs /home/nextjs
161
+
162
+ COPY --from=builder /app/public ./public
163
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
164
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
165
+
166
+ # Copy .next directory but exclude cache to avoid permission issues
167
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/server ./.next/server
168
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/required-server-files.json ./.next/required-server-files.json
169
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/routes-manifest.json ./.next/routes-manifest.json
170
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/build-manifest.json ./.next/build-manifest.json
171
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/package.json ./.next/package.json
172
+ # Copy optional manifest files (may not exist in all builds)
173
+ RUN --mount=type=bind,from=builder,source=/app/.next,target=/tmp/source \
174
+ if [ -f /tmp/source/prerender-manifest.json ]; then cp /tmp/source/prerender-manifest.json ./.next/; fi && \
175
+ if [ -f /tmp/source/react-loadable-manifest.json ]; then cp /tmp/source/react-loadable-manifest.json ./.next/; fi && \
176
+ chown -R nextjs:nodejs ./.next/
177
+
178
+ # Create cache directories and ensure full permissions (combining both approaches)
179
+ RUN mkdir -p /tmp/.next/cache/images && \
180
+ mkdir -p /app/.next/cache/images && \
181
+ chmod -R 777 /tmp && \
182
+ chmod -R 755 /app/.next && \
183
+ chmod -R 777 /app/.next/cache/images && \
184
+ chown -R nextjs:nodejs /tmp/.next && \
185
+ chown -R nextjs:nodejs /app/.next
186
+
187
+ USER nextjs
188
+
189
+ EXPOSE 7860
190
+
191
+ ENV PORT=7860
192
+ ENV HOSTNAME="0.0.0.0"
193
+ # Optimize for HuggingFace free tier
194
+ ENV NODE_OPTIONS="--max-old-space-size=1024"
195
+
196
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,10 +1,39 @@
1
  ---
2
- title: FE Dev
3
- emoji: 🏆
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FE_Dev
3
+ emoji: 🧪
4
+ colorFrom: pink
5
+ colorTo: yellow
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # FE_Dev (Development)
13
+
14
+ 🧪 **This is a development environment** - Features may be unstable
15
+
16
+ This is a Next.js application deployed to Hugging Face Spaces from the `dev` branch.
17
+
18
+ ## Features
19
+
20
+ - Built with Next.js and React
21
+ - Containerized with Docker
22
+ - Automatic deployment from GitHub dev branch
23
+ - Development environment
24
+
25
+ ## Development
26
+
27
+ To run locally:
28
+
29
+ ```bash
30
+ npm install
31
+ npm run dev
32
+ ```
33
+
34
+ The application will be available at http://localhost:3200
35
+
36
+ ---
37
+ **Branch**: dev
38
+ **Environment**: Development
39
+ **Last Deploy**: Fri Oct 31 07:28:41 UTC 2025
analyze-components.mjs ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+
3
+ const config = JSON.parse(fs.readFileSync('api-config.json', 'utf-8'));
4
+
5
+ // get_proposal_cn inputs
6
+ const cnInputs = [153, 108, 133, 68, 69, 237, 240, 1, 247, 288];
7
+ // get_proposal_fv inputs
8
+ const fvInputs = [153, 108, 133, 68, 69, 231, 232, 117, 288, 1];
9
+
10
+ console.log('=== get_proposal_cn Parameters ===\n');
11
+ cnInputs.forEach((id, index) => {
12
+ const component = config.components.find(c => c.id === id + 1); // Components array is 0-indexed but IDs are 1-indexed
13
+ if (component) {
14
+ console.log(`Parameter ${index}: ID=${id}`);
15
+ console.log(` Type: ${component.type}`);
16
+ if (component.props?.label) console.log(` Label: ${component.props.label}`);
17
+ if (component.props?.elem_id) console.log(` Elem ID: ${component.props.elem_id}`);
18
+ if (component.api_info) console.log(` API Info:`, component.api_info);
19
+ console.log();
20
+ }
21
+ });
22
+
23
+ console.log('\n=== get_proposal_fv Parameters ===\n');
24
+ fvInputs.forEach((id, index) => {
25
+ const component = config.components.find(c => c.id === id + 1);
26
+ if (component) {
27
+ console.log(`Parameter ${index}: ID=${id}`);
28
+ console.log(` Type: ${component.type}`);
29
+ if (component.props?.label) console.log(` Label: ${component.props.label}`);
30
+ if (component.props?.elem_id) console.log(` Elem ID: ${component.props.elem_id}`);
31
+ if (component.api_info) console.log(` API Info:`, component.api_info);
32
+ console.log();
33
+ }
34
+ });
35
+
36
+ // Find components with meaningful labels
37
+ console.log('\n=== All Components with Labels ===\n');
38
+ config.components.forEach(c => {
39
+ if (c.props?.label && !c.skip_api) {
40
+ console.log(`ID ${c.id - 1}: ${c.props.label} (${c.type})`);
41
+ }
42
+ });
api-client/gradio-proxy/check-url.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { rpcClient } from '@/api-client/utils/rpc-client';
2
+ import type { CheckUrlRequest, CheckUrlResponse } from '@/schema/gradio-proxy/check-url';
3
+ import { useMutation } from '@tanstack/react-query';
4
+
5
+ /**
6
+ * check_url APIを呼び出すカスタムフック(mutation版)
7
+ * @returns mutationオブジェクト
8
+ */
9
+ export function useCheckUrl() {
10
+ return useMutation({
11
+ mutationFn: async (request: CheckUrlRequest): Promise<CheckUrlResponse> => {
12
+ console.log('[useCheckUrl] API呼び出し開始:', {
13
+ ownUrl: request.ownUrl,
14
+ urlCount: request.urlText.length,
15
+ dummyMode: request.dummyMode,
16
+ });
17
+
18
+ try {
19
+ const response = await rpcClient['gradio-proxy']['check-url'].$post({
20
+ json: request,
21
+ });
22
+
23
+ if (!response.ok) {
24
+ const errorData = (await response.json()) as { message?: string };
25
+ console.error('[useCheckUrl] API エラー:', errorData);
26
+ throw new Error(errorData.message || 'check_url APIの呼び出しに失敗しました');
27
+ }
28
+
29
+ const data = await response.json();
30
+ console.log('[useCheckUrl] API呼び出し成功');
31
+ return data as CheckUrlResponse;
32
+ } catch (error) {
33
+ console.error('[useCheckUrl] API呼び出しエラー:', error);
34
+ throw error;
35
+ }
36
+ },
37
+ });
38
+ }
api-client/gradio-proxy/excel.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import type { ExcelRequest, ExcelResponse } from '@/schema/gradio-proxy/excel';
4
+ import type { FetchQueryOptions } from '@tanstack/react-query';
5
+ import { useQueryClient } from '@tanstack/react-query';
6
+ import { useCallback } from 'react';
7
+
8
+ /**
9
+ * excel APIを呼び出すカスタムフック
10
+ * @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
11
+ */
12
+ export function useExcel() {
13
+ const queryClient = useQueryClient();
14
+
15
+ return useCallback(
16
+ async (request: ExcelRequest): Promise<ExcelResponse> => {
17
+ console.log('[useExcel] API呼び出し開始:', {
18
+ ownUrl: request.ownUrl,
19
+ urlCount: request.urlText.length,
20
+ hasCommonDict: !!request.commonDict,
21
+ hasScoreDict: !!request.scoreDict,
22
+ hasSummary: !!request.summary,
23
+ hasSwot: !!request.swot,
24
+ hasProposalFV: !!request.proposal_fv,
25
+ hasProposalCN: !!request.proposal_cn,
26
+ hasProposalPrediction: !!request.proposal_prediction,
27
+ hasProposalIntent: !!request.proposal_intent,
28
+ dummyMode: request.dummyMode,
29
+ });
30
+
31
+ return queryClient.fetchQuery(
32
+ createQueryOptions<ExcelResponse>({
33
+ queryKey: ['excel', request],
34
+ queryFn: async (): Promise<ExcelResponse> => {
35
+ try {
36
+ const response = await rpcClient['gradio-proxy']['excel'].$post({
37
+ json: request,
38
+ });
39
+
40
+ if (!response.ok) {
41
+ console.error('[useExcel] Response status:', response.status);
42
+ const errorData = (await response.json()) as { message?: string; status?: string };
43
+ console.error('[useExcel] API エラー:', errorData);
44
+ throw new Error(errorData.message || 'excel APIの呼び出しに失敗しました');
45
+ }
46
+
47
+ const data = await response.json();
48
+ console.log('[useExcel] API呼び出し成功');
49
+ return data as ExcelResponse;
50
+ } catch (error) {
51
+ console.error('[useExcel] API呼び出しエラー:', error);
52
+ throw error;
53
+ }
54
+ },
55
+ }) as FetchQueryOptions<ExcelResponse>,
56
+ );
57
+ },
58
+ [queryClient],
59
+ );
60
+ }
api-client/gradio-proxy/pox.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import type { PoxRequest, PoxResponse } from '@/schema/gradio-proxy/pox';
4
+ import type { FetchQueryOptions } from '@tanstack/react-query';
5
+ import { useQueryClient } from '@tanstack/react-query';
6
+ import { useCallback } from 'react';
7
+
8
+ /**
9
+ * pox APIを呼び出すカスタムフック
10
+ * @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
11
+ */
12
+ export function usePox() {
13
+ const queryClient = useQueryClient();
14
+
15
+ return useCallback(
16
+ async (request: PoxRequest): Promise<PoxResponse> => {
17
+ console.log('[usePox] API呼び出し開始:', {
18
+ hasCommonDict: !!request.commonDict,
19
+ hasScoreDict: !!request.scoreDict,
20
+ hasScoreTotal: !!request.score_total,
21
+ dummyMode: request.dummyMode,
22
+ });
23
+
24
+ return queryClient.fetchQuery(
25
+ createQueryOptions<PoxResponse>({
26
+ queryKey: ['pox', request],
27
+ queryFn: async (): Promise<PoxResponse> => {
28
+ try {
29
+ const response = await rpcClient['gradio-proxy']['pox'].$post({
30
+ json: request,
31
+ });
32
+
33
+ if (!response.ok) {
34
+ console.error('[usePox] Response status:', response.status);
35
+ const errorData = (await response.json()) as { message?: string; status?: string };
36
+ console.error('[usePox] API エラー:', errorData);
37
+ throw new Error(errorData.message || 'pox APIの呼び出しに失敗しました');
38
+ }
39
+
40
+ const data = await response.json();
41
+ console.log('[usePox] API呼び出し成功');
42
+ return data as PoxResponse;
43
+ } catch (error) {
44
+ console.error('[usePox] API呼び出しエラー:', error);
45
+ throw error;
46
+ }
47
+ },
48
+ }) as FetchQueryOptions<PoxResponse>,
49
+ );
50
+ },
51
+ [queryClient],
52
+ );
53
+ }
api-client/gradio-proxy/score-step3.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import type { ScoreStep3Request, ScoreStep3Response } from '@/schema/gradio-proxy/score-step3';
4
+ import type { InputData } from '@/types/api';
5
+ import type { FetchQueryOptions } from '@tanstack/react-query';
6
+ import { useQueryClient } from '@tanstack/react-query';
7
+ import { useCallback } from 'react';
8
+
9
+ /**
10
+ * score_step3 APIを呼び出すカスタムフック
11
+ * @returns キャッシュ対応のAPI呼び出し関数(タイムアウト10秒、キャッシュ24時間)
12
+ */
13
+ export function useScoreStep3() {
14
+ const queryClient = useQueryClient();
15
+
16
+ return useCallback(
17
+ async (request: ScoreStep3Request): Promise<ScoreStep3Response> => {
18
+ console.log('[useScoreStep3] API呼び出し開始:', {
19
+ ownUrl: (request.commonDict as InputData)?.own_url,
20
+ urlCount: (request.commonDict as InputData)?.competitor_urls?.length,
21
+ tempImagesCount: request.tempImages ? Object.keys(request.tempImages).length : 0,
22
+ hasCommonDict: !!request.commonDict,
23
+ dummyMode: request.dummyMode,
24
+ });
25
+
26
+ return queryClient.fetchQuery(
27
+ createQueryOptions<ScoreStep3Response>({
28
+ queryKey: ['score-step3', request],
29
+ queryFn: async (): Promise<ScoreStep3Response> => {
30
+ try {
31
+ const response = await rpcClient['gradio-proxy']['score-step3'].$post({
32
+ json: request,
33
+ });
34
+
35
+ if (!response.ok) {
36
+ console.error('[useScoreStep3] Response status:', response.status);
37
+ const errorData = (await response.json()) as { message?: string; status?: string };
38
+ console.error('[useScoreStep3] API エラー:', errorData);
39
+ throw new Error(errorData.message || 'score_step3 APIの呼び出しに失敗しました');
40
+ }
41
+
42
+ const data = await response.json();
43
+ console.log('[useScoreStep3] API呼び出し成功');
44
+ return data as ScoreStep3Response;
45
+ } catch (error) {
46
+ console.error('[useScoreStep3] API呼び出しエラー:', error);
47
+ throw error;
48
+ }
49
+ },
50
+ }) as FetchQueryOptions<ScoreStep3Response>,
51
+ );
52
+ },
53
+ [queryClient],
54
+ );
55
+ }
api-client/gradio-proxy/summary.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import type { SummaryRequest, SummaryResponse } from '@/schema/gradio-proxy/summary';
4
+ import type { FetchQueryOptions } from '@tanstack/react-query';
5
+ import { useQueryClient } from '@tanstack/react-query';
6
+ import { useCallback } from 'react';
7
+
8
+ /**
9
+ * summary APIを呼び出すカスタムフック
10
+ * @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
11
+ */
12
+ export function useSummary() {
13
+ const queryClient = useQueryClient();
14
+
15
+ return useCallback(
16
+ async (request: SummaryRequest): Promise<SummaryResponse> => {
17
+ console.log('[useSummary] API呼び出し開始:', {
18
+ hasCommonDict: !!request.commonDict,
19
+ hasScoreDict: !!request.scoreDict,
20
+ hasScoreTotal: !!request.score_total,
21
+ dummyMode: request.dummyMode,
22
+ });
23
+
24
+ return queryClient.fetchQuery(
25
+ createQueryOptions<SummaryResponse>({
26
+ queryKey: ['summary', request],
27
+ queryFn: async (): Promise<SummaryResponse> => {
28
+ try {
29
+ const response = await rpcClient['gradio-proxy']['summary'].$post({
30
+ json: request,
31
+ });
32
+
33
+ if (!response.ok) {
34
+ console.error('[useSummary] Response status:', response.status);
35
+ const errorData = (await response.json()) as { message?: string; status?: string };
36
+ console.error('[useSummary] API エラー:', errorData);
37
+ throw new Error(errorData.message || 'summary APIの呼び出しに失敗しました');
38
+ }
39
+
40
+ const data = await response.json();
41
+ console.log('[useSummary] API呼び出し成功');
42
+ return data as SummaryResponse;
43
+ } catch (error) {
44
+ console.error('[useSummary] API呼び出しエラー:', error);
45
+ throw error;
46
+ }
47
+ },
48
+ }) as FetchQueryOptions<SummaryResponse>,
49
+ );
50
+ },
51
+ [queryClient],
52
+ );
53
+ }
api-client/gradio-proxy/use-pre-analysis.ts ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ExcelRequest } from '@/schema/gradio-proxy/excel';
2
+ import type { PoxRequest } from '@/schema/gradio-proxy/pox';
3
+ import type { SummaryRequest } from '@/schema/gradio-proxy/summary';
4
+ import type { VisScoreRequest } from '@/schema/gradio-proxy/vis-score';
5
+ import { useResultStore } from '@/store/result';
6
+ import { useStateStore } from '@/store/state';
7
+ import { InputData } from '@/types/api';
8
+ import { useCallback, useMemo, useRef, useState } from 'react';
9
+ import { useExcel } from './excel';
10
+ import { usePox } from './pox';
11
+ import { useScoreStep3 } from './score-step3';
12
+ import { useSummary } from './summary';
13
+ import { useVisScore } from './vis-score';
14
+
15
+ interface UsePreAnalysisParams {
16
+ tempImages: Record<string, string>;
17
+ fvInfos: Record<string, unknown> | null;
18
+ cnInfo: Record<string, unknown> | null;
19
+ commonDict: InputData | null;
20
+ dummyMode?: boolean;
21
+ userEmail: string | null;
22
+ userIdentifier: string;
23
+ }
24
+
25
+ interface ApiStatus {
26
+ duration: number;
27
+ status: 'idle' | 'loading' | 'success' | 'error';
28
+ }
29
+
30
+ export function usePreAnalysis(params: UsePreAnalysisParams) {
31
+ const { tempImages, fvInfos, cnInfo, commonDict, dummyMode = false, userEmail, userIdentifier } = params;
32
+
33
+ // Zustandストアから取得
34
+ const {
35
+ setScoreDict,
36
+ setCommonDict,
37
+ setScoreTotal,
38
+ setUrlCategoryScores,
39
+ setOwnCategoryScores,
40
+ setSummaryData,
41
+ setSummaryBaseData,
42
+ setSwotData75,
43
+ setSwotData78,
44
+ setSwotData79,
45
+ setSwotTable,
46
+ setDownloadData,
47
+ } = useResultStore();
48
+
49
+ const { updateApiStatus: updateStateStoreApiStatus } = useStateStore();
50
+
51
+ // API実行時間追跡
52
+ const apiStartTimesRef = useRef<Record<string, number>>({});
53
+ const [apiStatuses, setApiStatuses] = useState<Record<string, ApiStatus>>({
54
+ scoreStep3: { duration: 0, status: 'idle' },
55
+ visScore: { duration: 0, status: 'idle' },
56
+ summary: { duration: 0, status: 'idle' },
57
+ pox: { duration: 0, status: 'idle' },
58
+ excel: { duration: 0, status: 'idle' },
59
+ });
60
+
61
+ // API呼び出し用フック
62
+ const scoreStep3Api = useScoreStep3();
63
+ const visScoreApi = useVisScore();
64
+ const summaryApi = useSummary();
65
+ const poxApi = usePox();
66
+ const excelApi = useExcel();
67
+
68
+ // ステップごとのデータとエラー状態
69
+ const [scoreStep3Data, setScoreStep3Data] = useState<any>(null);
70
+ const [visScoreData, setVisScoreData] = useState<any>(null);
71
+ const [summaryData, setSummaryDataLocal] = useState<any>(null);
72
+ const [poxData, setPoxData] = useState<any>(null);
73
+ const [excelData, setExcelData] = useState<any>(null);
74
+
75
+ const [scoreStep3Error, setScoreStep3Error] = useState<Error | null>(null);
76
+ const [visScoreError, setVisScoreError] = useState<Error | null>(null);
77
+ const [summaryError, setSummaryError] = useState<Error | null>(null);
78
+ const [poxError, setPoxError] = useState<Error | null>(null);
79
+ const [excelError, setExcelError] = useState<Error | null>(null);
80
+
81
+ // API実行中フラグ
82
+ const [isExecuting, setIsExecuting] = useState(false);
83
+ const executionRef = useRef(false);
84
+
85
+ const updateApiStatus = useCallback(
86
+ (apiName: string, status: ApiStatus['status']) => {
87
+ // Map API names to match StateStore expectations
88
+ const stateStoreApiName =
89
+ {
90
+ scoreStep3: 'getScore',
91
+ visScore: 'getVisScore',
92
+ summary: 'getSummary',
93
+ pox: 'getPox',
94
+ excel: 'getExcel',
95
+ }[apiName] || apiName;
96
+
97
+ if (status === 'loading') {
98
+ apiStartTimesRef.current[apiName] = Date.now();
99
+ setApiStatuses((prev) => ({
100
+ ...prev,
101
+ [apiName]: { duration: 0, status: 'loading' },
102
+ }));
103
+ // Sync to StateStore
104
+ updateStateStoreApiStatus(stateStoreApiName, { duration: 0, status: 'loading' });
105
+ } else if (status === 'success' || status === 'error') {
106
+ const startTime = apiStartTimesRef.current[apiName];
107
+ const duration = startTime ? (Date.now() - startTime) / 1000 : 0;
108
+ setApiStatuses((prev) => ({
109
+ ...prev,
110
+ [apiName]: { duration, status },
111
+ }));
112
+ // Sync to StateStore
113
+ updateStateStoreApiStatus(stateStoreApiName, { duration, status });
114
+ }
115
+ },
116
+ [updateStateStoreApiStatus],
117
+ );
118
+
119
+ // メイン実行関数
120
+ const execute = useCallback(async () => {
121
+ // 手動実行なのでenabledはチェックしない(重複実行のみ防ぐ)
122
+ if (executionRef.current) {
123
+ console.log('[usePreAnalysis] Execution skipped: already running');
124
+ return;
125
+ }
126
+
127
+ if (!commonDict) {
128
+ console.warn('[usePreAnalysis] commonDict is null, proceeding with empty object');
129
+ }
130
+
131
+ executionRef.current = true;
132
+ setIsExecuting(true);
133
+
134
+ try {
135
+ // Step 1: ScoreStep3
136
+ updateApiStatus('scoreStep3', 'loading');
137
+ const score = await scoreStep3Api({
138
+ tempImages: tempImages || {},
139
+ fvInfos: fvInfos || null,
140
+ cnInfo: cnInfo || null,
141
+ commonDict: commonDict || {},
142
+ dummyMode,
143
+ userEmail: userEmail ?? undefined,
144
+ userIdentifier,
145
+ });
146
+
147
+ if (Array.isArray(score)) {
148
+ setScoreStep3Data(score);
149
+ setScoreDict(score[0]);
150
+ setCommonDict(score[1]);
151
+ updateApiStatus('scoreStep3', 'success');
152
+ setScoreStep3Error(null);
153
+ } else {
154
+ throw new Error('scoreStep3の結果が不正な形式です');
155
+ }
156
+
157
+ // Step 2: VisScore
158
+ updateApiStatus('visScore', 'loading');
159
+ const vis: VisScoreRequest = {
160
+ commonDict: score[1] as Record<string, unknown>,
161
+ scoreDict: score[0] as Record<string, unknown>,
162
+ dummyMode,
163
+ userEmail: userEmail ?? undefined,
164
+ userIdentifier,
165
+ };
166
+ const visResult = await visScoreApi(vis);
167
+
168
+ if (Array.isArray(visResult)) {
169
+ setVisScoreData(visResult);
170
+ setScoreTotal(visResult[0]);
171
+ setUrlCategoryScores(visResult[1]);
172
+ setOwnCategoryScores(visResult[2]);
173
+ updateApiStatus('visScore', 'success');
174
+ setVisScoreError(null);
175
+ } else {
176
+ throw new Error('visScoreの結果が不正な形式です');
177
+ }
178
+
179
+ // Step 3: Summary & Pox(並列実行)
180
+ const summaryReq: SummaryRequest = {
181
+ commonDict: score[1] as Record<string, unknown>,
182
+ scoreDict: score[0] as Record<string, unknown>,
183
+ score_total: visResult[0] as Record<string, unknown>,
184
+ url_category_scores: visResult[1] as Record<string, unknown>,
185
+ own_category_score: visResult[2] as Record<string, unknown>,
186
+ dummyMode,
187
+ userEmail: userEmail ?? undefined,
188
+ userIdentifier,
189
+ };
190
+
191
+ const poxReq: PoxRequest = {
192
+ commonDict: score[1] as Record<string, unknown>,
193
+ scoreDict: score[0] as Record<string, unknown>,
194
+ score_total: visResult[0] as Record<string, unknown>,
195
+ dummyMode,
196
+ userEmail: userEmail ?? undefined,
197
+ userIdentifier,
198
+ };
199
+
200
+ updateApiStatus('summary', 'loading');
201
+ updateApiStatus('pox', 'loading');
202
+
203
+ const [summaryResult, poxResult] = await Promise.all([summaryApi(summaryReq), poxApi(poxReq)]);
204
+
205
+ if (Array.isArray(summaryResult)) {
206
+ setSummaryDataLocal(summaryResult);
207
+ setSummaryData(summaryResult[0]);
208
+ setSummaryBaseData(summaryResult[2]);
209
+ updateApiStatus('summary', 'success');
210
+ setSummaryError(null);
211
+ } else {
212
+ throw new Error('summaryの結果が不正な形式です');
213
+ }
214
+
215
+ if (Array.isArray(poxResult)) {
216
+ setPoxData(poxResult);
217
+ setSwotData75(poxResult[0]);
218
+ setSwotData78(poxResult[1]);
219
+ setSwotData79(poxResult[2]);
220
+ setSwotTable(poxResult[3]);
221
+ updateApiStatus('pox', 'success');
222
+ setPoxError(null);
223
+ } else {
224
+ throw new Error('poxの結果が不正な形式です');
225
+ }
226
+
227
+ // Step 4: Excel
228
+ updateApiStatus('excel', 'loading');
229
+ const excelReq: ExcelRequest = {
230
+ ownUrl: ((score[1] as Record<string, unknown>)?.own_url as string) || '',
231
+ urlText: ((score[1] as Record<string, unknown>)?.urls as string[]) || [],
232
+ commonDict: score[1] as Record<string, unknown>,
233
+ scoreDict: score[0] as Record<string, unknown>,
234
+ summary: summaryResult[0] as Record<string, unknown>,
235
+ swot: poxResult[0] as Record<string, unknown>,
236
+ dummyMode,
237
+ userEmail: userEmail ?? undefined,
238
+ userIdentifier,
239
+ };
240
+
241
+ const excelResult = await excelApi(excelReq);
242
+
243
+ if (Array.isArray(excelResult)) {
244
+ setExcelData(excelResult);
245
+ setDownloadData(excelResult[0]);
246
+ updateApiStatus('excel', 'success');
247
+ setExcelError(null);
248
+ } else {
249
+ throw new Error('excelの結果が不正な形式です');
250
+ }
251
+
252
+ console.log('[usePreAnalysis] All API calls completed successfully');
253
+ } catch (error) {
254
+ console.error('[usePreAnalysis] Error during execution:', error);
255
+
256
+ // エラー状態を設定
257
+ if (!scoreStep3Data) {
258
+ setScoreStep3Error(error instanceof Error ? error : new Error(String(error)));
259
+ updateApiStatus('scoreStep3', 'error');
260
+ } else if (!visScoreData) {
261
+ setVisScoreError(error instanceof Error ? error : new Error(String(error)));
262
+ updateApiStatus('visScore', 'error');
263
+ } else if (!summaryData || !poxData) {
264
+ if (!summaryData) {
265
+ setSummaryError(error instanceof Error ? error : new Error(String(error)));
266
+ updateApiStatus('summary', 'error');
267
+ }
268
+ if (!poxData) {
269
+ setPoxError(error instanceof Error ? error : new Error(String(error)));
270
+ updateApiStatus('pox', 'error');
271
+ }
272
+ } else {
273
+ setExcelError(error instanceof Error ? error : new Error(String(error)));
274
+ updateApiStatus('excel', 'error');
275
+ }
276
+
277
+ // エラーを再スローしてonSubmitのcatchブロックでキャッチできるようにする
278
+ throw error;
279
+ } finally {
280
+ setIsExecuting(false);
281
+ executionRef.current = false;
282
+ }
283
+ }, [
284
+ tempImages,
285
+ fvInfos,
286
+ cnInfo,
287
+ commonDict,
288
+ dummyMode,
289
+ userEmail,
290
+ userIdentifier,
291
+ scoreStep3Api,
292
+ visScoreApi,
293
+ summaryApi,
294
+ poxApi,
295
+ excelApi,
296
+ setScoreDict,
297
+ setCommonDict,
298
+ setScoreTotal,
299
+ setUrlCategoryScores,
300
+ setOwnCategoryScores,
301
+ setSummaryData,
302
+ setSummaryBaseData,
303
+ setSwotData75,
304
+ setSwotData78,
305
+ setSwotData79,
306
+ setSwotTable,
307
+ setDownloadData,
308
+ updateApiStatus,
309
+ ]);
310
+
311
+ // 統合されたローディング状態
312
+ const isLoading = useMemo(() => {
313
+ return Object.values(apiStatuses).some((s) => s.status === 'loading');
314
+ }, [apiStatuses]);
315
+
316
+ // 統合されたエラー状態
317
+ const isError = useMemo(() => {
318
+ return !!scoreStep3Error || !!visScoreError || !!summaryError || !!poxError || !!excelError;
319
+ }, [scoreStep3Error, visScoreError, summaryError, poxError, excelError]);
320
+
321
+ // エラーメッセージ取得
322
+ const getErrorMessage = useCallback(() => {
323
+ if (scoreStep3Error) return `スコア分析に失敗しました (1/5): ${scoreStep3Error.message}`;
324
+ if (visScoreError) return `ビジュアル分析に失敗しました (2/5): ${visScoreError.message}`;
325
+ if (summaryError) return `サマリー生成に失敗しました (3/5): ${summaryError.message}`;
326
+ if (poxError) return `SWOT分析に失敗しました (4/5): ${poxError.message}`;
327
+ if (excelError) return `Excelファイル生成に失敗しました (5/5): ${excelError.message}`;
328
+ return 'データの取得に失敗しました';
329
+ }, [scoreStep3Error, visScoreError, summaryError, poxError, excelError]);
330
+
331
+ // リトライ関数
332
+ const refetch = useCallback(() => {
333
+ // エラー状態をリセット
334
+ setScoreStep3Error(null);
335
+ setVisScoreError(null);
336
+ setSummaryError(null);
337
+ setPoxError(null);
338
+ setExcelError(null);
339
+
340
+ // 再実行
341
+ execute();
342
+ }, [execute]);
343
+
344
+ return {
345
+ execute,
346
+ isLoading,
347
+ isError,
348
+ error: isError ? new Error(getErrorMessage()) : null,
349
+ apiStatuses,
350
+ refetch,
351
+ // 個別データ(デバッグ用)
352
+ data: {
353
+ scoreStep3: scoreStep3Data,
354
+ visScore: visScoreData,
355
+ summary: summaryData,
356
+ pox: poxData,
357
+ excel: excelData,
358
+ },
359
+ };
360
+ }
api-client/gradio-proxy/vis-score.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import type { VisScoreRequest, VisScoreResponse } from '@/schema/gradio-proxy/vis-score';
4
+ import type { FetchQueryOptions } from '@tanstack/react-query';
5
+ import { useQueryClient } from '@tanstack/react-query';
6
+ import { useCallback } from 'react';
7
+
8
+ /**
9
+ * vis_score APIを呼び出すカスタムフック
10
+ * @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
11
+ */
12
+ export function useVisScore() {
13
+ const queryClient = useQueryClient();
14
+
15
+ return useCallback(
16
+ async (request: VisScoreRequest): Promise<VisScoreResponse> => {
17
+ console.log('[useVisScore] API呼び出し開始:', {
18
+ hasCommonDict: !!request.commonDict,
19
+ hasScoreDict: !!request.scoreDict,
20
+ dummyMode: request.dummyMode,
21
+ });
22
+
23
+ return queryClient.fetchQuery(
24
+ createQueryOptions<VisScoreResponse>({
25
+ queryKey: ['vis-score', request],
26
+ queryFn: async (): Promise<VisScoreResponse> => {
27
+ try {
28
+ const response = await rpcClient['gradio-proxy']['vis-score'].$post({
29
+ json: request,
30
+ });
31
+
32
+ if (!response.ok) {
33
+ console.error('[useVisScore] Response status:', response.status);
34
+ const errorData = (await response.json()) as { message?: string; status?: string };
35
+ console.error('[useVisScore] API エラー:', errorData);
36
+ throw new Error(errorData.message || 'vis_score APIの呼び出しに失敗しました');
37
+ }
38
+
39
+ const data = await response.json();
40
+ console.log('[useVisScore] API呼び出し成功');
41
+ return data as VisScoreResponse;
42
+ } catch (error) {
43
+ console.error('[useVisScore] API呼び出しエラー:', error);
44
+ throw error;
45
+ }
46
+ },
47
+ }) as FetchQueryOptions<VisScoreResponse>,
48
+ );
49
+ },
50
+ [queryClient],
51
+ );
52
+ }
api-client/pox/queries.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { SwotData } from '@/schema/pox';
2
+ import { getPox } from '@/services/gradio-with-dummy';
3
+ import { useGlobalStore } from '@/store/global';
4
+ import { useQuery } from '@tanstack/react-query';
5
+
6
+ export function usePox(commonDict: unknown, scoreDict: unknown, scoreTotal: unknown, userEmail: string) {
7
+ const { dummyMode, userIdentifier } = useGlobalStore();
8
+ return useQuery<SwotData>({
9
+ queryKey: ['pox', commonDict, scoreDict, scoreTotal, userEmail, dummyMode],
10
+ queryFn: async () => {
11
+ const userData = {
12
+ commonDict: commonDict as object,
13
+ scoreDict: scoreDict as object,
14
+ score_total: scoreTotal as object,
15
+ };
16
+
17
+ // Gradio APIを直接呼び出し
18
+ const result = await getPox(userData, userEmail || null, userIdentifier);
19
+
20
+ // 結果の最初の要素(基本SWOT)を返す
21
+ if (Array.isArray(result) && result.length > 0) {
22
+ return result[0] as SwotData;
23
+ }
24
+
25
+ throw new Error('Invalid POX response format');
26
+ },
27
+ enabled:
28
+ (dummyMode || userEmail !== '') &&
29
+ (dummyMode ||
30
+ (commonDict !== undefined &&
31
+ commonDict !== null &&
32
+ scoreDict !== undefined &&
33
+ scoreDict !== null &&
34
+ scoreTotal !== undefined &&
35
+ scoreTotal !== null)),
36
+ staleTime: 1000 * 60 * 5, // 5分間はキャッシュを使用
37
+ });
38
+ }
api-client/proposal/html-preview/index.ts ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { parseApiResponse } from '@/api-client/utils/parseApiResponse';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import type { SwotData, SwotDataWithoutSummary } from '@/schema/pox';
4
+ import {
5
+ generateContentsHtmlResponseSchema,
6
+ generateFvHtmlResponseSchema,
7
+ type ContentSection,
8
+ type FvData,
9
+ type GenerateContentsHtmlResponse,
10
+ type GenerateFvHtmlResponse,
11
+ } from '@/schema/proposal';
12
+ import type { ThemeExtractionResult } from '@/schema/theme-extraction';
13
+
14
+ /**
15
+ * FV提案のHTML生成
16
+ */
17
+ export async function generateFvHtml(params: {
18
+ tabName: string;
19
+ fvData: FvData;
20
+ provider?: 'openai' | 'gemini' | 'claude';
21
+ referenceUrl?: string;
22
+ screenshot?: string;
23
+ dummyMode?: boolean;
24
+ swotData?: SwotData | SwotDataWithoutSummary;
25
+ userEmail?: string;
26
+ sourcePage?: string;
27
+ forceFallbackMode?: boolean;
28
+ themeData?: ThemeExtractionResult | null;
29
+ }): Promise<GenerateFvHtmlResponse> {
30
+ // テーマデータがある場合はログ出力のみ(将来のAPI対応用)
31
+ if (params.themeData) {
32
+ console.log('[FV HTML] テーマデータが適用されます:', params.themeData);
33
+ }
34
+
35
+ const response = await rpcClient.proposal['html-preview']['generate-fv-html'].$post({
36
+ json: {
37
+ tabName: params.tabName,
38
+ fvData: params.fvData,
39
+ provider: params.provider,
40
+ referenceUrl: params.referenceUrl,
41
+ screenshotUrl: params.screenshot, // パラメータ名をscreenshotUrlに変換
42
+ dummyMode: params.dummyMode,
43
+ swotData: params.swotData,
44
+ userEmail: params.userEmail,
45
+ sourcePage: params.sourcePage,
46
+ forceFallbackMode: params.forceFallbackMode,
47
+ themeData: params.themeData ?? undefined, // nullをundefinedに変換
48
+ },
49
+ });
50
+ return parseApiResponse(response, generateFvHtmlResponseSchema);
51
+ }
52
+
53
+ /**
54
+ * コンテンツ提案のHTML生成
55
+ */
56
+ export async function generateContentsHtml(params: {
57
+ tabName: string;
58
+ cnData: ContentSection[];
59
+ provider?: 'openai' | 'gemini' | 'claude';
60
+ referenceUrl?: string;
61
+ screenshot?: string;
62
+ dummyMode?: boolean;
63
+ userEmail?: string;
64
+ themeData?: ThemeExtractionResult | null;
65
+ }): Promise<GenerateContentsHtmlResponse> {
66
+ // テーマデータがある場合はログ出力のみ(将来のAPI対応用)
67
+ if (params.themeData) {
68
+ console.log('[CONTENTS HTML] テーマデータが適用されます:', params.themeData);
69
+ }
70
+
71
+ const response = await rpcClient.proposal['html-preview']['generate-contents-html'].$post({
72
+ json: {
73
+ tabName: params.tabName,
74
+ cnData: params.cnData,
75
+ provider: params.provider,
76
+ referenceUrl: params.referenceUrl,
77
+ screenshotUrl: params.screenshot, // パラメータ名をscreenshotUrlに変換
78
+ dummyMode: params.dummyMode,
79
+ userEmail: params.userEmail,
80
+ themeData: params.themeData ?? undefined, // nullをundefinedに変換
81
+ },
82
+ });
83
+ return parseApiResponse(response, generateContentsHtmlResponseSchema);
84
+ }
85
+
86
+ /**
87
+ * 画像生成を開始
88
+ */
89
+ export async function startImageGeneration(
90
+ html: string,
91
+ tabName: string,
92
+ cnData: ContentSection[],
93
+ provider: 'openai' | 'claude' = 'claude',
94
+ ): Promise<{
95
+ batchId: string;
96
+ estimatedCompletionTime: string;
97
+ message: string;
98
+ status: string;
99
+ }> {
100
+ const response = await fetch('/api/rpc/proposal/start-contents-image-generation', {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ body: JSON.stringify({
106
+ html,
107
+ tabName,
108
+ cnData,
109
+ provider,
110
+ }),
111
+ });
112
+
113
+ if (!response.ok) {
114
+ throw new Error(`画像生成開始に失敗しました: ${response.statusText}`);
115
+ }
116
+
117
+ return response.json();
118
+ }
119
+
120
+ /**
121
+ * 画像生成状況を確認
122
+ */
123
+ export async function getImageGenerationStatus(batchId: string): Promise<{
124
+ batchId: string;
125
+ status: 'pending' | 'processing' | 'completed' | 'failed';
126
+ jobs: Array<{
127
+ jobId: string;
128
+ imageId: string;
129
+ status: 'pending' | 'generating' | 'completed' | 'failed';
130
+ generatedImageUrl?: string;
131
+ errorMessage?: string;
132
+ }>;
133
+ createdAt: string;
134
+ completedAt?: string;
135
+ } | null> {
136
+ const response = await fetch(`/api/rpc/proposal/image-generation-status/${batchId}`);
137
+
138
+ if (!response.ok) {
139
+ if (response.status === 404) {
140
+ // バッチIDが存在しない場合は null を返す
141
+ return null;
142
+ }
143
+ throw new Error(`ステータス確認に失敗しました: ${response.statusText}`);
144
+ }
145
+
146
+ return response.json();
147
+ }
api-client/proposal/html-preview/queries.ts ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import type { SwotData, SwotDataWithoutSummary } from '@/schema/pox';
3
+ import type { ContentSection, FvData, GenerateContentsHtmlResponse, GenerateFvHtmlResponse } from '@/schema/proposal';
4
+ import type { ThemeExtractionResult } from '@/schema/theme-extraction';
5
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
6
+ import React from 'react';
7
+ import { generateContentsHtml, generateFvHtml, getImageGenerationStatus, startImageGeneration } from './index';
8
+
9
+ // FV提案HTMLを取得するフック(タブごと)
10
+ export function useFvHtmlGeneration(
11
+ tabName: string,
12
+ fvData?: FvData,
13
+ enabled: boolean = false,
14
+ provider?: 'openai' | 'gemini' | 'claude',
15
+ referenceUrl?: string,
16
+ screenshot?: string,
17
+ dummyMode?: boolean,
18
+ swotData?: SwotData | SwotDataWithoutSummary,
19
+ userEmail?: string,
20
+ sourcePage?: string,
21
+ forceFallbackMode?: boolean,
22
+ themeData?: ThemeExtractionResult | null,
23
+ ) {
24
+ // パラメータから安定したハッシュを生成
25
+ const generateParamsHash = () => {
26
+ if (!fvData) return 'no-fv-data';
27
+
28
+ // FVデータの主要パラメータからハッシュを生成(実際の構造に合わせる)
29
+ const relevantParams = {
30
+ strategy: fvData.strategy,
31
+ need: fvData.need,
32
+ // FVデータの主要部分
33
+ mainCopy: fvData.data?.fv?.コピー?.メインコピー || '',
34
+ subCopy1: fvData.data?.fv?.コピー?.サブコピー1 || '',
35
+ subCopy2: fvData.data?.fv?.コピー?.サブコピー2 || '',
36
+ subCopy3: fvData.data?.fv?.コピー?.サブコピー3 || '',
37
+ ctaButton: fvData.data?.fv?.CTA?.ボタンテキスト || '',
38
+ microCopy: fvData.data?.fv?.CTA?.マイクロコピー || '',
39
+ visualCreationInstruction: fvData.data?.fv?.ビジュアル?.作成指示 || '',
40
+ authorityCreationInstruction: fvData.data?.fv?.権威付け?.作成指示 || '',
41
+ fvCreationIntent: fvData.fvCreationIntent || '',
42
+ // SWOTデータの主要部分
43
+ swotKeys: swotData ? Object.keys(swotData).sort().join('_') : '',
44
+ swotSample: swotData ? `${swotData['業界'] || ''}_${swotData['商材'] || ''}_${swotData['強み'] || ''}_${swotData['弱み'] || ''}` : '',
45
+ };
46
+
47
+ const str = JSON.stringify(relevantParams);
48
+ let hash = 0;
49
+ for (let i = 0; i < str.length; i++) {
50
+ hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
51
+ }
52
+ return Math.abs(hash).toString(36);
53
+ };
54
+
55
+ const paramsHash = generateParamsHash();
56
+ const fvDataKey = `${tabName}_${provider || 'default'}_${dummyMode ? 'dummy' : 'live'}_${paramsHash}`;
57
+
58
+ // 前回のキーを保存するためのRef
59
+ const prevKeyRef = React.useRef<string>('');
60
+
61
+ // 現在のqueryKeyを生成
62
+ // 画像再生成を防ぐため、themeDataはクエリキーに含めない
63
+ const currentQueryKey = ['fvHtml', tabName, fvDataKey, provider, referenceUrl, dummyMode];
64
+
65
+ return useQuery<GenerateFvHtmlResponse>({
66
+ ...createQueryOptions<GenerateFvHtmlResponse>(),
67
+ queryKey: currentQueryKey,
68
+ queryFn: () => {
69
+ if (!fvData) {
70
+ throw new Error('fvData is required');
71
+ }
72
+ return generateFvHtml({
73
+ tabName,
74
+ fvData,
75
+ provider,
76
+ referenceUrl,
77
+ screenshot,
78
+ dummyMode,
79
+ swotData,
80
+ userEmail,
81
+ sourcePage,
82
+ forceFallbackMode,
83
+ themeData: null, // テーマはCSSのみで適用するため、画像生成時には渡さない
84
+ });
85
+ },
86
+ enabled: enabled && !!fvData,
87
+ });
88
+ }
89
+
90
+ // コンテンツ提案HTMLを取得するフック(タブごと)
91
+ export function useContentsHtmlGeneration(
92
+ tabName: string,
93
+ cnData?: ContentSection[],
94
+ enabled: boolean = false,
95
+ provider?: 'openai' | 'gemini' | 'claude',
96
+ referenceUrl?: string,
97
+ screenshot?: string,
98
+ dummyMode?: boolean,
99
+ swotData?: SwotData | SwotDataWithoutSummary,
100
+ userEmail?: string,
101
+ themeData?: ThemeExtractionResult | null,
102
+ ) {
103
+ // パラメータから安定したハッシュを生成
104
+ const generateParamsHash = () => {
105
+ if (!cnData) return 'no-cn-data';
106
+
107
+ // CNデータの主要パラメータからハッシュを生成
108
+ const relevantParams = {
109
+ // CNデータのサンプル(最初の3要素)
110
+ sectionsCount: cnData.length || 0,
111
+ firstSections:
112
+ cnData
113
+ .slice(0, 3)
114
+ .map((s) => s.中区分)
115
+ .join('_') || '',
116
+ // SWOTデータの主要部分
117
+ swotKeys: swotData ? Object.keys(swotData).sort().join('_') : '',
118
+ swotSample: swotData ? `${swotData['業界'] || ''}_${swotData['商材'] || ''}` : '',
119
+ };
120
+
121
+ const str = JSON.stringify(relevantParams);
122
+ let hash = 0;
123
+ for (let i = 0; i < str.length; i++) {
124
+ hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
125
+ }
126
+ return Math.abs(hash).toString(36);
127
+ };
128
+
129
+ const paramsHash = generateParamsHash();
130
+ const cnDataKey = `${tabName}_${provider || 'default'}_${dummyMode ? 'dummy' : 'live'}_${paramsHash}`;
131
+
132
+ // 前回のキーを保存するためのRef
133
+ const prevKeyRef = React.useRef<string>('');
134
+
135
+ // 現在のqueryKeyを生成
136
+ // 画像再生成を防ぐため、themeDataはクエリキーに含めない
137
+ const currentQueryKey = ['cnHtml', tabName, cnDataKey, provider, referenceUrl, dummyMode];
138
+
139
+ return useQuery<GenerateContentsHtmlResponse>({
140
+ ...createQueryOptions<GenerateContentsHtmlResponse>(),
141
+ queryKey: currentQueryKey,
142
+ queryFn: () => {
143
+ if (!cnData) {
144
+ throw new Error('cnData is required');
145
+ }
146
+ console.log(`[CN HTML] 生成開始: ${tabName}`);
147
+ return generateContentsHtml({
148
+ tabName,
149
+ cnData,
150
+ provider,
151
+ referenceUrl,
152
+ screenshot,
153
+ dummyMode,
154
+ userEmail,
155
+ themeData: null, // テーマはCSSのみで適用するため、画像生成時には渡さない
156
+ });
157
+ },
158
+ enabled: enabled && !!cnData,
159
+ });
160
+ }
161
+
162
+ // 画像生成を別途実行するフック
163
+ export function useContentsImageGeneration(
164
+ tabName: string,
165
+ cnData?: ContentSection[],
166
+ htmlContent?: string,
167
+ enabled: boolean = false,
168
+ provider?: 'openai' | 'gemini' | 'claude',
169
+ ) {
170
+ const queryClient = useQueryClient();
171
+
172
+ return useQuery({
173
+ queryKey: ['contentsImageGeneration', tabName, cnData, provider],
174
+ queryFn: async () => {
175
+ if (!cnData || !htmlContent) {
176
+ throw new Error('cnData and htmlContent are required');
177
+ }
178
+
179
+ try {
180
+ const batchResult = await startImageGeneration(
181
+ htmlContent,
182
+ tabName,
183
+ cnData,
184
+ provider === 'openai' || provider === 'claude' ? provider : 'claude',
185
+ );
186
+
187
+ if (batchResult.batchId) {
188
+ // ポーリング処理を開始
189
+ const pollInterval = setInterval(async () => {
190
+ try {
191
+ const status = await getImageGenerationStatus(batchResult.batchId);
192
+
193
+ if (!status) {
194
+ clearInterval(pollInterval);
195
+ return;
196
+ }
197
+
198
+ if (status.status === 'completed' && status.jobs) {
199
+ clearInterval(pollInterval);
200
+
201
+ const imageReplacements = status.jobs
202
+ .filter((job) => job.status === 'completed' && job.generatedImageUrl)
203
+ .map((job) => ({
204
+ imageId: job.imageId,
205
+ imageUrl: job.generatedImageUrl || '',
206
+ }));
207
+
208
+ if (imageReplacements.length > 0) {
209
+ let updatedHtml = htmlContent;
210
+ imageReplacements.forEach(({ imageId, imageUrl }) => {
211
+ const regex = new RegExp(`src="placeholder:${imageId}"`, 'g');
212
+ updatedHtml = updatedHtml.replace(regex, `src="${imageUrl}"`);
213
+ });
214
+
215
+ // キャッシュを更新
216
+ const currentData = queryClient.getQueryData<GenerateContentsHtmlResponse>(['contentsHtml', tabName, cnData, provider]);
217
+
218
+ if (currentData) {
219
+ queryClient.setQueryData(['contentsHtml', tabName, cnData, provider], {
220
+ ...currentData,
221
+ html: updatedHtml,
222
+ });
223
+ }
224
+ }
225
+ } else if (status.status === 'failed') {
226
+ clearInterval(pollInterval);
227
+ console.error('[Contents Image Batch] Failed:', status);
228
+ }
229
+ } catch (error) {
230
+ console.error('[Contents Image Polling] Error:', error);
231
+ }
232
+ }, 20000); // 20秒ごとにポーリング(負荷軽減)
233
+
234
+ // 最大20分後にタイムアウト(画像生成の実際の処理時間を考慮)
235
+ setTimeout(() => {
236
+ clearInterval(pollInterval);
237
+ console.warn('[Contents Image Polling] Timeout after 20 minutes');
238
+ }, 1200000); // 20分 = 1200秒
239
+ }
240
+
241
+ return batchResult;
242
+ } catch (error) {
243
+ console.error('[Contents Image Batch] Failed to start:', error);
244
+ throw error;
245
+ }
246
+ },
247
+ enabled: enabled && !!cnData && !!htmlContent,
248
+ staleTime: Infinity, // 一度実行したら再実行しない
249
+ });
250
+ }
api-client/proposal/index.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // Re-export everything from subdirectories
2
+ export * from './html-preview/index';
3
+ export * from './html-preview/queries';
4
+ export * from './proposal/queries';
5
+ export * from './proposal/translator';
6
+ export * from './screenshot/queries';
api-client/proposal/proposal/individual-queries.ts ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import type { ProposalCnGradioResponse } from '@/schema/gradio-proxy/proposal-cn';
4
+ import type { ProposalFvGradioResponse } from '@/schema/gradio-proxy/proposal-fv';
5
+ import type { ProposalIntentGradioResponse } from '@/schema/gradio-proxy/proposal-intent';
6
+ import type { ProposalPredictionGradioResponse } from '@/schema/gradio-proxy/proposal-prediction';
7
+ import type { ThemeByMomentStrategy } from '@/schema/gradio-proxy/theme-by-moment';
8
+ import { useGlobalStore } from '@/store/global';
9
+ import { useQuery, type UseQueryResult } from '@tanstack/react-query';
10
+
11
+ // 個別のuseQueryフック
12
+ interface BaseProposalParams {
13
+ selectionHash: string;
14
+ userEmail?: string | null;
15
+ dummyMode?: boolean;
16
+ }
17
+
18
+ interface ProposalFvParams extends BaseProposalParams {
19
+ strategy_x_moment?: Record<string, unknown>;
20
+ commonDict?: Record<string, unknown>;
21
+ fv_infos?: Record<string, unknown>;
22
+ strategies?: Record<string, unknown>;
23
+ cnall_json?: Record<string, unknown>;
24
+ url_category_scores?: Record<string, unknown>;
25
+ }
26
+
27
+ export function useProposalFv(params: ProposalFvParams, enabled: boolean = true): UseQueryResult<ProposalFvGradioResponse> {
28
+ const { selectionHash, dummyMode, ...apiParams } = params;
29
+ const { userIdentifier } = useGlobalStore();
30
+
31
+ return useQuery({
32
+ queryKey: ['proposalFv', selectionHash],
33
+ ...createQueryOptions<ProposalFvGradioResponse>({
34
+ queryFn: async () => {
35
+ const response = await rpcClient['gradio-proxy']['proposal-fv'].$post({
36
+ json: {
37
+ strategy_x_moment: apiParams.strategy_x_moment,
38
+ commonDict: apiParams.commonDict,
39
+ fv_infos: apiParams.fv_infos,
40
+ strategies: apiParams.strategies,
41
+ cnall_json: apiParams.cnall_json,
42
+ url_category_scores: apiParams.url_category_scores,
43
+ dummyMode: dummyMode,
44
+ userEmail: apiParams.userEmail ?? undefined,
45
+ userIdentifier,
46
+ },
47
+ });
48
+
49
+ if (!response.ok) {
50
+ console.error('[useProposalFv] Response status:', response.status);
51
+ const errorData = (await response.json()) as { message?: string; status?: string };
52
+ console.error('[useProposalFv] API エラー:', errorData);
53
+ throw new Error(errorData.message || 'proposal_fv APIの呼び出しに失敗しました');
54
+ }
55
+
56
+ const data = await response.json();
57
+ return data as ProposalFvGradioResponse;
58
+ },
59
+ enabled: enabled,
60
+ staleTime: 30 * 60 * 1000, // 30分
61
+ gcTime: 60 * 60 * 1000, // 1時間
62
+ retry: false,
63
+ }),
64
+ });
65
+ }
66
+
67
+ interface ProposalCnParams extends BaseProposalParams {
68
+ strategy_x_moment?: Record<string, unknown>;
69
+ commonDict?: Record<string, unknown>;
70
+ cnall_json?: Record<string, unknown>;
71
+ strategies?: Record<string, unknown>;
72
+ fv_infos?: Record<string, unknown>;
73
+ }
74
+
75
+ export function useProposalCn(params: ProposalCnParams, enabled: boolean = true): UseQueryResult<ProposalCnGradioResponse> {
76
+ const { selectionHash, dummyMode, ...apiParams } = params;
77
+ const { userIdentifier } = useGlobalStore();
78
+
79
+ return useQuery({
80
+ queryKey: ['proposalCn', selectionHash],
81
+ ...createQueryOptions<ProposalCnGradioResponse>({
82
+ queryFn: async () => {
83
+ const response = await rpcClient['gradio-proxy']['proposal-cn'].$post({
84
+ json: {
85
+ strategy_x_moment: apiParams.strategy_x_moment,
86
+ commonDict: apiParams.commonDict,
87
+ cnall_json: apiParams.cnall_json,
88
+ strategies: apiParams.strategies,
89
+ fv_infos: apiParams.fv_infos,
90
+ dummyMode: dummyMode,
91
+ userEmail: apiParams.userEmail ?? undefined,
92
+ userIdentifier,
93
+ },
94
+ });
95
+
96
+ if (!response.ok) {
97
+ console.error('[useProposalCn] Response status:', response.status);
98
+ const errorData = (await response.json()) as { message?: string; status?: string };
99
+ console.error('[useProposalCn] API エラー:', errorData);
100
+ throw new Error(errorData.message || 'proposal_cn APIの呼び出しに失敗しました');
101
+ }
102
+
103
+ const data = await response.json();
104
+ return data as ProposalCnGradioResponse;
105
+ },
106
+ enabled: enabled,
107
+ staleTime: 30 * 60 * 1000,
108
+ gcTime: 60 * 60 * 1000,
109
+ retry: false,
110
+ }),
111
+ });
112
+ }
113
+
114
+ interface ProposalPredictionParams extends BaseProposalParams {
115
+ proposal_fv?: ProposalFvGradioResponse;
116
+ proposal_cn?: ProposalCnGradioResponse;
117
+ commonDict?: Record<string, unknown>;
118
+ fv_infos?: Record<string, unknown>;
119
+ }
120
+
121
+ export function useProposalPrediction(params: ProposalPredictionParams, enabled: boolean = true): UseQueryResult<ProposalPredictionGradioResponse> {
122
+ const { dummyMode, selectionHash, ...apiParams } = params;
123
+ const { userIdentifier } = useGlobalStore();
124
+
125
+ return useQuery({
126
+ // selectionHashのみでキ���ッシュ管理(FV/CNが更新されても同じ選択肢なら再フェッチしない)
127
+ queryKey: ['proposalPrediction', selectionHash],
128
+ ...createQueryOptions<ProposalPredictionGradioResponse>({
129
+ queryFn: async () => {
130
+ try {
131
+ const response = await rpcClient['gradio-proxy']['proposal-prediction'].$post({
132
+ json: {
133
+ proposal_fv: apiParams.proposal_fv,
134
+ proposal_cn: apiParams.proposal_cn,
135
+ commonDict: apiParams.commonDict,
136
+ fv_infos: apiParams.fv_infos,
137
+ dummyMode: dummyMode,
138
+ userEmail: apiParams.userEmail ?? undefined,
139
+ userIdentifier,
140
+ },
141
+ });
142
+
143
+ if (!response.ok) {
144
+ console.error('[useProposalPrediction] Response status:', response.status);
145
+ const errorData = (await response.json()) as { message?: string; status?: string };
146
+ console.error('[useProposalPrediction] API エラー:', errorData);
147
+ throw new Error(errorData.message || 'proposal_prediction APIの呼び出しに失敗しました');
148
+ }
149
+
150
+ const data = await response.json();
151
+ return data as ProposalPredictionGradioResponse;
152
+ } catch (error: any) {
153
+ console.error('[useProposalPrediction] Error during fetch or processing:', {
154
+ message: error?.message,
155
+ stack: error?.stack,
156
+ code: error?.code,
157
+ });
158
+ throw error;
159
+ }
160
+ },
161
+ enabled: enabled && !!apiParams.proposal_fv && !!apiParams.proposal_cn,
162
+ staleTime: 30 * 60 * 1000,
163
+ gcTime: 60 * 60 * 1000,
164
+ // デフォルトのリトライ設定を使用(retry: 1)
165
+ // retry: false を削除して、ネットワークエラー等での適切なリトライを許可
166
+ }),
167
+ });
168
+ }
169
+
170
+ interface ProposalIntentParams extends BaseProposalParams {
171
+ proposal_fv?: ProposalFvGradioResponse;
172
+ proposal_cn?: ProposalCnGradioResponse;
173
+ commonDict?: Record<string, unknown>;
174
+ swot?: Record<string, unknown>;
175
+ q_summary?: Record<string, unknown>;
176
+ strategy_x_moment?: Record<string, unknown>;
177
+ }
178
+
179
+ export function useProposalIntent(params: ProposalIntentParams, enabled: boolean = true): UseQueryResult<ProposalIntentGradioResponse> {
180
+ const { dummyMode, selectionHash, ...apiParams } = params;
181
+ const { userIdentifier } = useGlobalStore();
182
+
183
+ return useQuery({
184
+ // selectionHashのみでキャッシュ管理(FV/CNが更新されても同じ選択肢なら再フェッチしない)
185
+ queryKey: ['proposalIntent', selectionHash],
186
+ ...createQueryOptions<ProposalIntentGradioResponse>({
187
+ queryFn: async () => {
188
+ const response = await rpcClient['gradio-proxy']['proposal-intent'].$post({
189
+ json: {
190
+ proposal_fv: apiParams.proposal_fv,
191
+ proposal_cn: apiParams.proposal_cn,
192
+ commonDict: apiParams.commonDict,
193
+ swot: apiParams.swot,
194
+ q_summary: apiParams.q_summary,
195
+ strategy_x_moment: apiParams.strategy_x_moment as ThemeByMomentStrategy | undefined,
196
+ dummyMode: dummyMode,
197
+ userEmail: apiParams.userEmail ?? undefined,
198
+ userIdentifier,
199
+ },
200
+ });
201
+
202
+ if (!response.ok) {
203
+ console.error('[useProposalIntent] Response status:', response.status);
204
+ const errorData = (await response.json()) as { message?: string; status?: string };
205
+ console.error('[useProposalIntent] API エラー:', errorData);
206
+ throw new Error(errorData.message || 'proposal_intent APIの呼び出しに失敗しました');
207
+ }
208
+
209
+ const data = await response.json();
210
+ return data as ProposalIntentGradioResponse;
211
+ },
212
+ enabled: enabled && !!apiParams.proposal_fv && !!apiParams.proposal_cn,
213
+ staleTime: 30 * 60 * 1000,
214
+ gcTime: 60 * 60 * 1000,
215
+ retry: false, // デバッグのためリトライを無効化
216
+ refetchOnWindowFocus: false, // ウィンドウフォーカス時の再フェッチを無効化
217
+ }),
218
+ });
219
+ }
220
+
221
+ /**
222
+ * useThemeByMomentも個別フック化
223
+ */
224
+ interface ThemeByMomentParams extends BaseProposalParams {
225
+ commonDict?: Record<string, unknown>;
226
+ scoreDict?: Record<string, unknown>;
227
+ swot?: Record<string, unknown>;
228
+ strategies?: Record<string, unknown>;
229
+ fv_infos?: Record<string, unknown>;
230
+ moments?: string[];
231
+ own_theme?: string;
232
+ }
233
+
234
+ export function useThemeByMomentOptimized(params: ThemeByMomentParams, enabled: boolean = true): UseQueryResult<unknown[]> {
235
+ const { selectionHash, dummyMode, userEmail, ...apiParams } = params;
236
+ const { userIdentifier } = useGlobalStore();
237
+
238
+ const queryResult = useQuery({
239
+ queryKey: ['themeByMoment', selectionHash],
240
+ ...createQueryOptions<unknown[]>({
241
+ queryFn: async () => {
242
+ // 通常モードで必要なデータが不足している場合はエラーをthrow
243
+ if (!dummyMode) {
244
+ if (!apiParams.commonDict || Object.keys(apiParams.commonDict).length === 0) {
245
+ console.error('[useThemeByMoment] ERROR: commonDict is missing in normal mode');
246
+ throw new Error('分析データ(commonDict)が設定されていません。詳細画面から正しく遷移してください。');
247
+ }
248
+ if (!apiParams.scoreDict || Object.keys(apiParams.scoreDict).length === 0) {
249
+ console.error('[useThemeByMoment] ERROR: scoreDict is missing in normal mode');
250
+ throw new Error('スコアデータ(scoreDict)が設定されていません。詳細画面から正しく遷移してください。');
251
+ }
252
+ if (!apiParams.moments || apiParams.moments.length === 0) {
253
+ console.error('[useThemeByMoment] ERROR: moments is empty in normal mode');
254
+ throw new Error('改善案の方向性が選択されていません。');
255
+ }
256
+ // swotData78 (strategies)は必須ではないが、警告を出す
257
+ if (!apiParams.strategies) {
258
+ console.warn('[useThemeByMoment] WARNING: strategies (swotData78) is missing, using empty object');
259
+ }
260
+ }
261
+
262
+ const response = await rpcClient['gradio-proxy']['theme-by-moment'].$post({
263
+ json: {
264
+ commonDict: apiParams.commonDict || {},
265
+ scoreDict: apiParams.scoreDict || {},
266
+ swot: apiParams.swot || {},
267
+ strategies: apiParams.strategies || {},
268
+ moments: apiParams.moments || [],
269
+ fv_infos: apiParams.fv_infos,
270
+ own_theme: apiParams.own_theme || '',
271
+ dummyMode: dummyMode,
272
+ userEmail: userEmail || '',
273
+ userIdentifier,
274
+ },
275
+ });
276
+
277
+ if (!response.ok) {
278
+ console.error('[useThemeByMoment] Response status:', response.status);
279
+ const errorData = (await response.json()) as { message?: string; status?: string };
280
+ console.error('[useThemeByMoment] API エラー:', errorData);
281
+ throw new Error(errorData.message || 'theme_by_moment APIの呼び出しに失敗しました');
282
+ }
283
+
284
+ const data = await response.json();
285
+ return data as unknown[];
286
+ },
287
+ enabled: enabled && (dummyMode || (!!apiParams.moments && apiParams.moments.length > 0)),
288
+ staleTime: 30 * 60 * 1000,
289
+ gcTime: 60 * 60 * 1000,
290
+ retry: false,
291
+ }),
292
+ });
293
+
294
+ // デバッグ用ログ
295
+ if (queryResult.isError) {
296
+ console.error('[useThemeByMoment] Query is in error state:', {
297
+ error: queryResult.error,
298
+ errorMessage: queryResult.error?.message,
299
+ });
300
+ }
301
+
302
+ return queryResult;
303
+ }
api-client/proposal/proposal/optimized-queries.ts ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ProposalTranslator } from '@/api-client/proposal/proposal/translator';
2
+ import type { ProposalCnGradioResponse } from '@/schema/gradio-proxy/proposal-cn';
3
+ import type { ProposalFvGradioResponse } from '@/schema/gradio-proxy/proposal-fv';
4
+ import type { ProposalIntentGradioResponse } from '@/schema/gradio-proxy/proposal-intent';
5
+ import type { ProposalPredictionGradioResponse } from '@/schema/gradio-proxy/proposal-prediction';
6
+ import type { ThemeByMomentStrategy } from '@/schema/gradio-proxy/theme-by-moment';
7
+ import type { ExtendedProposalTabsResponse } from '@/schema/proposal';
8
+ import { useGlobalStore } from '@/store/global';
9
+ import { useInputStore } from '@/store/input';
10
+ import { generateSelectionHash } from '@/store/proposal-trigger';
11
+ import { useResultStore } from '@/store/result';
12
+ import { useUserStore } from '@/store/user';
13
+ import { useQueryClient } from '@tanstack/react-query';
14
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
+ import { useProposalCn, useProposalFv, useProposalIntent, useProposalPrediction, useThemeByMomentOptimized } from './individual-queries';
16
+
17
+ interface UseProposalTabsOptimizedParams {
18
+ // 外部から指定可能なパラメータ
19
+ strategy_x_moment?: ThemeByMomentStrategy;
20
+ enabled?: boolean;
21
+ dummyMode?: boolean;
22
+ }
23
+
24
+ export function useProposalTabsOptimized(params: UseProposalTabsOptimizedParams = {}) {
25
+ const { strategy_x_moment: externalStrategyXMoment, enabled: externalEnabled = true, dummyMode: paramDummyMode } = params;
26
+
27
+ // Store からの必要なデータを取得
28
+ const { dummyMode: storeDummyMode } = useGlobalStore();
29
+ const { fvInfos, selectedMoments, cnInfo } = useInputStore();
30
+ const { commonDict, scoreDict, swotData75, swotData78, summaryData, urlCategoryScores } = useResultStore();
31
+ const { user } = useUserStore();
32
+ const queryClient = useQueryClient();
33
+ const prevContentHashRef = useRef<string>('');
34
+
35
+ // API実行時間追跡用
36
+ const apiStartTimesRef = useRef<Record<string, number>>({});
37
+ const [apiStatuses, setApiStatuses] = useState<Record<string, { duration: number; status: 'loading' | 'success' | 'error' }>>({});
38
+
39
+ // ダミーモードの判定
40
+ const isDummyMode = paramDummyMode !== undefined ? paramDummyMode : storeDummyMode;
41
+
42
+ // ダミーモード用のデータ
43
+ const dummyCommonDict = {
44
+ dummy: 'data',
45
+ regulation: 'test',
46
+ mode: true,
47
+ };
48
+ const effectiveCommonDict = isDummyMode ? commonDict || dummyCommonDict : commonDict;
49
+
50
+ // コンテンツデータからハッシュを生成(キャッシュキーとして使用)
51
+ // selectedMomentsも含めて、選択肢が変わった場合に新しいキャッシュキーを生成
52
+ const contentData = useMemo(
53
+ () => ({
54
+ fvInfos: fvInfos ? JSON.parse(JSON.stringify(fvInfos)) : null, // ディープコピーで参照を除去
55
+ cnInfo: cnInfo ? JSON.parse(JSON.stringify(cnInfo)) : null, // ディープコピーで参照を除去
56
+ selectedMoments: selectedMoments ? [...selectedMoments] : [], // 選択肢も含める
57
+ }),
58
+ [fvInfos, cnInfo, selectedMoments],
59
+ );
60
+
61
+ const contentHash = useMemo(() => generateSelectionHash(contentData), [contentData]);
62
+
63
+ // キャッシュキーの変更を追跡
64
+ useEffect(() => {
65
+ if (prevContentHashRef.current !== contentHash) {
66
+ if (prevContentHashRef.current) {
67
+ console.log('[useProposalTabsOptimized] ⚠️ Content hash changed - will trigger new queries', {
68
+ oldHash: prevContentHashRef.current.substring(0, 8),
69
+ newHash: contentHash.substring(0, 8),
70
+ selectedMoments,
71
+ fvInfos: !!fvInfos,
72
+ cnInfo: !!cnInfo,
73
+ });
74
+ }
75
+ prevContentHashRef.current = contentHash;
76
+ }
77
+ }, [contentHash, selectedMoments, fvInfos, cnInfo, externalEnabled, isDummyMode]);
78
+
79
+ // コンポーネントのアンマウント時にクエリをキャンセル
80
+ useEffect(() => {
81
+ return () => {
82
+ // 現在実行中のクエリをキャンセル
83
+ if (contentHash) {
84
+ const currentKeys = [
85
+ ['themeByMoment', contentHash],
86
+ ['proposalFv', contentHash],
87
+ ['proposalCn', contentHash],
88
+ ['proposalPrediction', contentHash],
89
+ ['proposalIntent', contentHash],
90
+ ];
91
+
92
+ currentKeys.forEach((key) => {
93
+ queryClient.cancelQueries({ queryKey: key });
94
+ });
95
+ }
96
+ };
97
+ }, [contentHash, queryClient]);
98
+
99
+ // 基本パラメータ
100
+ const baseParams = {
101
+ selectionHash: contentHash, // 内部的にはselectionHashという名前を維持(API互換性のため)
102
+ userEmail: user?.user?.email || null,
103
+ dummyMode: isDummyMode,
104
+ };
105
+
106
+ // Step 1: ThemeByMoment(ダミーモードでも実行)
107
+ const themeByMoment = useThemeByMomentOptimized(
108
+ {
109
+ ...baseParams,
110
+ commonDict: effectiveCommonDict || undefined,
111
+ scoreDict: scoreDict || undefined,
112
+ swot: swotData75 || undefined,
113
+ strategies: swotData78 || {}, // undefinedの���合は空オブジェクトを渡す
114
+ fv_infos: fvInfos as Record<string, unknown> | undefined,
115
+ moments: selectedMoments,
116
+ own_theme: '', // RefreshMomentから取得する必要がある場合は追加
117
+ },
118
+ externalEnabled && (isDummyMode || (!isDummyMode && selectedMoments.length > 0)),
119
+ );
120
+
121
+ // strategy_x_moment の決定
122
+ const strategy_x_moment = useMemo((): ThemeByMomentStrategy | null => {
123
+ if (externalStrategyXMoment) {
124
+ return externalStrategyXMoment;
125
+ }
126
+ // themeByMomentの結果から取得(ダミーモードも含む)
127
+ const result =
128
+ themeByMoment.data && Array.isArray(themeByMoment.data) && themeByMoment.data.length > 2
129
+ ? (themeByMoment.data[2] as ThemeByMomentStrategy)
130
+ : null;
131
+
132
+ return result;
133
+ }, [externalStrategyXMoment, themeByMoment.data]);
134
+
135
+ // APIステータス追跡用のヘルパー関数
136
+ const updateApiStatus = useCallback((apiName: string, status: 'loading' | 'success' | 'error') => {
137
+ if (status === 'loading') {
138
+ apiStartTimesRef.current[apiName] = Date.now();
139
+ setApiStatuses((prev) => ({
140
+ ...prev,
141
+ [apiName]: { duration: 0, status: 'loading' },
142
+ }));
143
+ } else {
144
+ const startTime = apiStartTimesRef.current[apiName];
145
+ const duration = startTime ? (Date.now() - startTime) / 1000 : 0;
146
+ setApiStatuses((prev) => ({
147
+ ...prev,
148
+ [apiName]: { duration, status },
149
+ }));
150
+ }
151
+ }, []);
152
+
153
+ // Step 2: ProposalFv & ProposalCn(並列実行)
154
+ const hasStrategyXMoment = !!strategy_x_moment && Object.keys(strategy_x_moment).length > 0;
155
+ const canStartFvCn = isDummyMode || (hasStrategyXMoment && !themeByMoment.isLoading);
156
+
157
+ const proposalFv = useProposalFv(
158
+ {
159
+ ...baseParams,
160
+ strategy_x_moment: strategy_x_moment || undefined,
161
+ commonDict: effectiveCommonDict || undefined,
162
+ fv_infos: (fvInfos as Record<string, unknown>) || undefined,
163
+ strategies: swotData78 || {}, // undefinedの場合は空オブジェクトを渡す
164
+ cnall_json: (cnInfo as Record<string, unknown>) || undefined,
165
+ url_category_scores: urlCategoryScores || undefined,
166
+ },
167
+ externalEnabled && canStartFvCn,
168
+ );
169
+
170
+ const proposalCn = useProposalCn(
171
+ {
172
+ ...baseParams,
173
+ strategy_x_moment: strategy_x_moment || undefined,
174
+ commonDict: effectiveCommonDict || undefined,
175
+ cnall_json: (cnInfo as Record<string, unknown>) || undefined,
176
+ strategies: swotData78 || {}, // undefinedの場合は空オブジェクトを渡す
177
+ fv_infos: (fvInfos as Record<string, unknown>) || undefined,
178
+ },
179
+ externalEnabled && canStartFvCn,
180
+ );
181
+
182
+ // Step 3: ProposalPrediction & ProposalIntent(FV/CN完了後)
183
+ // シンプルに、FV/CNが成功したらPrediction/Intentを実行
184
+ const canStartPredictionIntent = proposalFv.isSuccess && proposalCn.isSuccess;
185
+
186
+ const proposalPrediction = useProposalPrediction(
187
+ {
188
+ ...baseParams,
189
+ proposal_fv: proposalFv.data as ProposalFvGradioResponse,
190
+ proposal_cn: proposalCn.data as ProposalCnGradioResponse,
191
+ commonDict: effectiveCommonDict || undefined,
192
+ fv_infos: (fvInfos as Record<string, unknown>) || undefined,
193
+ },
194
+ externalEnabled && canStartPredictionIntent,
195
+ );
196
+
197
+ const proposalIntent = useProposalIntent(
198
+ {
199
+ ...baseParams,
200
+ proposal_fv: proposalFv.data as ProposalFvGradioResponse,
201
+ proposal_cn: proposalCn.data as ProposalCnGradioResponse,
202
+ commonDict: effectiveCommonDict || undefined,
203
+ swot: swotData75 || undefined,
204
+ q_summary: summaryData || undefined,
205
+ strategy_x_moment: strategy_x_moment || undefined,
206
+ },
207
+ externalEnabled && canStartPredictionIntent,
208
+ );
209
+
210
+ // 各APIのステータスを追跡
211
+ useEffect(() => {
212
+ if (themeByMoment.isLoading) updateApiStatus('themeByMoment', 'loading');
213
+ else if (themeByMoment.isSuccess) updateApiStatus('themeByMoment', 'success');
214
+ else if (themeByMoment.isError) updateApiStatus('themeByMoment', 'error');
215
+ }, [themeByMoment.isLoading, themeByMoment.isSuccess, themeByMoment.isError, updateApiStatus]);
216
+
217
+ useEffect(() => {
218
+ if (proposalFv.isLoading) updateApiStatus('proposalFv', 'loading');
219
+ else if (proposalFv.isSuccess) updateApiStatus('proposalFv', 'success');
220
+ else if (proposalFv.isError) updateApiStatus('proposalFv', 'error');
221
+ }, [proposalFv.isLoading, proposalFv.isSuccess, proposalFv.isError, updateApiStatus]);
222
+
223
+ useEffect(() => {
224
+ if (proposalCn.isLoading) updateApiStatus('proposalCn', 'loading');
225
+ else if (proposalCn.isSuccess) updateApiStatus('proposalCn', 'success');
226
+ else if (proposalCn.isError) updateApiStatus('proposalCn', 'error');
227
+ }, [proposalCn.isLoading, proposalCn.isSuccess, proposalCn.isError, updateApiStatus]);
228
+
229
+ useEffect(() => {
230
+ if (proposalPrediction.isLoading) updateApiStatus('proposalPrediction', 'loading');
231
+ else if (proposalPrediction.isSuccess) updateApiStatus('proposalPrediction', 'success');
232
+ else if (proposalPrediction.isError) updateApiStatus('proposalPrediction', 'error');
233
+ }, [proposalPrediction.isLoading, proposalPrediction.isSuccess, proposalPrediction.isError, updateApiStatus]);
234
+
235
+ useEffect(() => {
236
+ if (proposalIntent.isLoading) updateApiStatus('proposalIntent', 'loading');
237
+ else if (proposalIntent.isSuccess) updateApiStatus('proposalIntent', 'success');
238
+ else if (proposalIntent.isError) {
239
+ updateApiStatus('proposalIntent', 'error');
240
+ // エラーの詳細をログ出力(開発環境のみ)
241
+ if (process.env.NODE_ENV === 'development') {
242
+ console.error('[useProposalTabsOptimized] proposalIntent error:', proposalIntent.error);
243
+ }
244
+ }
245
+ }, [proposalIntent.isLoading, proposalIntent.isSuccess, proposalIntent.isError, proposalIntent.error, updateApiStatus]);
246
+
247
+ // データの変換(シリアライズ可能なオブジェクトのみを返すように安全に処理)
248
+ const translatedData = useMemo(() => {
249
+ if (!proposalFv.data || !proposalCn.data || !proposalIntent.data || !proposalPrediction.data) {
250
+ return null;
251
+ }
252
+
253
+ try {
254
+ const result = ProposalTranslator.translateToTabsData(
255
+ proposalFv.data as ProposalFvGradioResponse,
256
+ proposalCn.data as ProposalCnGradioResponse,
257
+ proposalIntent.data as ProposalIntentGradioResponse,
258
+ proposalPrediction.data as ProposalPredictionGradioResponse,
259
+ strategy_x_moment || undefined,
260
+ );
261
+
262
+ return result;
263
+ } catch (error) {
264
+ if (process.env.NODE_ENV === 'development') {
265
+ console.error('[useProposalTabsOptimized] Translation error:', error);
266
+ }
267
+ return null;
268
+ }
269
+ }, [proposalFv.data, proposalCn.data, proposalIntent.data, proposalPrediction.data, strategy_x_moment]);
270
+
271
+ // 統合されたローディング状態
272
+ const isLoading =
273
+ themeByMoment.isLoading || proposalFv.isLoading || proposalCn.isLoading || proposalPrediction.isLoading || proposalIntent.isLoading;
274
+
275
+ // 統合されたエラー状態
276
+ const isError = themeByMoment.isError || proposalFv.isError || proposalCn.isError || proposalPrediction.isError || proposalIntent.isError;
277
+
278
+ // エラーメッセージの取得
279
+ const getErrorMessage = useCallback(() => {
280
+ if (themeByMoment.isError) {
281
+ // themeByMomentのエラーメッセージを優先的に返す
282
+ const errorMsg = themeByMoment.error?.message || 'テーマ取得に失敗しました';
283
+ return errorMsg;
284
+ }
285
+ if (proposalFv.isError) return 'FV提案の取得に失敗しました';
286
+ if (proposalCn.isError) return 'コンテンツ提案の取得に失敗しました';
287
+ if (proposalPrediction.isError) return '予測データの取得に失敗しました';
288
+ if (proposalIntent.isError) return 'インテント提案の取得に失敗しました';
289
+ return 'データの取得に失敗しました';
290
+ }, [
291
+ themeByMoment.isError,
292
+ themeByMoment.error?.message,
293
+ proposalFv.isError,
294
+ proposalCn.isError,
295
+ proposalPrediction.isError,
296
+ proposalIntent.isError,
297
+ ]);
298
+
299
+ // 個別の手動リトライ関数
300
+ const refetchFailed = () => {
301
+ if (themeByMoment.isError) themeByMoment.refetch();
302
+ if (proposalFv.isError) proposalFv.refetch();
303
+ if (proposalCn.isError) proposalCn.refetch();
304
+ if (proposalPrediction.isError) proposalPrediction.refetch();
305
+ if (proposalIntent.isError) proposalIntent.refetch();
306
+ };
307
+
308
+ // 安全にシリアライズ可能なデータのみを返す
309
+ const safeRawData = useMemo(() => {
310
+ if (!proposalFv.data || !proposalCn.data || !proposalIntent.data || !proposalPrediction.data) {
311
+ return undefined;
312
+ }
313
+
314
+ try {
315
+ // React Queryからの返り値を確実にシリアライズして非シリアライズ可能オブジェクトを除去
316
+ const serializedData = {
317
+ proposal_fv: proposalFv.data,
318
+ proposal_cn: proposalCn.data,
319
+ proposal_intent: proposalIntent.data,
320
+ proposal_prediction: proposalPrediction.data,
321
+ strategy_x_moment: strategy_x_moment || undefined,
322
+ };
323
+
324
+ // JSON.parse(JSON.stringify())でシリアライズ
325
+ return JSON.parse(JSON.stringify(serializedData));
326
+ } catch (error) {
327
+ if (process.env.NODE_ENV === 'development') {
328
+ console.error('[useProposalTabsOptimized] Serialization error in safeRawData:', error);
329
+ }
330
+ return undefined;
331
+ }
332
+ }, [proposalFv.data, proposalCn.data, proposalIntent.data, proposalPrediction.data, strategy_x_moment]);
333
+
334
+ // 返却値の構造は既存のuseProposalTabsと互換性を保つ
335
+ const result: ExtendedProposalTabsResponse | undefined = translatedData
336
+ ? {
337
+ data: translatedData,
338
+ rawData: safeRawData,
339
+ }
340
+ : undefined;
341
+
342
+ // 最終的な返り値もシリアライズして確実に安全にする
343
+ const serializedResult = useMemo(() => {
344
+ if (!result) return undefined;
345
+
346
+ try {
347
+ return JSON.parse(
348
+ JSON.stringify({
349
+ data: result.data,
350
+ rawData: result.rawData,
351
+ }),
352
+ );
353
+ } catch (error) {
354
+ if (process.env.NODE_ENV === 'development') {
355
+ console.error('[useProposalTabsOptimized] Final serialization error:', error);
356
+ }
357
+ return undefined;
358
+ }
359
+ }, [result]);
360
+
361
+ return {
362
+ data: serializedResult?.data,
363
+ rawData: serializedResult?.rawData,
364
+ isLoading,
365
+ isError,
366
+ error: isError ? new Error(getErrorMessage()) : null,
367
+ refetch: refetchFailed,
368
+ apiStatuses, // API実行時間とステータス情報
369
+ selectionHash: contentHash, // コンテンツハッシュを外部で利用可能にする(互換性のためselectionHashという名前で公開)
370
+ // queries オブジェクトを削除(React Queryの内部オブジェクトを含みシリアライズできない)
371
+ };
372
+ }
api-client/proposal/proposal/queries.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { getThemeByMoment } from '@/services/gradio-with-dummy';
3
+ import { useGlobalStore } from '@/store/global';
4
+ import { useQuery } from '@tanstack/react-query';
5
+
6
+ // useProposalTabs関数は削除されました。
7
+ // useProposalTabsOptimizedを使用してください。
8
+
9
+ interface UseThemeByMomentParams {
10
+ commonDict?: Record<string, unknown>;
11
+ scoreDict?: Record<string, unknown>;
12
+ swot?: Record<string, unknown>;
13
+ strategies?: Record<string, unknown>;
14
+ moments?: string[];
15
+ fv_infos?: Record<string, unknown>;
16
+ own_theme?: string;
17
+ userEmail?: string;
18
+ enabled?: boolean;
19
+ }
20
+
21
+ /**
22
+ * getThemeByMomentをReact Queryでキャッシュ化するフック
23
+ * 選択されたモーメントが同じ場合はキャッシュを使用
24
+ */
25
+ export function useThemeByMoment(params: UseThemeByMomentParams) {
26
+ const { commonDict, scoreDict, swot, strategies, moments, fv_infos, own_theme, userEmail, enabled = true } = params;
27
+ const { userIdentifier } = useGlobalStore();
28
+
29
+ return useQuery({
30
+ ...createQueryOptions(),
31
+ queryKey: [
32
+ 'themeByMoment',
33
+ moments, // 選択されたモーメント(これが主要なキャッシュキー)
34
+ commonDict,
35
+ scoreDict,
36
+ swot,
37
+ strategies,
38
+ fv_infos,
39
+ own_theme,
40
+ userEmail,
41
+ ],
42
+ queryFn: async () => {
43
+ if (!commonDict || !scoreDict || !swot || !moments || moments.length === 0) {
44
+ throw new Error('Required parameters are missing');
45
+ }
46
+
47
+ const response = await getThemeByMoment(
48
+ {
49
+ commonDict,
50
+ scoreDict,
51
+ swot,
52
+ strategies: strategies || {},
53
+ moments,
54
+ fv_infos: fv_infos || undefined,
55
+ own_theme: own_theme || '',
56
+ },
57
+ userEmail || '',
58
+ userIdentifier,
59
+ );
60
+
61
+ return response;
62
+ },
63
+ enabled: enabled && !!commonDict && !!scoreDict && !!swot && !!moments && moments.length > 0,
64
+ });
65
+ }
api-client/proposal/proposal/translator.spec.ts ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ProposalTranslator } from './translator';
3
+ import type {
4
+ ContentSection,
5
+ ProposalCnGradioResponse,
6
+ ProposalFvGradioResponse,
7
+ ProposalIntentGradioResponse,
8
+ ProposalPredictionGradioResponse,
9
+ ThemeByMomentStrategy,
10
+ } from '@/schema/proposal';
11
+
12
+ describe('ProposalTranslator', () => {
13
+ const translator = ProposalTranslator;
14
+
15
+ describe('translateCnDataToTabs', () => {
16
+ it('ダミーデータの各戦略×フェーズが正しいタブにマッピングされる', () => {
17
+ // ダミーデータと同じ構造のテストデータ
18
+ const testData: ProposalCnGradioResponse = {
19
+ 積極化戦略: {
20
+ ニーズ1: {
21
+ cn: [
22
+ { 大区分: 'News/更新情報', 中区分: 'テスト1', 小区分json: [], 制作意図: '' },
23
+ { 大区分: '商品/サービスの特徴', 中区分: 'テスト2', 小区分json: [], 制作意図: '' },
24
+ { 大区分: '商品/サービスの詳細情報', 中区分: 'テスト3', 小区分json: [], 制作意図: '' },
25
+ { 大区分: 'メディア掲載情報', 中区分: 'テスト4', 小区分json: [], 制作意図: '' },
26
+ ],
27
+ },
28
+ ニーズ2: {
29
+ cn: [
30
+ { 大区分: '問題提起/共感', 中区分: 'テスト5', 小区分json: [], 制作意図: '' },
31
+ { 大区分: 'シミュレーション', 中区分: 'テスト6', 小区分json: [], 制作意図: '' },
32
+ { 大区分: '解決策/ベネフィット提示', 中区分: 'テスト7', 小区分json: [], 制作意図: '' },
33
+ { 大区分: 'SPオファー/限定特典', 中区分: 'テスト8', 小区分json: [], 制作意図: '' },
34
+ ],
35
+ },
36
+ ニーズ3: {
37
+ cn: [
38
+ { 大区分: '料金/プラン', 中区分: 'テスト9', 小区分json: [], 制作意図: '' },
39
+ { 大区分: '成功事例/ユーザーボイス紹介', 中区分: 'テスト10', 小区分json: [], 制作意図: '' },
40
+ { 大区分: 'ご利用の流れ/ステップ', 中区分: 'テスト11', 小区分json: [], 制作意図: '' },
41
+ { 大区分: '競合比較', 中区分: 'テスト12', 小区分json: [], 制作意図: '' },
42
+ ],
43
+ },
44
+ },
45
+ 改善戦略: {
46
+ ニーズ4: {
47
+ cn: [
48
+ { 大区分: 'スタッフ・メンバー紹介', 中区分: 'テスト13', 小区分json: [], 制作意図: '' },
49
+ { 大区分: '権威訴求', 中区分: 'テスト14', 小区分json: [], 制作意図: '' },
50
+ { 大区分: '店舗情報', 中区分: 'テスト15', 小区分json: [], 制作意図: '' },
51
+ { 大区分: '活用シーン', 中区分: 'テスト16', 小区分json: [], 制作意図: '' },
52
+ { 大区分: 'FAQ/よくある質問', 中区分: 'テスト17', 小区分json: [], 制作意図: '' },
53
+ ],
54
+ },
55
+ ニーズ5: {
56
+ cn: [
57
+ { 大区分: 'News/更新情報', 中区分: 'テスト18', 小区分json: [], 制作意図: '' },
58
+ { 大区分: '商品/サービスの特徴', 中区分: 'テスト19', 小区分json: [], 制作意図: '' },
59
+ { 大区分: '商品/サービスの詳細情報', 中区分: 'テスト20', 小区分json: [], 制作意図: '' },
60
+ { 大区分: 'メディア掲載情報', 中区分: 'テスト21', 小区分json: [], 制作意図: '' },
61
+ { 大区分: '問題提起/共感', 中区分: 'テスト22', 小区分json: [], 制作意図: '' },
62
+ ],
63
+ },
64
+ ニーズ6: {
65
+ cn: [
66
+ { 大区分: 'シミュレーション', 中区分: 'テスト23', 小区分json: [], 制作意図: '' },
67
+ { 大区分: '解決策/ベネフィット提示', 中区分: 'テスト24', 小区分json: [], 制作意図: '' },
68
+ { 大区分: 'SPオファー/限定特典', 中区分: 'テスト25', 小区分json: [], 制作意図: '' },
69
+ { 大区分: '料金/プラン', 中区分: 'テスト26', 小区分json: [], 制作意図: '' },
70
+ { 大区分: '成功事例/ユーザーボイス紹介', 中区分: 'テスト27', 小区分json: [], 制作意図: '' },
71
+ ],
72
+ },
73
+ },
74
+ 差別化戦略: {
75
+ ニーズ7: {
76
+ cn: [
77
+ { 大区分: 'ご利用の流れ/ステップ', 中区分: 'テスト28', 小区分json: [], 制作意図: '' },
78
+ { 大区分: '競合比較', 中区分: 'テスト29', 小区分json: [], 制作意図: '' },
79
+ { 大区分: 'スタッフ・メンバー紹介', 中区分: 'テスト30', 小区分json: [], 制作意図: '' },
80
+ { 大区分: '権威訴求', 中区分: 'テスト31', 小区分json: [], 制作意図: '' },
81
+ { 大区分: '店舗情報', 中区分: 'テスト32', 小区分json: [], 制作意図: '' },
82
+ { 大区分: '活用シーン', 中区分: 'テスト33', 小区分json: [], 制作意図: '' },
83
+ ],
84
+ },
85
+ ニーズ8: {
86
+ cn: [
87
+ { 大区分: 'FAQ/よくある質問', 中区分: 'テスト34', 小区分json: [], 制作意図: '' },
88
+ { 大区分: 'News/更新情報', 中区分: 'テスト35', 小区分json: [], 制作意図: '' },
89
+ { 大区分: '商品/サービスの特徴', 中区分: 'テスト36', 小区分json: [], 制作意図: '' },
90
+ { 大区分: '商品/サービスの詳細情報', 中区分: 'テスト37', 小区分json: [], 制作意図: '' },
91
+ { 大区分: 'メディア掲載情報', 中区分: 'テスト38', 小区分json: [], 制作意図: '' },
92
+ { 大区分: '問題提起/共感', 中区分: 'テスト39', 小区分json: [], 制作意図: '' },
93
+ ],
94
+ },
95
+ ニーズ9: {
96
+ cn: [
97
+ { 大区分: 'シミュレーション', 中区分: 'テスト40', 小区分json: [], 制作意図: '' },
98
+ { 大区分: '解決策/ベネフィット提示', 中区分: 'テスト41', 小区分json: [], 制作意図: '' },
99
+ { 大区分: 'SPオファー/限定特典', 中区分: 'テスト42', 小区分json: [], 制作意図: '' },
100
+ { 大区分: '料金/プラン', 中区分: 'テスト43', 小区分json: [], 制作意図: '' },
101
+ { 大区分: '成功事例/ユーザーボイス紹介', 中区分: 'テスト44', 小区分json: [], 制作意図: '' },
102
+ { 大区分: 'ご利用の流れ/ステップ', 中区分: 'テスト45', 小区分json: [], 制作意図: '' },
103
+ ],
104
+ },
105
+ },
106
+ };
107
+
108
+ const result = translator.translateCnDataToTabs(testData);
109
+
110
+ // 9つのタブが生成されること
111
+ expect(Object.keys(result)).toHaveLength(9);
112
+ expect(Object.keys(result)).toEqual(['A案', 'B案', 'C案', 'D案', 'E案', 'F案', 'G案', 'H案', 'I案']);
113
+
114
+ // 各タブのセクション数を確認
115
+ expect(result['A案']).toHaveLength(4); // 積極化戦略・ニーズ1
116
+ expect(result['B案']).toHaveLength(4); // 積極化戦略・ニーズ2
117
+ expect(result['C案']).toHaveLength(4); // 積極化戦略・ニーズ3
118
+ expect(result['D案']).toHaveLength(5); // 改善戦略・ニーズ4
119
+ expect(result['E案']).toHaveLength(5); // 改善戦略・ニーズ5
120
+ expect(result['F案']).toHaveLength(5); // 改善戦略・ニーズ6
121
+ expect(result['G案']).toHaveLength(6); // 差別化戦略・ニーズ7
122
+ expect(result['H案']).toHaveLength(6); // 差別化戦略・ニーズ8
123
+ expect(result['I案']).toHaveLength(6); // 差別化戦略・ニーズ9
124
+
125
+ // 合計セクション数の確認
126
+ const totalSections = Object.values(result).reduce((sum, sections) => sum + sections.length, 0);
127
+ expect(totalSections).toBe(45); // 4+4+4+5+5+5+6+6+6 = 45
128
+ });
129
+
130
+ it('10セクション以上のデータが正しく処理される', () => {
131
+ const testData: ProposalCnGradioResponse = {
132
+ 積極化戦略: {
133
+ 大量セクションのニーズ: {
134
+ cn: Array.from({ length: 10 }, (_, i) => ({
135
+ 大区分: 'News/更新情報' as ContentSection['大区分'],
136
+ 中区分: `セクション${i + 1}の内容`,
137
+ 小区分json: [],
138
+ 制作意図: '',
139
+ })),
140
+ },
141
+ },
142
+ 改善戦略: {},
143
+ 差別化戦略: {},
144
+ };
145
+
146
+ const result = translator.translateCnDataToTabs(testData);
147
+
148
+ // A案に10セクション全てが含まれること
149
+ expect(result['A案']).toHaveLength(10);
150
+
151
+ // 各セクションの内容が正しいこと
152
+ result['A案'].forEach((section, index) => {
153
+ expect(section.大区分).toBe('News/更新情報');
154
+ expect(section.中区分).toBe(`セクション${index + 1}の内容`);
155
+ });
156
+ });
157
+
158
+ it('小区分jsonのフィールド変換が正しく行われる', () => {
159
+ const testData: ProposalCnGradioResponse = {
160
+ 積極化戦略: {
161
+ テストニーズ: {
162
+ cn: [
163
+ {
164
+ 大区分: 'News/更新情報',
165
+ 中区分: '最新ニュース',
166
+ 小区分json: [
167
+ {
168
+ 見出し: {
169
+ value: 'タイトル1',
170
+ 情報源: { 他社: '', 自社: '' }
171
+ },
172
+ 内容: {
173
+ value: 'コンテンツ1',
174
+ 情報源: { 他社: '', 自社: '' }
175
+ },
176
+ 注釈: {
177
+ value: '注釈1',
178
+ 情報源: { 他社: '', 自社: '' }
179
+ },
180
+ ユーザー情報: {
181
+ value: 'ユーザー1',
182
+ 情報源: { 他社: '', 自社: '' }
183
+ },
184
+ },
185
+ ],
186
+ 制作意図: 'テスト意図',
187
+ },
188
+ ],
189
+ },
190
+ },
191
+ 改善戦略: {},
192
+ 差別化戦略: {},
193
+ };
194
+
195
+ const result = translator.translateCnDataToTabs(testData);
196
+
197
+ const section = result['A案'][0];
198
+ expect(section.小区分json[0]).toEqual({
199
+ 見出し: {
200
+ value: 'タイトル1',
201
+ 情報源: { 他社: '', 自社: '' }
202
+ },
203
+ 内容: {
204
+ value: 'コンテンツ1',
205
+ 情報源: { 他社: '', 自社: '' }
206
+ },
207
+ 注釈: {
208
+ value: '注釈1',
209
+ 情報源: { 他社: '', 自社: '' }
210
+ },
211
+ ユーザー情報: {
212
+ value: 'ユーザー1',
213
+ 情報源: { 他社: '', 自社: '' }
214
+ },
215
+ });
216
+ });
217
+
218
+ it('cnフィールドが存在しない場合、空配列を返す', () => {
219
+ const testData: ProposalCnGradioResponse = {
220
+ 積極化戦略: {
221
+ cnなしニーズ: {} as any,
222
+ },
223
+ 改善戦略: {},
224
+ 差別化戦略: {},
225
+ };
226
+
227
+ const result = translator.translateCnDataToTabs(testData);
228
+ expect(result['A案']).toEqual([]);
229
+ });
230
+
231
+ it('空の戦略データでもエラーにならない', () => {
232
+ const testData: ProposalCnGradioResponse = {
233
+ 積極化戦略: {},
234
+ 改善戦略: {},
235
+ 差別化戦略: {},
236
+ };
237
+
238
+ expect(() => translator.translateCnDataToTabs(testData)).not.toThrow();
239
+ const result = translator.translateCnDataToTabs(testData);
240
+ expect(Object.keys(result)).toHaveLength(0);
241
+ });
242
+
243
+ it('15セクションのデータが切り捨てられずに処理される', () => {
244
+ const testData: ProposalCnGradioResponse = {
245
+ 積極化戦略: {
246
+ 超大量セクション: {
247
+ cn: Array.from({ length: 15 }, (_, i) => ({
248
+ 大区分: 'News/更新情報' as ContentSection['大区分'],
249
+ 中区分: `中区分${i + 1}`,
250
+ 小区分json: [
251
+ {
252
+ 見出し: {
253
+ value: `見出し${i + 1}`,
254
+ 情報源: { 他社: '', 自社: '' }
255
+ },
256
+ 内容: {
257
+ value: `内容${i + 1}`,
258
+ 情報源: { 他社: '', 自社: '' }
259
+ },
260
+ },
261
+ ],
262
+ 制作意図: `意図${i + 1}`,
263
+ })),
264
+ },
265
+ },
266
+ 改善戦略: {},
267
+ 差別化戦略: {},
268
+ };
269
+
270
+ const result = translator.translateCnDataToTabs(testData);
271
+
272
+ // 15セクション全てが含まれることを確認
273
+ expect(result['A案']).toHaveLength(15);
274
+
275
+ // 最初と最後のセクションの内容を確認
276
+ expect(result['A案'][0].大区分).toBe('News/更新情報');
277
+ expect(result['A案'][14].大区分).toBe('News/更新情報');
278
+
279
+ // 全セクションが順番通りに存在することを確認
280
+ result['A案'].forEach((section, index) => {
281
+ expect(section.大区分).toBe('News/更新情報');
282
+ expect(section.中区分).toBe(`中区分${index + 1}`);
283
+ expect(section.制作意図).toBe(`意図${index + 1}`);
284
+ });
285
+ });
286
+ });
287
+
288
+ describe('translateToTabsData', () => {
289
+ it('すべてのタブにprediction/intentデータが正しくマッピングされる', () => {
290
+ // ダミーデータと同じ構造のテストデータ
291
+ const proposalFvData: ProposalFvGradioResponse = {
292
+ 積極化戦略: {
293
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
294
+ fv: { コピー: { メインコピー: '積極化AI1' } },
295
+ },
296
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
297
+ fv: { コピー: { メインコピー: '積極化デジタル2' } },
298
+ },
299
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
300
+ fv: { コピー: { メインコピー: '積極化顧客3' } },
301
+ },
302
+ },
303
+ 改善戦略: {
304
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
305
+ fv: { コピー: { メインコピー: '改善AI4' } },
306
+ },
307
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
308
+ fv: { コピー: { メインコピー: '改善デジタル5' } },
309
+ },
310
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
311
+ fv: { コピー: { メインコピー: '改善顧客6' } },
312
+ },
313
+ },
314
+ 差別化戦略: {
315
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
316
+ fv: { コピー: { メインコピー: '差別化AI7' } },
317
+ },
318
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
319
+ fv: { コピー: { メインコピー: '差別化デジタル8' } },
320
+ },
321
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
322
+ fv: { コピー: { メインコピー: '差別化顧客9' } },
323
+ },
324
+ },
325
+ };
326
+
327
+ const proposalCnData: ProposalCnGradioResponse = {
328
+ 積極化戦略: {
329
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
330
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN1', 小区分json: [], 制作意図: '' }],
331
+ },
332
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
333
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN2', 小区分json: [], 制作意図: '' }],
334
+ },
335
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
336
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN3', 小区分json: [], 制作意図: '' }],
337
+ },
338
+ },
339
+ 改善戦略: {
340
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
341
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN4', 小区分json: [], 制作意図: '' }],
342
+ },
343
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
344
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN5', 小区分json: [], 制作意図: '' }],
345
+ },
346
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
347
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN6', 小区分json: [], 制作意図: '' }],
348
+ },
349
+ },
350
+ 差別化戦略: {
351
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
352
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN7', 小区分json: [], 制作意図: '' }],
353
+ },
354
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
355
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN8', 小区分json: [], 制作意図: '' }],
356
+ },
357
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
358
+ cn: [{ 大区分: 'News/更新情報', 中区分: 'CN9', 小区分json: [], 制作意図: '' }],
359
+ },
360
+ },
361
+ };
362
+
363
+ const proposalIntentData: ProposalIntentGradioResponse = [
364
+ {
365
+ 積極化戦略: {
366
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
367
+ intent_all: { FV_intent: 'FV1', CN_intent: 'CN1' },
368
+ },
369
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
370
+ intent_all: { FV_intent: 'FV2', CN_intent: 'CN2' },
371
+ },
372
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
373
+ intent_all: { FV_intent: 'FV3', CN_intent: 'CN3' },
374
+ },
375
+ },
376
+ 改善戦略: {
377
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
378
+ intent_all: { FV_intent: 'FV4', CN_intent: 'CN4' },
379
+ },
380
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
381
+ intent_all: { FV_intent: 'FV5', CN_intent: 'CN5' },
382
+ },
383
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
384
+ intent_all: { FV_intent: 'FV6', CN_intent: 'CN6' },
385
+ },
386
+ },
387
+ 差別化戦略: {
388
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
389
+ intent_all: { FV_intent: 'FV7', CN_intent: 'CN7' },
390
+ },
391
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
392
+ intent_all: { FV_intent: 'FV8', CN_intent: 'CN8' },
393
+ },
394
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
395
+ intent_all: { FV_intent: 'FV9', CN_intent: 'CN9' },
396
+ },
397
+ },
398
+ },
399
+ ];
400
+
401
+ const proposalPredictionData: ProposalPredictionGradioResponse = [
402
+ {
403
+ 積極化戦略: {
404
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
405
+ prediction: 3,
406
+ },
407
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
408
+ prediction: 2,
409
+ },
410
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた矬間)': {
411
+ prediction: 0,
412
+ },
413
+ },
414
+ 改善戦略: {
415
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
416
+ prediction: 2,
417
+ },
418
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
419
+ prediction: 3,
420
+ },
421
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
422
+ prediction: 3,
423
+ },
424
+ },
425
+ 差別化戦略: {
426
+ 'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
427
+ prediction: 1,
428
+ },
429
+ 'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
430
+ prediction: 1,
431
+ },
432
+ '顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた矬間)': {
433
+ prediction: 0,
434
+ },
435
+ },
436
+ },
437
+ ];
438
+
439
+ const result = ProposalTranslator.translateToTabsData(
440
+ proposalFvData,
441
+ proposalCnData,
442
+ proposalIntentData,
443
+ proposalPredictionData,
444
+ undefined,
445
+ );
446
+
447
+ // resultがnullでないことを確認
448
+ expect(result).not.toBeNull();
449
+
450
+ // 9つのタブが生成されること
451
+ expect(Object.keys(result!)).toHaveLength(9);
452
+ expect(Object.keys(result!)).toEqual(['A案', 'B案', 'C案', 'D案', 'E案', 'F案', 'G案', 'H案', 'I案']);
453
+
454
+ // すべてのタブにpredictionとintentデータが存在することを確認
455
+ Object.entries(result!).forEach(([tabName, tabData]) => {
456
+ // intentデータが存在
457
+ expect(tabData.intent).toBeDefined();
458
+ expect(tabData.intent?.prediction).toBeDefined();
459
+ expect(typeof tabData.intent?.prediction).toBe('number');
460
+ expect(tabData.intent?.FV_intent).toBeDefined();
461
+ expect(tabData.intent?.CN_intent).toBeDefined();
462
+
463
+ // FVデータが存在
464
+ expect(tabData.fv).toBeDefined();
465
+ expect(tabData.fv?.data?.fv?.['コピー']?.['メインコピー']).toBeDefined();
466
+
467
+ // CNデータが存在
468
+ expect(tabData.cn).toBeDefined();
469
+ expect(tabData.cn!.length).toBeGreaterThan(0);
470
+ });
471
+
472
+ // prediction値によるソートが正しく行われていること
473
+ // prediction値が高い順: 積極化AI(3) > 改善デジタル(3) > 改善顧客(3) > 積極化デジタル(2) > 改善AI(2) > 差別化AI(1) > 差別化デジタル(1) > 積極化顧客(0) > 差別化顧客(0)
474
+ expect(result!['A案'].intent?.prediction).toBe(3);
475
+ expect(result!['A案'].fv?.data?.fv?.['コピー']?.['メインコピー']).toContain('積極化AI');
476
+
477
+ // H案、I案にもデータが存在することを特に確認
478
+ expect(result!['H案'].intent?.prediction).toBeDefined();
479
+ expect(result!['H案'].intent?.FV_intent).toBeDefined();
480
+ expect(result!['H案'].intent?.CN_intent).toBeDefined();
481
+ expect(result!['H案'].fv).toBeDefined();
482
+ expect(result!['H案'].cn).toBeDefined();
483
+
484
+ expect(result!['I案'].intent?.prediction).toBeDefined();
485
+ expect(result!['I案'].intent?.FV_intent).toBeDefined();
486
+ expect(result!['I案'].intent?.CN_intent).toBeDefined();
487
+ expect(result!['I案'].fv).toBeDefined();
488
+ expect(result!['I案'].cn).toBeDefined();
489
+ });
490
+ });
491
+ });
api-client/proposal/proposal/translator.ts ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ProposalCnGradioResponse } from '@/schema/gradio-proxy/proposal-cn';
2
+ import type { ProposalFvGradioResponse } from '@/schema/gradio-proxy/proposal-fv';
3
+ import type { ProposalIntentGradioResponse } from '@/schema/gradio-proxy/proposal-intent';
4
+ import type { ProposalPredictionGradioResponse } from '@/schema/gradio-proxy/proposal-prediction';
5
+ import type { ThemeByMomentStrategy } from '@/schema/gradio-proxy/theme-by-moment';
6
+ import type { ContentSection, FvData, ProposalTabData, ProposalTabsResponse } from '@/schema/proposal';
7
+ import { findWithNormalizedKey } from '@/utils/normalize-strategy-keys';
8
+
9
+ export const ProposalTranslator = {
10
+ /**
11
+ * FVデータをタブ形式に変換
12
+ * 戦略とニーズをキーとして格納
13
+ */
14
+ translateFvDataToTabs(data: ProposalFvGradioResponse): Record<string, FvData> {
15
+ const result: Record<string, FvData> = {};
16
+
17
+ // データを変換(戦略とニーズをキーとして格納)
18
+ Object.entries(data).forEach(([strategy]) => {
19
+ const strategyData = data[strategy as keyof ProposalFvGradioResponse];
20
+
21
+ if (strategyData) {
22
+ Object.entries(strategyData).forEach(([need, needData]) => {
23
+ const key = `${strategy}_${need}`;
24
+ result[key] = {
25
+ strategy,
26
+ need,
27
+ data: {
28
+ fv: needData.fv,
29
+ },
30
+ };
31
+ });
32
+ }
33
+ });
34
+
35
+ return result;
36
+ },
37
+
38
+ /**
39
+ * CNデータをタブ形式に変換
40
+ * 戦略とニーズをキーとして格納
41
+ */
42
+ translateCnDataToTabs(data: ProposalCnGradioResponse): Record<string, ContentSection[]> {
43
+ const result: Record<string, ContentSection[]> = {};
44
+
45
+ // 戦略→ニーズ→cn の階層構造でデータを処理
46
+ const strategies = ['積極化戦略', '改善戦略', '差別化戦略'];
47
+
48
+ strategies.forEach((strategy) => {
49
+ if (data[strategy as keyof ProposalCnGradioResponse]) {
50
+ const strategyData = data[strategy as keyof ProposalCnGradioResponse];
51
+
52
+ if (strategyData && typeof strategyData === 'object') {
53
+ Object.entries(strategyData).forEach(([need, needData]) => {
54
+ const key = `${strategy}_${need}`;
55
+
56
+ // cnフィールドから配列を取得
57
+ if (needData && typeof needData === 'object' && 'cn' in needData) {
58
+ const cnData = (needData as any).cn;
59
+ if (Array.isArray(cnData)) {
60
+ result[key] = cnData.map((section: any) => {
61
+ const convertedSection: any = {
62
+ 大区分: section.大区分 as any,
63
+ 中区分: section.中区分,
64
+ 小区分json: [],
65
+ 制作意図: section.制作意図 || '',
66
+ };
67
+
68
+ if (Array.isArray(section.小区分json)) {
69
+ convertedSection.小区分json = section.小区分json.map((item: any) => {
70
+ // 各フィールドを統一形式(オブジェクト形式)に変換
71
+ const convertedItem: any = {};
72
+
73
+ // 見出しの変換
74
+ if (item.見出し !== undefined) {
75
+ if (typeof item.見出し === 'string') {
76
+ convertedItem.見出し = { value: item.見出し };
77
+ } else if (item.見出し && typeof item.見出し === 'object' && 'value' in item.見出し) {
78
+ convertedItem.見出し = item.見出し;
79
+ } else {
80
+ convertedItem.見出し = { value: '' };
81
+ }
82
+ } else {
83
+ convertedItem.見出し = { value: '' };
84
+ }
85
+
86
+ // 内容の変換
87
+ if (item.内容 !== undefined) {
88
+ if (typeof item.内容 === 'string') {
89
+ convertedItem.内容 = { value: item.内容 };
90
+ } else if (item.内容 && typeof item.内容 === 'object' && 'value' in item.内容) {
91
+ convertedItem.内容 = item.内容;
92
+ } else {
93
+ convertedItem.内容 = { value: '' };
94
+ }
95
+ } else {
96
+ convertedItem.内容 = { value: '' };
97
+ }
98
+
99
+ // 注釈の変換(オプショナル)
100
+ if (item.注釈 !== undefined && item.注釈 !== null) {
101
+ if (typeof item.注釈 === 'string') {
102
+ convertedItem.注釈 = { value: item.注釈 };
103
+ } else if (item.注釈 && typeof item.注釈 === 'object' && 'value' in item.注釈) {
104
+ convertedItem.注釈 = item.注釈;
105
+ }
106
+ }
107
+
108
+ // ユーザー情報の変換(オプショナル)
109
+ if (item.ユーザー情報 !== undefined && item.ユーザー情報 !== null) {
110
+ if (typeof item.ユーザー情報 === 'string') {
111
+ convertedItem.ユーザー情報 = { value: item.ユーザー情報 };
112
+ } else if (item.ユーザー情報 && typeof item.ユーザー情報 === 'object' && 'value' in item.ユーザー情報) {
113
+ convertedItem.ユーザー情報 = item.ユーザー情報;
114
+ }
115
+ }
116
+
117
+ // その他のフィールドをそのまま保持
118
+ Object.keys(item).forEach((key) => {
119
+ if (!['見出し', '内容', '注釈', 'ユーザー情報'].includes(key)) {
120
+ convertedItem[key] = item[key];
121
+ }
122
+ });
123
+
124
+ return convertedItem;
125
+ });
126
+ }
127
+
128
+ return convertedSection;
129
+ });
130
+ } else {
131
+ console.warn(`CNデータのcnフィールドが配列ではありません: ${strategy}/${need}`, cnData);
132
+ result[key] = [];
133
+ }
134
+ } else {
135
+ console.warn(`CNデータにcnフィールドがありません: ${strategy}/${need}`, needData);
136
+ result[key] = [];
137
+ }
138
+ });
139
+ }
140
+ }
141
+ });
142
+
143
+ return result;
144
+ },
145
+
146
+ /**
147
+ * 全てのProposalデータをタブ形式に統合変換
148
+ */
149
+ translateToTabsData(
150
+ proposalFvData: ProposalFvGradioResponse,
151
+ proposalCnData: ProposalCnGradioResponse,
152
+ proposalIntentData: ProposalIntentGradioResponse,
153
+ proposalPredictionData: ProposalPredictionGradioResponse,
154
+ strategyXMoment: ThemeByMomentStrategy | undefined,
155
+ ): ProposalTabsResponse | null {
156
+ // 本番環境でも確実にログを出力(console.warnを使用)
157
+ console.warn('[translateToTabsData] START - Processing proposal data');
158
+ console.warn('[translateToTabsData] FV data strategies:', Object.keys(proposalFvData || {}));
159
+
160
+ // APIデータの完全性チェック(配列の場合も考慮)
161
+ const checkValidData = (data: ProposalIntentGradioResponse | ProposalPredictionGradioResponse): boolean => {
162
+ if (!data) return false;
163
+ if (Array.isArray(data)) {
164
+ return data.length > 0 && data[0] && Object.keys(data[0]).length > 0;
165
+ }
166
+ return Object.keys(data).length > 0;
167
+ };
168
+
169
+ const hasValidPrediction = checkValidData(proposalPredictionData);
170
+ const hasValidIntent = checkValidData(proposalIntentData);
171
+
172
+ if (!hasValidPrediction || !hasValidIntent) {
173
+ console.error('[translateToTabsData] 必須データが不完全です:', {
174
+ hasValidPrediction,
175
+ hasValidIntent,
176
+ predictionData: proposalPredictionData,
177
+ intentData: proposalIntentData,
178
+ });
179
+ return null;
180
+ }
181
+
182
+ // 戦略表示名のマッピング
183
+ const strategyDisplayMap: Record<string, string> = {
184
+ 積極化戦略: '積極化戦略(強み×機会)',
185
+ 改善戦略: '改善戦略(弱み×機会)',
186
+ 差別化戦略: '差別化戦略(強み×脅威)',
187
+ };
188
+
189
+ // 戦略の順序定義(第2ソート用)
190
+ const strategyOrder: Record<string, number> = {
191
+ 積極化戦略: 1,
192
+ 改善戦略: 2,
193
+ 差別化戦略: 3,
194
+ };
195
+
196
+ // 戦略別のデータを処理
197
+ const fvTabs = this.translateFvDataToTabs(proposalFvData);
198
+ const cnTabs = this.translateCnDataToTabs(proposalCnData);
199
+
200
+ // 一時的にデータを収集(ソート前)
201
+ interface TempTabData {
202
+ strategy: string;
203
+ need: string;
204
+ strategyOrder: number;
205
+ prediction: number;
206
+ tabData: ProposalTabData;
207
+ }
208
+ const tempTabsData: TempTabData[] = [];
209
+
210
+ Object.entries(proposalFvData).forEach(([strategy]) => {
211
+ const strategyFvData = proposalFvData[strategy as keyof typeof proposalFvData];
212
+
213
+ if (strategyFvData) {
214
+ Object.entries(strategyFvData).forEach(([need]) => {
215
+ // 戦略とニーズをキーとして使用
216
+ const dataKey = `${strategy}_${need}`;
217
+
218
+ // FVデータの取得(正規化したキーで検索)
219
+ const fvData: FvData = findWithNormalizedKey(fvTabs, dataKey) || {
220
+ strategy,
221
+ need,
222
+ data: {
223
+ fv: strategyFvData[need]?.fv || {},
224
+ },
225
+ };
226
+
227
+ // CNデータの取得(正規化したキーで検索)
228
+ let cnData: ContentSection[] = findWithNormalizedKey(cnTabs, dataKey) || [];
229
+
230
+ // 戦略タイプの表示名を設定
231
+ const strategyDisplayName = strategyDisplayMap[strategy] || strategy;
232
+
233
+ // Intentデータの抽出と展開
234
+ let intentData: ProposalTabData['intent'] = {
235
+ strategy: strategyDisplayName,
236
+ direction: need, // ニーズを方向性として���定
237
+ };
238
+
239
+ // strategyXMomentから該当する戦略とモーメントのデータを抽出
240
+
241
+ if (strategyXMoment && strategyXMoment[strategy]) {
242
+ const momentItem = findWithNormalizedKey(strategyXMoment[strategy], need);
243
+ if (momentItem) {
244
+ intentData.strategyXMomentItem = momentItem;
245
+ }
246
+ }
247
+
248
+ // proposalIntentDataから該当する戦略・ニーズのデータを抽出
249
+ const intentArray = Array.isArray(proposalIntentData) ? proposalIntentData : [proposalIntentData];
250
+ if (intentArray && intentArray[0]) {
251
+ const intentItem = intentArray[0];
252
+ const strategyIntent = intentItem[strategy as keyof typeof intentItem];
253
+
254
+ // 正規化したキーで検索を試みる
255
+ const needIntent = findWithNormalizedKey(strategyIntent, need);
256
+
257
+ if (needIntent) {
258
+ if (needIntent.intent_all) {
259
+ intentData = {
260
+ ...intentData,
261
+ FV_intent: needIntent.intent_all.FV_intent,
262
+ CN_intent: needIntent.intent_all.CN_intent,
263
+ FV_suggest: needIntent.intent_all.FV_suggest,
264
+ CN_suggest: needIntent.intent_all.CN_suggest,
265
+ };
266
+ }
267
+ if (needIntent.cn_creation_intent) {
268
+ intentData.cn_creation_intent = needIntent.cn_creation_intent;
269
+ // cn_creation_intentから各セクションの制作意図を設定
270
+ cnData = cnData.map((section) => ({
271
+ ...section,
272
+ 制作意図: needIntent.cn_creation_intent[section.大区分] || '',
273
+ }));
274
+ }
275
+ if (needIntent.fv_creation_intent) {
276
+ fvData.fvCreationIntent = needIntent.fv_creation_intent;
277
+ }
278
+ }
279
+ }
280
+
281
+ // proposalPredictionDataから該当する戦略・ニーズのデータを抽出
282
+ let predictionValue = 0; // デフォルト値
283
+ const predictionArray = Array.isArray(proposalPredictionData) ? proposalPredictionData : [proposalPredictionData];
284
+
285
+ if (predictionArray && predictionArray[0]) {
286
+ const predictionItem = predictionArray[0];
287
+ const strategyPrediction = predictionItem[strategy as keyof typeof predictionItem];
288
+
289
+ // 正規化したキーで検索
290
+ const needPrediction = findWithNormalizedKey(strategyPrediction, need);
291
+
292
+ if (needPrediction) {
293
+ if (needPrediction.prediction !== undefined) {
294
+ intentData.prediction = needPrediction.prediction;
295
+ predictionValue = needPrediction.prediction;
296
+ } else {
297
+ }
298
+ } else {
299
+ // 本番環境でも確実に出力するためにconsole.warnも使用
300
+ console.warn(`[translateToTabsData] WARNING: No prediction found for ${dataKey}`, {
301
+ need,
302
+ availableNeeds: strategyPrediction ? Object.keys(strategyPrediction) : [],
303
+ strategyPrediction,
304
+ });
305
+ }
306
+ } else {
307
+ console.warn(`[translateToTabsData] WARNING: No prediction data available for ${dataKey}`);
308
+ }
309
+
310
+ // タブデータの作成
311
+ const tabData: ProposalTabData = {
312
+ intent: intentData,
313
+ fv: fvData,
314
+ cn: cnData,
315
+ };
316
+
317
+ // 一時データに追加
318
+ tempTabsData.push({
319
+ strategy,
320
+ need,
321
+ strategyOrder: strategyOrder[strategy] || 999,
322
+ prediction: predictionValue,
323
+ tabData,
324
+ });
325
+ });
326
+ }
327
+ });
328
+
329
+ // prediction値でソート(第1ソート: 降順)、同じ値の場合は戦略順(第2ソート)
330
+ tempTabsData.sort((a, b) => {
331
+ // 第1ソート: prediction値の降順
332
+ if (b.prediction !== a.prediction) {
333
+ return b.prediction - a.prediction;
334
+ }
335
+ // 第2ソート: 戦略の順序
336
+ return a.strategyOrder - b.strategyOrder;
337
+ });
338
+
339
+ // ソート後にA案〜I案を再割り当て
340
+ const result: ProposalTabsResponse = {};
341
+ const proposalLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
342
+
343
+ tempTabsData.forEach((item, index) => {
344
+ if (index < proposalLetters.length) {
345
+ const proposalKey = `${proposalLetters[index]}案`;
346
+ result[proposalKey] = item.tabData;
347
+ // 本番環境でも出力されるようにconsole.warnも使用
348
+ console.warn(`[translateToTabsData] Assigned ${proposalKey} to ${item.strategy}/${item.need} with prediction: ${item.prediction}`);
349
+ }
350
+ });
351
+
352
+ // デバッグ: 最終結果の確認(本番環境用にconsole.warnも追加)
353
+ console.warn('[translateToTabsData] Final result keys:', Object.keys(result));
354
+
355
+ Object.entries(result).forEach(([key, data]) => {
356
+ const summary = {
357
+ hasIntentData: !!data.intent,
358
+ hasFvData: !!data.fv,
359
+ hasCnData: !!data.cn,
360
+ strategy: data.intent?.strategy,
361
+ direction: data.intent?.direction,
362
+ fvメインコピー: data.fv?.data?.fv?.コピー?.メインコピー?.substring(0, 30),
363
+ };
364
+
365
+ // 本番環境でも確実に出力
366
+ console.warn(`[translateToTabsData] Final ${key}:`, summary);
367
+ });
368
+
369
+ console.warn('[translateToTabsData] END - Processing completed');
370
+ return result;
371
+ },
372
+ };
api-client/proposal/queries.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ // Re-export all hooks from subdirectories
2
+ export { generateContentsHtml, generateFvHtml, getImageGenerationStatus, startImageGeneration } from './html-preview/index';
3
+ export { useContentsHtmlGeneration, useContentsImageGeneration, useFvHtmlGeneration } from './html-preview/queries';
4
+ export { useProposalTabsOptimized } from './proposal/optimized-queries';
5
+ export { useThemeByMoment } from './proposal/queries';
6
+ export { ProposalTranslator } from './proposal/translator';
7
+ export { useBatchScreenshots, useScreenshot } from './screenshot/queries';
api-client/proposal/screenshot/queries.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { useQuery } from '@tanstack/react-query';
3
+
4
+ // バッチスクリーンショット取得用のフック
5
+ export function useBatchScreenshots(urls: Record<string, string>, enabled: boolean = false, dummyMode: boolean = false) {
6
+ return useQuery<Record<string, string>>({
7
+ ...createQueryOptions<Record<string, string>>(),
8
+ queryKey: ['batchScreenshots', urls, dummyMode],
9
+ queryFn: async () => {
10
+ // ダミーモードの場合はプレースホルダー画像を返す
11
+ if (dummyMode) {
12
+ console.log('[useBatchScreenshots] Dummy mode enabled, returning placeholder images');
13
+ const placeholderImage =
14
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
15
+ const screenshots: Record<string, string> = {};
16
+ Object.keys(urls).forEach((key) => {
17
+ screenshots[key] = placeholderImage;
18
+ });
19
+ return screenshots;
20
+ }
21
+
22
+ const response = await fetch('/api/rpc/screenshot/batch-screenshot', {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({
26
+ urls: Object.entries(urls).map(([key, url]) => ({
27
+ key,
28
+ url,
29
+ })),
30
+ dummyMode,
31
+ }),
32
+ });
33
+
34
+ if (!response.ok) {
35
+ throw new Error('Failed to fetch screenshots');
36
+ }
37
+
38
+ const data = await response.json();
39
+ console.log('[useBatchScreenshots] Response data:', data);
40
+
41
+ const screenshots: Record<string, string> = {};
42
+
43
+ Object.entries(data.screenshots || {}).forEach(([key, value]: [string, any]) => {
44
+ // imageBase64フィールドを使用(サーバーはこのフィールドで返している)
45
+ screenshots[key] = value.imageBase64 || value.screenshotBase64;
46
+ });
47
+
48
+ console.log('[useBatchScreenshots] Processed screenshots:', {
49
+ keys: Object.keys(screenshots),
50
+ hasA案: !!screenshots['A案'],
51
+ });
52
+
53
+ return screenshots;
54
+ },
55
+ enabled,
56
+ });
57
+ }
58
+
59
+ // 単一スクリーンショット取得用のフック
60
+ export function useScreenshot(url: string, enabled: boolean = false, dummyMode: boolean = false) {
61
+ return useQuery<string>({
62
+ ...createQueryOptions<string>(),
63
+ queryKey: ['screenshot', url, dummyMode],
64
+ queryFn: async () => {
65
+ // ダミーモードの場合はプレースホルダー画像を返す
66
+ if (dummyMode) {
67
+ console.log('[useScreenshot] Dummy mode enabled, returning placeholder image');
68
+ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
69
+ }
70
+
71
+ const params = new URLSearchParams({
72
+ url: url,
73
+ width: '512',
74
+ height: '768',
75
+ dummyMode: dummyMode.toString(),
76
+ });
77
+
78
+ const response = await fetch(`/api/rpc/screenshot?${params.toString()}`, {
79
+ method: 'GET',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ });
82
+
83
+ if (!response.ok) {
84
+ throw new Error('Failed to fetch screenshot');
85
+ }
86
+
87
+ const data = await response.json();
88
+
89
+ // imageBase64が既にdata URLの場合はそのまま返す
90
+ if (data.imageBase64 && data.imageBase64.startsWith('data:')) {
91
+ return data.imageBase64;
92
+ }
93
+
94
+ // そうでない場合はdata:プレフィックスを追加
95
+ return `data:${data.mimeType || 'image/jpeg'};base64,${data.imageBase64}`;
96
+ },
97
+ enabled,
98
+ });
99
+ }
api-client/query-config.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { UseQueryOptions } from '@tanstack/react-query';
2
+
3
+ /**
4
+ * React Query共通設定
5
+ * 全APIクライアントで使用する共通のクエリオプション
6
+ */
7
+ export const defaultQueryOptions: Partial<UseQueryOptions> = {
8
+ // ウィンドウフォーカス時の再フェッチを無効化
9
+ refetchOnWindowFocus: false,
10
+
11
+ // コンポーネント再マウント時の再フェッチを無効化
12
+ refetchOnMount: false,
13
+
14
+ // データが古くならない期間(無期限)
15
+ staleTime: Infinity,
16
+
17
+ // ガベージコレクション時間(24時間)
18
+ gcTime: 24 * 60 * 60 * 1000,
19
+
20
+ retry: 0,
21
+
22
+ // エラー時のリトライ遅延
23
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
24
+ };
25
+
26
+ /**
27
+ * 特定のクエリ用のオプションを作成するヘルパー関数
28
+ * @param overrides 上書きしたいオプション
29
+ * @returns マージされたクエリオプション
30
+ */
31
+ export function createQueryOptions<T = unknown>(overrides?: Partial<UseQueryOptions<T>>): Partial<UseQueryOptions<T>> {
32
+ return { ...defaultQueryOptions, ...overrides } as Partial<UseQueryOptions<T>>;
33
+ }
api-client/refresh-moments/queries.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import { rpcClient } from '@/api-client/utils/rpc-client';
3
+ import { useGlobalStore } from '@/store/global';
4
+ import { generateSelectionHash } from '@/store/proposal-trigger';
5
+ import { useQuery } from '@tanstack/react-query';
6
+ import { useMemo } from 'react';
7
+
8
+ // RefreshMoment用のパラメータ型定義
9
+ interface UseRefreshMomentParams {
10
+ commonDict?: Record<string, unknown> | object | null;
11
+ scoreDict?: Record<string, unknown> | object | null;
12
+ swot?: Record<string, unknown> | object | null;
13
+ userEmail?: string;
14
+ }
15
+
16
+ // 変換後のデータ型
17
+ export interface RefreshMomentResult {
18
+ moments: Array<{ text: string; value: boolean }>;
19
+ ownTheme: string;
20
+ }
21
+
22
+ /**
23
+ * RefreshMoment(改善案の方向性)を取得するフック
24
+ * AIが生成する追加の方向性選択肢とownThemeを取得
25
+ */
26
+ export function useRefreshMoment(params: UseRefreshMomentParams) {
27
+ const { commonDict, scoreDict, swot, userEmail } = params;
28
+ const { userIdentifier } = useGlobalStore();
29
+
30
+ // オブジェクトをハッシュ化して安定したキーを生成
31
+ const queryKeyHash = useMemo(() => {
32
+ return generateSelectionHash({
33
+ commonDict,
34
+ scoreDict,
35
+ swot,
36
+ userEmail,
37
+ });
38
+ }, [commonDict, scoreDict, swot, userEmail]);
39
+
40
+ return useQuery({
41
+ queryKey: ['refreshMoment', queryKeyHash],
42
+ ...createQueryOptions<[string, string]>({
43
+ queryFn: async () => {
44
+ if (!commonDict || !scoreDict || !swot) {
45
+ throw new Error('Required parameters are missing');
46
+ }
47
+
48
+ const response = await rpcClient['gradio-proxy']['refresh-moment'].$post({
49
+ json: {
50
+ commonDict: commonDict as Record<string, unknown>,
51
+ scoreDict: scoreDict as Record<string, unknown>,
52
+ swot: swot as Record<string, unknown>,
53
+ dummyMode: false,
54
+ userEmail: userEmail ?? undefined,
55
+ userIdentifier,
56
+ },
57
+ });
58
+
59
+ if (!response.ok) {
60
+ console.error('[useRefreshMoment] Response status:', response.status);
61
+ const errorData = (await response.json()) as { message?: string; status?: string };
62
+ console.error('[useRefreshMoment] API エラー:', errorData);
63
+ throw new Error(errorData.message || 'refresh_moment APIの呼び出しに失敗しました');
64
+ }
65
+
66
+ const data = await response.json();
67
+ return data as [string, string];
68
+ },
69
+ enabled: !!commonDict && !!scoreDict && !!swot,
70
+ }),
71
+ select: (data): RefreshMomentResult => {
72
+ // データを整形してから返す
73
+ const responseArray = data as unknown as [string, string] | undefined;
74
+ const moments =
75
+ responseArray?.[0]
76
+ ?.split('\n')
77
+ .map((moment: string) => ({
78
+ text: moment.trim(),
79
+ value: false,
80
+ }))
81
+ .filter((m: { text: string }) => m.text) || [];
82
+
83
+ const ownTheme = responseArray?.[1] || '';
84
+
85
+ return {
86
+ moments,
87
+ ownTheme,
88
+ };
89
+ },
90
+ });
91
+ }
api-client/screenshot.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ScreenshotRequest, ScreenshotResponse } from '@/schema/proposal';
2
+ import { AppError, parseApiError } from '@/lib/errors';
3
+
4
+ export const screenshotApi = {
5
+ capture: async (request: ScreenshotRequest & { dummyMode?: boolean }): Promise<ScreenshotResponse> => {
6
+ try {
7
+ // GETメソッドを使用
8
+ const params = new URLSearchParams({
9
+ url: request.url,
10
+ width: request.width?.toString() || '1920',
11
+ height: request.height?.toString() || '1080',
12
+ ...(request.dummyMode !== undefined && { dummyMode: request.dummyMode.toString() }),
13
+ });
14
+
15
+ const response = await fetch(`/api/rpc/screenshot?${params.toString()}`, {
16
+ method: 'GET',
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ });
21
+
22
+ if (!response.ok) {
23
+ const errorData = await response.json();
24
+ throw parseApiError(errorData);
25
+ }
26
+
27
+ const data = await response.json();
28
+
29
+ // レスポンスフォーマットを統一
30
+ return {
31
+ success: data.success,
32
+ screenshotBase64: data.imageBase64 || '',
33
+ mimeType: data.mimeType || 'image/jpeg',
34
+ description: '',
35
+ };
36
+ } catch (error) {
37
+ // ネットワークエラーやタイムアウトを捕捉
38
+ if (error instanceof TypeError && error.message.includes('fetch')) {
39
+ throw new AppError(
40
+ 'ネットワークエラーが発生しました',
41
+ 'NETWORK_ERROR',
42
+ 503,
43
+ );
44
+ }
45
+
46
+ // 既にAppErrorの場合はそのまま投げる
47
+ if (error instanceof AppError) {
48
+ throw error;
49
+ }
50
+
51
+ // その他のエラー
52
+ throw new AppError(
53
+ 'スクリーンショットの取得に失敗しました',
54
+ 'SCREENSHOT_FAILED',
55
+ 500,
56
+ error,
57
+ );
58
+ }
59
+ },
60
+ };
api-client/theme-extraction/index.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ThemeExtractionRequest, ThemeExtractionResponse } from '@/schema/theme-extraction';
2
+
3
+ /**
4
+ * テーマ抽出API
5
+ */
6
+ export const themeExtractionApi = {
7
+ /**
8
+ * テーマを抽出
9
+ */
10
+ async extractTheme(request: ThemeExtractionRequest): Promise<ThemeExtractionResponse> {
11
+ const response = await fetch('/api/rpc/theme-extraction', {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ body: JSON.stringify(request),
17
+ });
18
+
19
+ if (!response.ok) {
20
+ const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
21
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
22
+ }
23
+
24
+ const data = await response.json();
25
+ return data;
26
+ },
27
+
28
+ /**
29
+ * 利用可能なプロバイダーを取得
30
+ */
31
+ async getProviders(): Promise<{ success: boolean; data?: { providers: string[]; default: string }; error?: string }> {
32
+ const response = await fetch('/api/rpc/theme-extraction/providers', {
33
+ method: 'GET',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ },
37
+ });
38
+
39
+ if (!response.ok) {
40
+ const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
41
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
42
+ }
43
+
44
+ return response.json();
45
+ },
46
+
47
+ /**
48
+ * ヘルスチェック
49
+ */
50
+ async healthCheck(): Promise<{ success: boolean; status: string; data?: any; error?: string }> {
51
+ const response = await fetch('/api/rpc/theme-extraction/health', {
52
+ method: 'GET',
53
+ headers: {
54
+ 'Content-Type': 'application/json',
55
+ },
56
+ });
57
+
58
+ if (!response.ok) {
59
+ const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
60
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
61
+ }
62
+
63
+ return response.json();
64
+ },
65
+ };
api-client/theme-extraction/queries.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ThemeExtractionRequest, ThemeExtractionResponse } from '@/schema/theme-extraction';
2
+ import { useMutation, useQuery } from '@tanstack/react-query';
3
+ import { themeExtractionApi } from './index';
4
+
5
+ /**
6
+ * テーマ抽出用のReact Queryフック
7
+ */
8
+ export const useThemeExtraction = () => {
9
+ return useMutation<ThemeExtractionResponse, Error, ThemeExtractionRequest>({
10
+ mutationFn: themeExtractionApi.extractTheme,
11
+ onSuccess: (data) => {
12
+ console.log('[useThemeExtraction] Theme extraction completed:', data);
13
+ },
14
+ onError: (error) => {
15
+ console.error('[useThemeExtraction] Theme extraction failed:', error);
16
+ },
17
+ });
18
+ };
19
+
20
+ /**
21
+ * 利用可能なプロバイダー取得用のReact Queryフック
22
+ */
23
+ export const useThemeExtractionProviders = (enabled: boolean = true) => {
24
+ return useQuery({
25
+ queryKey: ['theme-extraction', 'providers'],
26
+ queryFn: themeExtractionApi.getProviders,
27
+ enabled,
28
+ staleTime: 5 * 60 * 1000, // 5分間キャッシュ
29
+ retry: 2,
30
+ });
31
+ };
32
+
33
+ /**
34
+ * テーマ抽出ヘルスチェック用のReact Queryフック
35
+ */
36
+ export const useThemeExtractionHealth = (enabled: boolean = false) => {
37
+ return useQuery({
38
+ queryKey: ['theme-extraction', 'health'],
39
+ queryFn: themeExtractionApi.healthCheck,
40
+ enabled,
41
+ staleTime: 1 * 60 * 1000, // 1分間キャッシュ
42
+ retry: 1,
43
+ });
44
+ };
api-client/theme/index.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ThemeExtractionResult } from '@/schema/theme-extraction';
2
+
3
+ export interface GenerateThemeCssResponse {
4
+ success: boolean;
5
+ css?: string;
6
+ error?: string;
7
+ }
8
+
9
+ /**
10
+ * テーマデータからCSSを生成
11
+ */
12
+ export async function generateThemeCss(themeData: ThemeExtractionResult): Promise<GenerateThemeCssResponse> {
13
+ try {
14
+ const response = await fetch('/api/rpc/theme-extraction/generate-css', {
15
+ method: 'POST',
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ body: JSON.stringify(themeData),
20
+ });
21
+
22
+ if (!response.ok) {
23
+ const errorData = await response.json();
24
+ throw new Error(errorData.error || 'CSS生成に失敗しました');
25
+ }
26
+
27
+ const data: GenerateThemeCssResponse = await response.json();
28
+ return data;
29
+ } catch (error) {
30
+ console.error('[generateThemeCss] Error:', error);
31
+ return {
32
+ success: false,
33
+ error: error instanceof Error ? error.message : 'CSS生成中にエラーが発生しました',
34
+ };
35
+ }
36
+ }
api-client/theme/queries.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createQueryOptions } from '@/api-client/query-config';
2
+ import type { ThemeExtractionResult } from '@/schema/theme-extraction';
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { generateThemeCss, type GenerateThemeCssResponse } from './index';
5
+
6
+ /**
7
+ * テーマCSS生成フック
8
+ * テーマデータが渡された場合のみCSS生成を実行
9
+ */
10
+ export function useThemeCss(themeData: ThemeExtractionResult | null, enabled: boolean = true) {
11
+ return useQuery<GenerateThemeCssResponse>({
12
+ ...createQueryOptions<GenerateThemeCssResponse>(),
13
+ queryKey: ['themeCss', themeData ? JSON.stringify(themeData) : 'no-theme'],
14
+ queryFn: () => {
15
+ if (!themeData) {
16
+ return {
17
+ success: true,
18
+ css: undefined,
19
+ };
20
+ }
21
+ return generateThemeCss(themeData);
22
+ },
23
+ enabled: enabled && !!themeData,
24
+ staleTime: Infinity, // テーマデータが同じならキャッシュを使う
25
+ });
26
+ }