ツイートアプリを作成しながら学習 1-6【Django + React】

## 仕上がり

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3895655/e6370416-81a5-414e-85b9-4c9ac1f33374.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3895655/4f51beb4-b26d-47e8-9c20-c919b7f89578.png)


## 目次

#### 1. ブックマーク機能の実装

#### 2. フォロー機能の実装


## 1. ブックマーク機能の実装

#### :computer: 1. モデルを作る

```python

# backend/tweets/models.py

from django.db import models
from django.contrib.auth import get_user_model

# Create your models here.

User = get_user_model()

class Tweet(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tweets")
    content = models.CharField(max_length=280)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user.username}: {self.content[:30]}"

class Bookmark(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="bookmarks")
    tweet = models.ForeignKey(Tweet, on_delete=models.CASCADE, related_name="bookmarked_by")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ("user", "tweet")

    def __str__(self):
        return f"{self.user.username} bookmarked {self.tweet.id}"

```

#### :computer: 2. マイグレーションの実行

#### :computer:　3. APIの作成

```python

# backend/tweets/views.py

from rest_framework import viewsets, permissions, status
from rest_framework.views import APIView
from.models import Tweet, Bookmark
from.serializers import TweetSerializer
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response


class BookmarkAPIView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request, tweet_id):
        user = request.user
        try:
            tweet = Tweet.objects.get(id=tweet_id)
        except Tweet.DoesNotExist:
            return Response({"Error": "Tweet not Found."}, status=status.HTTP_404_NOT_FOUND)
        
        bookmark, created = Bookmark.objects.get_or_create(user=user, tweet=tweet)

        if not created:
            bookmark.delete()
            return Response({"Message": "Bookmark removed."}, status=status.HTTP_200_OK)
        return Response({"Message": "Tweet bookmarked."}, status=status.HTTP_201_CREATED)
    
    def get(self, request):
        user = request.user
        bookmarks = Bookmark.objects.filter(user=user).select_related("tweet")
        bookmarked_tweets = [
            {
                "id": b.tweet.id,
                "content": b.tweet.content,
                "created_at": b.tweet.created_at
            }
            for b in bookmarks
        ]
        return Response(bookmarked_tweets, status=status.HTTP_200_OK)
    
    def delete(self, request, tweet_id):
        user = request.user
        try:
            bookmark = Bookmark.objects.get(user=user, tweet_id=tweet_id)
            bookmark.delete()
            return Response({"Message": "Bookmark removed."}, status=status.HTTP_200_OK)
        except Bookmark.DoesNotExist:
            return Response({"Error": "Bookmark not found."}, status=status.HTTP_404_NOT_FOUND)

class TweetViewSet(viewsets.ModelViewSet):
    queryset = Tweet.objects.all().order_by("-created_at")
    serializer_class = TweetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def perform_create(self,serializer):
        # 新規作成時に "user" を自動的にセット
        tweet = serializer.save(user=self.request.user)

        # 新しいツイートが投稿されたらWebsocketで通知
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            "tweets",
            {
                "type": "tweet_message",
                "tweet": {
                    "id": tweet.id,
                    "content": tweet.content,
                    "username": tweet.user.username,
                    "created_at": str(tweet.created_at),
                }
            }
        )

```

#### :computer:　4. URLを作成

```python

# backend/tweets/urls.py

from rest_framework.routers import DefaultRouter
from.views import TweetViewSet, BookmarkAPIView
from django.urls import path

router = DefaultRouter()
router.register(r'tweets', TweetViewSet, basename='tweets')

urlpatterns = router.urls

urlpatterns += [
    path("tweets/<int:tweet_id>/bookmark/", BookmarkAPIView.as_view(), name="bookmark_tweet"),
    path("bookmarks/", BookmarkAPIView.as_view(), name="list_bookmarks"),
    path("tweets/<int:tweet_id>/unbookmark/", BookmarkAPIView.as_view(), name="unbookmark_tweet"),
]

```

#### :computer:　5. tweetserializers.pyでis_bookmarkの作成

