Open5

リファクタリングの依頼とLLMに検証の差について

kamaguchikamaguchi

以下で確認しました。
[copilot]
GPT4.1
[gemini]
Gemini 2.5 Flash:
[claude code]
Model • /model
LDefault Opus 4.1 for up to 20% of usage
limits, then use Sonnet 4 (currently Opus)
[cursor]
gpt-5.0

検証したリポジトリは以下です。
https://github.com/mindface/betaTasker

エージェントでも検証してみたりしますが、プロンプトの設定や評価、モデルの兼ね合いをベースにコードを使うための設計を一律的な検証過程から、チームや個人の知識に対して使うアプローチが組織ごとに設計が必要そうです。

今回は、かなり曖昧にリクエストしています。
「現状のプロジェクトのコードでリファクタリングをした内容を教えてください。」
のようなものです。
次はどのような情報を取得して、アウトプットされているかを定量化するための仕組みについて探すことを考えています。

kamaguchikamaguchi

[claude cude]

⏺ Update Todos
  ⎿  ☐ コードベース全体の構造を把握
     ☐ 重複コードやパターンの特定
     ☐ リファクタリング候補の分析
     ☐ 具体的な改善提案の作成
⏺ List(.)
  ⎿  Listed 943 paths (ctrl+r to expand)
⏺ Search(pattern: "fronter/src/**/*.tsx")
  ⎿  Found 48 files (ctrl+r to expand)
⏺ Read(fronter/src/components/parts/CommonModal.tsx)
  ⎿  Read 41 lines (ctrl+r to expand)


⏺ リファクタリング推奨事項まとめ

  実装済み改善
  1. BaseApiClient - API呼び出しの統一化
  2. 共通型定義 - 型安全性の向上
  3. FormModal - モーダルフォームの汎用化
  4. createCrudSlice - Redux CRUD処理の統一化
  5. useApiCall - API呼び出しフックの共通化
  6. ItemCard - カード表示コンポーネントの統一化

以下例。
fronter/src/hooks/useApiCall.ts

import { useState, useCallback } from 'react';
import { ApiResponse } from '../services/baseApiClient';

interface UseApiCallOptions {
  onSuccess?: (data: any) => void;
  onError?: (error: string) => void;
}

export function useApiCall<T>(
  apiFunction: (...args: any[]) => Promise<ApiResponse<T>>,
  options?: UseApiCallOptions
) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [data, setData] = useState<T | null>(null);

  const execute = useCallback(async (...args: any[]) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await apiFunction(...args);
      
      if (response.success) {
        setData(response.data || null);
        options?.onSuccess?.(response.data);
        return response.data;
      } else {
        const errorMessage = response.error || 'エラーが発生しました';
        setError(errorMessage);
        options?.onError?.(errorMessage);
        throw new Error(errorMessage);
      }
    } catch (err: any) {
      const errorMessage = err.message || 'エラーが発生しました';
      setError(errorMessage);
      options?.onError?.(errorMessage);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [apiFunction, options]);

  const reset = useCallback(() => {
    setData(null);
    setError(null);
    setLoading(false);
  }, []);

  return {
    execute,
    loading,
    error,
    data,
    reset
  };
}

fronter/src/components/common/ItemCard.tsx

import { ReactNode } from 'react';

interface ItemCardProps<T> {
  item: T;
  onEdit?: (item: T) => void;
  onDelete?: (id: number) => void;
  renderTitle: (item: T) => ReactNode;
  renderContent: (item: T) => ReactNode;
  renderActions?: (item: T) => ReactNode;
  className?: string;
}

export function ItemCard<T extends { id: number }>({
  item,
  onEdit,
  onDelete,
  renderTitle,
  renderContent,
  renderActions,
  className = '',
}: ItemCardProps<T>) {
  return (
    <div className={`item-card ${className}`}>
      <div className="item-card-header">
        <h3 className="item-card-title">{renderTitle(item)}</h3>
        <div className="item-card-actions">
          {onEdit && (
            <button
              className="btn-edit"
              onClick={() => onEdit(item)}
              aria-label="編集"
            >
              <img src="/image/edit.svg" alt="編集" />
            </button>
          )}
          {onDelete && (
            <button
              className="btn-delete"
              onClick={() => onDelete(item.id)}
              aria-label="削除"
            >
              <img src="/image/delete.svg" alt="削除" />
            </button>
          )}
        </div>
      </div>
      
      <div className="item-card-content">
        {renderContent(item)}
      </div>
      
      {renderActions && (
        <div className="item-card-footer">
          {renderActions(item)}
        </div>
      )}
    </div>
  );
}