```python

# backend/tweets/serializers.py

from rest_framework import serializers
from.models import Tweet,Bookmark
from rest_framework.response import Response

class TweetSerializer(serializers.ModelSerializer):
    username = serializers.ReadOnlyField(source='user.username')
    user_has_liked = serializers.SerializerMethodField()
    likes_count = serializers.SerializerMethodField()
    isBookmarked = serializers.SerializerMethodField()

    class Meta:
        model = Tweet
        fields = ["id", "username", "content", "created_at","user_has_liked", "likes_count", "isBookmarked"]
        read_only_fields = ["id", "username", "created_at"]

    def list(self, request, *args, **kwargs):
        queryset = self.get_queryset()
        serializer = self.get_serializer(queryset, many=True, context={'request': request})
        return Response(serializer.data)

    def get_user_has_liked(self, obj):
        request = self.context.get("request", None)
        if request and request.user.is_authenticated:
            return obj.likes.filter(user=request.user).exists()
        return False

    def get_likes_count(self, obj):
        return obj.likes.count()
    
    def get_isBookmarked(self, obj):
           request = self.context.get('request', None)
           if request and request.user.is_authenticated:
               return Bookmark.objects.filter(user=request.user, tweet=obj).exists()
           return False

```

#### :computer:　6. Bookmarkコンポーネントの作成

```jsx

// frontend/src/components/BookmarkToggle.jsx

import React, { useState, useEffect } from "react";
import API from "../api";

const  BookmarkToggle = ({ tweetId, initialBookmarked, onToggle }) => {
    const [bookmarked, setBookmarked] = useState(initialBookmarked);

    useEffect(() => {
        setBookmarked(initialBookmarked);
    }, [initialBookmarked]);

    const handleBookmark = async () => {
        try {
            const response = await API.post(`tweets/${tweetId}/bookmark/`);
            console.log("Bookmark Response:", response.data)
            setBookmarked(true);
            onToggle(true);
        } catch (error) {
            console.error("Error:", error);
        }
    };

    const handleUnBookmark = async () => {
        try {
            const response = await API.delete(`tweets/${tweetId}/unbookmark/`);
            console.log("UnBookmark Response:", response.data);
            setBookmarked(false);
            onToggle(false);
        } catch (error) {
            console.error("Error unbookmark", error);
        }
    };

    return (
        <div>
            {bookmarked ? (
                <button onClick={handleUnBookmark} className="text-red-500">
                    UnBookmark
                </button>
            ) : (
                <button onClick={handleBookmark} className="text-blue-500">
                    Bookmark
                </button>
            )}
        </div>

    );
};

export default BookmarkToggle;

```

#### :computer:　7. TweetItem にBookmarkToggleを追加

```jsx

// frontend/src/components/TweetItem.jsx

// src/components/TweetItem.jsx
import React, { useState } from "react";
import CommentList from "./CommentList";
import LikeToggle from "./LikeToggle";
import { Link } from "react-router-dom";
import BookmarkToggle from "./BookmarkToggle";

const TweetItem = ({ tweet, onDelete, onBookmarkToggle }) => {
  const [showComments, setShowComments] = useState(false);

  const handleDeleteTweet = () => {
    if (!window.confirm("Delete this tweet?")) return;
    // 親から受け取った onDelete があれば呼ぶ
    if (onDelete) {
      onDelete(tweet.id);
    }
  };

  return (
    <div className="border p-4 mb-4">
      <p className="font-bold">
        <Link to={`/users/${tweet.username}/profile`} className="text-blue-500 hover:underline">
          {tweet.username}
        </Link>
      </p>
      <p>{tweet.content}</p>
      <small className="text-gray-500">
        {new Date(tweet.created_at).toLocaleString()}
      </small>

      <div className="mt-2 flex items-center gap-4">
        <LikeToggle
            tweetId={tweet.id}
            initialLiked={tweet.user_has_liked}
            initialLikeCount={tweet.likes_count}
        />

        <BookmarkToggle
          tweetId={tweet.id}
          initialBookmarked={tweet.isBookmarked}
          onToggle={(newStatus) => onBookmarkToggle(tweet.id, newStatus)}
        />

        <button
          onClick={() => setShowComments(!showComments)}
          className="text-blue-500 mr-2"
        >
          {showComments ? "Hide Comments" : "Show Comments"}
        </button>
        <button
          onClick={handleDeleteTweet}
          className="text-red-500"
        >
          Delete Tweet
        </button>
      </div>

      {showComments && (
        <CommentList tweetId={tweet.id} />
      )}
    </div>
  );
};

export default TweetItem;

```