既存のコードからファイルを話すことがベースになっているように見えるのは、なぜなのか気になるところでした。
プロジェクトの内容でLLMが出力するパタンの方向性が決まるときに、プロンプトの前提を考慮する必要がありそうです。
実際にクライアントに依頼される業務がリファクタリングをベースにしていることが稀なので、開発チームの状況や運用方針の前提も含まれそうです。

kamaguchikamaguchi

[copilot]
GPT4.1
モデルがあまり良いものでなく比較になりにくいかもしれません。

1. 状態管理の整理
Redux(Slice)とuseStateの使い分けの統一
不要なuseStateやuseRefの削除
propsの受け渡しの簡素化
2. API呼び出しの統一
fetch/axiosの呼び出しをservices層に集約
エラーハンドリングの統一
型安全なレスポンス処理
3. UIコンポーネントの再利用性向上
共通部品(Modal, Card, Headerなど)のprops設計見直し
重複するUIロジックの関数化・部品化
4. 型定義の整理
model/配下のinterfaceの命名・フィールド名統一
DB/APIレスポンスとの整合性
5. 副作用管理(useEffect等)の最適化
useEffectの依存配列・クリーンアップ処理の見直し
モーダル開閉時のbody style制御の共通化
6. エラーハンドリング・ローディングUIの統一
エラー表示・ローディング表示の共通化
ユーザー体験の向上
7. ファイル・ディレクトリ構成の整理
コンポーネント・サービス・モデルの分離
命名規則の統一

axiosは存在しないのでどこのコードかわからない点があります。
fronter/src/components/SectionTaskView.tsx

"use client"
import React, { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
import Image from 'next/image'
import { Posts } from '../model/posts'
import ContentTask01 from "./ContentTask01"
import ContentHeader from "./ContentTasksHeader"
import ItemCard from "./parts/ItemCard"
import Loading from "./Loading"
import { RootState } from "../store"

export default function SectionTaskView() {
  const [switchClass, setSwitchClass] = useState('')
  const [filteredPosts, setFilteredPosts] = useState<Posts[] | null>(null)
  const [isClient, setIsClient] = useState(false)

  const posts = useSelector((state: RootState) => state.post.rePosts || [])
  const loading = useSelector((state: RootState) => state.post.loading)

  useEffect(() => {
    setIsClient(true)
  }, [])

  const handleViewSwitch = (type: string) => {
    setSwitchClass(type)
  }

  const handleSearch = (postsList: Posts[], text: string, type: string) => {
    if (!text) {
      setFilteredPosts(null)
      return
    }
    const filtered = postsList.filter((item) => {
      const target = type === 'title' ? item.title : item.name
      return target?.includes(text)
    })
    setFilteredPosts(filtered)
  }

  return (
    <ContentTask01>
      <>
        <ContentHeader searchAction={handleSearch} title="タスク形式" />
        <div className="section__inner section--task">
          <div className="view-switch">
            <button className="btn" onClick={() => handleViewSwitch('list')}>
              <Image src="/image/list.svg" alt="" width={20} height={20} />
            </button>
            <button className="btn" onClick={() => handleViewSwitch('cardview')}>
              <Image src="/image/card.svg" alt="" width={20} height={20} />
            </button>
            <button className="btn" onClick={() => handleViewSwitch('category')}>
              <Image src="/image/link.svg" alt="" width={20} height={20} />
            </button>
          </div>
          {isClient && (
            <div className={`task-box _flex_ ${switchClass}`}>
              {loading && <Loading />}
              {(filteredPosts ?? posts).map((item) => (
                <ItemCard posts={item} key={Number(item.id)} />
              ))}
            </div>
          )}
        </div>
      </>
    </ContentTask01>
  )
}
export const fetchMemoriesService = async () => {
  try {
    const res = await fetch('/api/memory', {
      method: 'GET',
      credentials: 'include',
    });
    const data = await res.json();
    if (!res.ok) throw new Error('メモリ一覧取得失敗');
    return data;
  } catch (err: any) {
    return { error: err.message };
  }
};
export const fetchMemoriesService = async (): Promise<Memory[] | { error: string }> => {
  try {
    const res = await fetch('/api/memory', {
      method: 'GET',
      credentials: 'include',
    });
    const data = await res.json();
    if (!res.ok) throw new Error('メモリ一覧取得失敗');
    return data.memories;
  } catch (err: any) {
    return { error: err.message };
  }
};
kamaguchikamaguchi

[gemini]
Gemini 2.5 Flash

全体的なリファクタリング方針
コンポーネントの共通化: 似たようなUIや機能を持つコンポーネントを汎用化し、コードの重複を削減します。
モダンなReactへの移行: クラスコンポーネントやDOMを直接操作する古い手法を、関数コンポーネントとHooks、宣言的なUI記述に置き換えます。
状態管理の簡素化: 複雑な状態管理ロジックを、useMemo などのHooksを活用してシンプルで予測可能な形に整理します。
コードのクリーンアップ: 明らかな重複コードを削除し、可読性を高めます。

例: コードのクリーンアップ(重複コードの削除)
SectionAssessment.tsx 内には、見出しと「新規アセスメント」ボタンを含むヘッダー部分が2回記述されていました。これは明らかな重複であり、コードの見通しを悪くする原因になります。 この重複部分を削除することで、コードをシンプルにし、可読性を向上させます。

 <div className="section-container">
        <div className="assessment-header">
          <h2>アセスメント</h2>
+          {learningLoading ? (
+            <div className="loading">学習構造データ取得中...</div>
+          ) : learningError ? (
+            <div className="error-message">{learningError}</div>
+          ) : learningData ? (
+            <div className="learning-structure-info">
+              <span className="d-block">学習カテゴリ: {learningData.learningStructure.category}+</span>
+              <span className="d-block">学習サイクル: {learningData.learningStructure.studyCycle.map((item) => <span className="d-iline-block p-1">{item}</span>)}</span>
+            </div>
+          ) : null}
          <button
            onClick={() => handleAddAssessment()}
            className="btn btn-primary"
          >
            新規アセスメント
          </button>
          <div className="assessment-header">
-            <h2>アセスメント</h2>
-            {learningLoading ? (
-              <div className="loading">学習構造データ取得中...</div>
-            ) : learningError ? (
-              <div className="error-message">{learningError}</div>
-            ) : learningData ? (
-              <div className="learning-structure-info">
-                <span className="d-block">学習カテゴリ: {learningData.learningStructure.category}</span>
-                <span className="d-block">学習サイクル: {learningData.learningStructure.studyCycle.map((item) => <span className="d-iline-block p-1">{item}</span>)}</span>
-              </div>
            ) : null}

-            <button
-              onClick={() => handleAddAssessment()}
-              className="btn btn-primary"
-            >
-              新規アセスメント
-            </button>
-          </div>
        </div>

kamaguchikamaguchi

[cursor][ChatGPT 5.0]

方針
APIレスポンスの正規化(ネスト排除・例外化)
ReduxにbyIdキャッシュ導入と命名整合(memoryItem)
型付きhooksでdispatch/selectorを安全に
APIベースURLの環境変数化

[fronter/src/services/fetcher.ts]

export async function fetchJSON<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
  const res = await fetch(input, init);
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(text || `HTTP ${res.status}`);
  }
  return res.json() as Promise<T>;
}

[fronter/src/services/memoryApi.ts]

import { fetchJSON } from './fetcher';
import { Memory } from '../model/memory';

export async function fetchMemoriesService(): Promise<{ memories: Memory[] }> {
  return fetchJSON<{ memories: Memory[] }>('/api/memory'); // 例: 既存の一覧API
}

export async function fetchMemoryService(id: number): Promise<Memory> {
  const data = await fetchJSON<{ memory: Memory }>(`/api/memory/${id}`);
  return data.memory; // ネストをここで剥がす
}

[fronter/src/hooks.ts]

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;