#### :computer: 8. TweetList.jsxでBookmarkの状態を管理

```jsx

// frontend/src/pages/TweetList.jsx

import React, { useContext, useState, useEffect } from "react";
import { AuthContext } from "../contexts/AuthContext";
import API from "../api";
import TweetForm from "../components/TweetForm";
import TweetItem from "../components/TweetItem";

function TweetList() {
    const { accessToken } = useContext(AuthContext);
    const [tweets, setTweets] = useState([]);
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(true);

    console.log("Access token:", accessToken);

    // ツイート一覧を取得
    const fetchTweets = async () => {
        setLoading(true);
        setError(null);
        try {
            const res = await API.get("tweets/");
            setTweets(res.data);
        } catch (err) {
            console.error("Failed  to fetch tweets", err);
            setError("Failed to load tweets.Please try again.")
        }
        setLoading(false);
    };

    useEffect(() => {
        fetchTweets();
    }, []);

    const handleNewTweet = (newTweet) => {
        setTweets([newTweet, ...tweets]);
    };

    // ツイート削除時のハンドラー
    const handleDeleteTweet = (deletedTweetId) => {
        setTweets(tweets.filter(tweet => tweet.id !== deletedTweetId))
    };

    const handleBookmarkToggle = (tweetId, newStatus) => {
        setTweets((prevTweets) =>
            prevTweets.map((tweet) =>
                tweet.id === tweetId ? { ...tweet, isBookmarked: newStatus } : tweet
            )
        );
    };

    return (
        <div className="container mx-auto mt-4 px-4">
            <h2 className="text-2xl font-bold mb-4">Tweets</h2>

            {accessToken && (
                <div className="mb-6">
                    <TweetForm onTweetCreated={handleNewTweet} />
                </div>
            )}

            {loading && <p>Loading tweets...</p>}
            {error &&  <div className="bg-red-100 text-red-700 p-2 mb-4 rounded">{error}</div>}

            {!loading && tweets.length === 0 && <p>No tweets available.</p>}

            <div className="space-y-4">
                {tweets.map((tweet) => (
                    <TweetItem key={tweet.id} tweet={tweet} onDelete={handleDeleteTweet} onBookmarkToggle={handleBookmarkToggle} />
                ))}
            </div>
        </div>
    );
}

export default TweetList;

```

#### :computer: 9. Bookmarks.jsxを作成して一覧ページの作成

```jsx

import React, { useState, useEffect } from "react";
import API from "../api";
import TweetItem from "../components/TweetItem";

const Bookmarks = () => {
    const [bookmarkedTweets, setBookmarkedTweets] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    // ブックマーク一覧を取得
    const fetchBookmarkedTweets = async () => {
        setLoading(true);
        setError(null);
        try {
            const res = await API.get("bookmarks/");
            setBookmarkedTweets(res.data);
        } catch (err) {
            console.error("Failed fetch bookmarks", err);
            setError("Failed fetch bookmarks");
        }
        setLoading(false);
    };

    useEffect(() => {
        fetchBookmarkedTweets();
    }, []);

    return (
        <div className="container mx-auto mt-4 px-4">
            <h2 className="text-2xl font-bold mb-4">Bookmarks</h2>

            {loading && <p>Loading bookmarks...</p>}
            {error && <div className="bg-red-100 text-red-700 p-2 mb-4 rounded">{error}</div>}

            {!loading && bookmarkedTweets.length === 0 && <p>Bookmarks Not yet.</p>}

            <div className="space-y-4">
                {bookmarkedTweets.map((tweet) => (
                    <TweetItem key={tweet.id} tweet={tweet} />
                ))}
            </div>
        </div>
    );
};

export default Bookmarks;

```

#### :computer: 10. App.jsに追加

## 2. フォロー機能の実装

#### :computer: 1. モデルの作成

```python

# backend/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model

class User(AbstractUser):
    
    # DjangoのAbstractUserを継承したカスタムUserモデル。
    # AbstractUserは username, email, first_name, last_name, password など
    # すでに多くのフィールドが定義されている。
    
    # 追加でプロフィール画像や自己紹介欄などを1つのモデルにまとめてもOK
    # 例:
    # profile_image = models.ImageField(upload_to='profiles/', null=True, blank=True)
    # bio = models.TextField(blank=True)

    # ここに追加フィールドを書ける
    pass

    def __str__(self):
        return self.username

class Follow(models.Model):
    follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name="following")
    following = models.ForeignKey(User, on_delete=models.CASCADE, related_name="followers")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ("follower", "following")

    def __str__(self):
        return f"{self.follower.username} follows {self.following.username}"

```

#### :computer: 2. マイグレーションの実行

#### :computer:　3. APIの作成

```python

# backend/users/views.py

from rest_framework import generics, permissions
from.models import User, Follow
from.serializers import UserCreateSerializer
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status

# Create your views here.

class UserCreateView(generics.CreateAPIView):
    # 新規ユーザーを作成するだけのAPI

    queryset = User.objects.all()
    serializer_class = UserCreateSerializer
    permission_classes = [permissions.AllowAny]

class FollowAPIView(APIView):

    permission_classes = [IsAuthenticated]

    def post(self, request, username):
        follower = request.user
        try:
            following = User.objects.get(username=username)
        except User.DoesNotExist:
            return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)

        if follower == following:
            return Response({"error": "You cannot follow yourself"}, status=status.HTTP_400_BAD_REQUEST)

        follow_relation, created = Follow.objects.get_or_create(follower=follower, following=following)

        if created:
            return Response({"message": "Followed"}, status=status.HTTP_201_CREATED)
        return Response({"message": "Already following"}, status=status.HTTP_200_OK)

    def delete(self, request, username):
        follower = request.user
        try:
            follow_relation = Follow.objects.get(follower=follower, following__username=username)
            follow_relation.delete()
            return Response({"message": "Unfollowed"}, status=status.HTTP_200_OK)
        except Follow.DoesNotExist:
            return Response({"error": "You are not following this user"}, status=status.HTTP_400_BAD_REQUEST)
    
class FollowersListView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, username):
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            return Response({"Error": "User not found."}, status=status.HTTP_404_NOT_FOUND)
        
        followers = user.followers.all()
        follower_list = [{"id": f.follower.id, "username": f.follower.username} for f in followers]

        return Response(follower_list, status=status.HTTP_200_OK)
    
class FollowingListView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, username):
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            return Response({"Error": "User not found."}, status=status.HTTP_404_NOT_FOUND)
        
        following = user.following.all()
        following_list = [{"id": f.following.id, "username": f.following.username} for f in following]

        return Response(following_list, status=status.HTTP_200_OK)

```

#### :computer: 4. URLの作成

```python

# backend/users/urls.py

from django.urls import path
from.views import UserCreateView,FollowAPIView,FollowersListView,FollowingListView

urlpatterns = [
    path('register/', UserCreateView.as_view(), name="user_register"),
    path("users/<str:username>/follow/", FollowAPIView.as_view(), name="follow_user"),
    path("users/<str:username>/unfollow/", FollowAPIView.as_view(), name="unfollow_user"),
    path("users/<str:username>followers/", FollowersListView.as_view(), name="list_followers"),
    path("users/<str:username>following/", FollowingListView.as_view(), name="list_following"),
]

```

#### :computer:　5. FollowButtonコンポーネントの作成

```jsx

// frontend/src/components/FollowButton.jsx

import React, { useState, useEffect } from "react";
import API from "../api";

const FollowButton = ({username, isFollowing, onToggle }) => {
    const [following, setFollowing] = useState(isFollowing);

    useEffect(() => {
        setFollowing(isFollowing);
    }, [isFollowing]);

    const handleFollow = async () => {
        try {
            const response = await API.post(`users/${username}/follow/`);
            console.log("Follow Response:", response.data)
            setFollowing(true);
            onToggle(true);
        } catch (error) {
            console.error("Error:", error);
        }
    };

    const handleUnFollow = async () => {
        try {
            const response = await API.delete(`users/${username}/unfollow/`);
            console.log("UnFollow Response:", response.data);
            setFollowing(false);
            onToggle(false);
        } catch (error) {
            console.error("Error unFollow", error);
        }
    };

    return (
        <button
            onClick={following ? handleUnFollow : handleFollow}
            className={`px-4 py-2 text-sm font-semibold rounded-full transition-all duration-200 ease-in-out 
                ${following 
                    ? "bg-gray-300 text-gray-700 hover:bg-gray-400" 
                    : "bg-blue-500 text-white hover:bg-blue-600 shadow-md"}`}
        >
            {following ? "Following" : "Follow"}
        </button>
    );
};

export default FollowButton;

```

#### :computer: 6. UserItem.jsxに埋め込む

```jsx

// frontend/src/components/UserItem.jsx

import React from "react";
import FollowButton from "./FollowButton";

const UserItem = ({ user }) => {

    return (
        <div className="flex items-center justify-between p-4 mb-2 bg-white rounded shadow-sm hover:shadow-md transition-shadow duration-200">
            <div className="flex items-center space-x-3">
                <span className="font-medium">{user.username}</span>
            </div>
            <FollowButton
                userId={user.id}
                isFollowing={user.isFollowing}
                onToggle={() => {}}
            />
        </div>
    );
};

export default UserItem;

```

#### :computer:　7. FollowersList.jsxの作成

```jsx

// frontend/src/components/FollowersList.jsx

import React, { useState, useEffect } from "react";
import API from "../api";
import UserItem from "../components/UserItem";

const FollowersList = ({userId}) => {
    const [followers, setFollowers] = useState([]);

    useEffect(()=> {
        const fetchFollowers = async () => {
            try {
                const response = await API.get(`users/${userId}/followers/`);
                setFollowers(response.data);
            } catch (error) {
                console.error("Error fetching followers:", error);
            }
        };

        fetchFollowers();
    }, [userId]);

    return (
        <div className="max-w-md mx-auto mt-8">
            <h2 className="text-xl font-bold mb-4">Followers</h2>
            {followers.length > 0 ? (
                followers.map((follower) => (
                    <UserItem key={follower.id} user={follower} />
                ))
            ): (
                <p className="text-gray-600">No followers yet</p>
            )}
        </div>
    );
};

export default FollowersList;

```

#### :computer: 8.FollowingList.jsxの作成

```jsx

// frontend/src/pages/FollowingList.jsx

import React, { useState, useEffect} from "react";
import API from "../api";
import UserItem from "../components/UserItem";

const FollowingList = ({ userId }) => {
    const [followingList, setFollowingList] = useState([]);

    useEffect(() => {
        const fetchFollowing = async () => {
            try {
                const response = await API.get(`users/${userId}/following/`);
                setFollowingList(response.data);
            } catch (error) {
                console.error("Error fetching following:", error);
            }
        };

        fetchFollowing();
    }, [userId]);

    return (
        <div className="max-w-md mx-auto mt-8">
            <h2 className="text-xl font-bold mb-4">Following</h2>
            {followingList.length > 0 ? (
                followingList.map((user) => (
                    <UserItem key={user.id} user={user} />
                ))
            ) : (
                <p className="text-gray-600">Not following anyone yet</p>
            )}
        </div>
    );
};

export default FollowingList;

```

#### :computer:　9. App.jsに埋め込む

#### :computer: 10. UserProfile.jsxに埋め込む

```jsx

// frontend/src/pages/UserProfile.jsx

import React, { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import { Link } from "react-router-dom";
import FollowButton from "../components/FollowButton";
import API, { REACT_APP_HOST_URL} from "../api";
import FollowersList from "./FollowersList";
import FollowingList from "./FollowingList";

function UserProfile() {
  const { username } = useParams();
  const [profile, setProfile] = useState({});
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(true);
  const [isFollowing, setIsFollowing] = useState(false);
  const [activeTab, setActiveTab] = useState("profile");

  const fetchUserProfile = useCallback(async () => {
    try {
      setLoading(true);
      setError("");
      const res = await API.get(`users/${username}/profile/`);
      console.log("🚀 API Response:", res.data);
      setProfile(res.data);
      setIsFollowing(res.data.is_following);
      // res.dataの例: { "id": 1, "bio": "...", "profile_image": "/media/..." }
    } catch (err) {
      console.error("Failed to load user profile:", err);
      setError("Failed to load user profile.");
    } finally {
      setLoading(false);
    }
  }, [username]);

  useEffect(() => {
    if (!username) return;
    fetchUserProfile();
  }, [username, fetchUserProfile]);

  const handleFollowToggle = () => {
    setIsFollowing(!isFollowing);
  };


  if (loading) return <p>Loading user profile...</p>;
  if (error) return <p className="text-red-500">{error}</p>;
  if (!profile || !profile.id) return <p>No profile found.</p>;


  console.log("profile:", profile);
  console.log("profile.id:", profile.id);

  const imageUrl = profile?.profile_image
    ? profile.profile_image.startsWith("http")
      ? profile.profile_image
      : `${REACT_APP_HOST_URL}${profile.profile_image}`
    : `${REACT_APP_HOST_URL}static/images/default_profile.png`;

  // キャッシュバスター（例: 現在のタイムスタンプ）を付与
  const finalImageUrl = `${imageUrl}?v=${new Date().getTime()}`;
  

  // ここで profile.user.username があるかどうかは
  // バックエンドのProfileSerializerによりけり
  return (
    <div className="max-w-md mx-auto p-4 bg-white shadow mt-4">
      <h2 className="text-2xl mb-4">{username}'s Profile</h2>

      {/* ProfileタブかFollowersタブかFollowingタブを切り替えるボタンを配置 */}
      <div className="flex space-x-4 mb-4">
        <button
          onClick={() => setActiveTab("profile")}
          className={`px-3 py-1 rounded
            ${activeTab === "profile" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
        >
          Profile
        </button>
        <button
          onClick={() => setActiveTab("followers")}
          className={`px-3 py-1 rounded
            ${activeTab === "followers" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
        >
          Followers
        </button>
        <button
          onClick={() => setActiveTab("following")}
          className={`px-3 py-1 rounded
            ${activeTab === "following" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
          >
          Following
        </button>
      </div>

      {/* タブごとの表示内容を切り替え */}
      {activeTab === "profile" && (
        <div className="flex flex-col items-center">
          <img 
            src={finalImageUrl}
            alt="Profile"
            className="w-24 h-24 rounded-full object-cover mb-2"
          />
        <p className="text-gray-700">{profile.bio}</p>
        
        {profile?.id && (
          <div>
            <Link to={`/dm/${profile.username}`} className="text-blue-500 hover:underline mb-4 inline-block">
              Send DM
            </Link>
            <FollowButton
              userId={profile.id}
              username={profile.username}
              isFollowing={isFollowing}
              onToggle={handleFollowToggle}
            />
          </div>      
        )}
        </div>
      )}

      {activeTab === "followers" && (
        <div>
          <FollowersList userId={profile.id} />
        </div>
      )}

      {activeTab === "following" && (
        <div>
          <FollowingList userId={profile.id} />
        </div>
      )}
    </div>
  );
}

export default UserProfile;

```

#### :computer:　10. MyProfile.jsxに埋め込む

```jsx
// frontend/src/pages/MyProfile.jsx

import React, { useContext, useState, useEffect } from "react";
import { AuthContext } from "../contexts/AuthContext";
import API, {  REACT_APP_HOST_URL,  } from "../api";
import FollowersList from "./FollowersList";
import FollowingList from "./FollowingList";

function Profile() {
  const { user } = useContext(AuthContext); // ログインユーザー情報
  const [bio, setBio] = useState("");
  const [profileImage, setProfileImage] = useState(null);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(null);
  const [loading, setLoading] = useState(true);
  const [activeTab, setActiveTab] = useState("profile");

  useEffect(() => {
    fetchMyProfile();
  }, []);

  const fetchMyProfile = async () => {
    try {
      setLoading(true);
      setError(null);
      const res = await API.get("profile/me/");
      // res.data: { "id": ..., "bio": "...", "profile_image": "/media/..."}
      setBio(res.data.bio || "");
      // プロフィール画像表示用に setProfileImage には「ファイル」ではなく初期値不要
      // 既存の画像URLはres.data.profile_image
    } catch (err) {
      console.error("Failed to fetch my profile:", err);
      setError("Failed to load profile.");
    } finally {
      setLoading(false);
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError(null);
    setSuccess(null);

    try {
      const formData = new FormData();
      formData.append("bio", bio);
      if (profileImage) {
        formData.append("profile_image", profileImage);
      }

      const res = await API.patch("profile/me/", formData, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      });
      console.log("Profile update response:", res.data);
      setSuccess("Profile updated successfully!");
    } catch (err) {
      console.error("Profile update failed", err);
      setError("Failed to update profile. Please try again.");
    }
  };

  const handleImageChange = (e) => {
    if (e.target.files && e.target.files[0]) {
      setProfileImage(e.target.files[0]);
    }
  };

  if (loading) return <p>Loading profile...</p>;
  if (error) return <p className="text-red-500">{error}</p>;

  const imageUrl = user?.profile_image
    ? user.profile_image.startsWith("http")
      ? user.profile_image
      : `${REACT_APP_HOST_URL}${user.profile_image}`
    : `${REACT_APP_HOST_URL}static/images/default_profile.png`;

  // キャッシュバスター（例: 現在のタイムスタンプ）を付与
  const finalImageUrl = `${imageUrl}?v=${new Date().getTime()}`;


  return (
    <div className="flex justify-center items-center min-h-screen bg-gray-100">
      <div className="bg-white p-6 rounded shadow-md w-full max-w-md h-[600px] overflow-y-auto">
        <h2 className="text-2xl font-bold mb-4 text-center">My Profile</h2>
        {success && <div className="bg-green-100 text-green-700 p-2 mb-4 rounded">{success}</div>}
  
        {/* タブボタン */}
        <div className="flex space-x-4 mb-4">
          <button
            onClick={() => setActiveTab("profile")}
            className={activeTab === "profile" ? "bg-blue-500 text-white px-3 py-1 rounded" : "bg-gray-200 text-gray-700 px-3 py-1 rounded"}
          >
            Profile
          </button>
          <button
            onClick={() => setActiveTab("followers")}
            className={activeTab === "followers" ? "bg-blue-500 text-white px-3 py-1 rounded" : "bg-gray-200 text-gray-700 px-3 py-1 rounded"}
          >
            Followers
          </button>
          <button
            onClick={() => setActiveTab("following")}
            className={activeTab === "following" ? "bg-blue-500 text-white px-3 py-1 rounded" : "bg-gray-200 text-gray-700 px-3 py-1 rounded"}
          >
            Following
          </button>
        </div>
  
        {/* タブの中身 */}
        {activeTab === "profile" && (
          <form onSubmit={handleSubmit}>
            <div className="mb-4 flex flex-col items-center">
              <img
                src={finalImageUrl}
                alt="Profile"
                className="w-24 h-24 rounded-full object-cover mb-2"
              />
              <input type="file" accept="image/*" onChange={handleImageChange} />
            </div>
  
            <div className="mb-4">
              <label className="block text-gray-700">Bio</label>
              <textarea
                className="w-full px-3 py-2 border rounded"
                value={bio}
                onChange={(e) => setBio(e.target.value)}
                placeholder="Tell us about yourself"
                rows="3"
              />
            </div>
  
            <button
              type="submit"
              className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition duration-200"
            >
              Update Profile
            </button>
          </form>
        )}
  
        {activeTab === "followers" && (
          <div>
            <FollowersList userId={user.id} />
          </div>
        )}
  
        {activeTab === "following" && (
          <div>
            <FollowingList userId={user.id} />
          </div>
        )}
      </div>
    </div>
  );
}

export default Profile;

```