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

## 初めに

今日の分を進めていくとこういう仕上がりになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3895655/0603786b-be3a-d65d-f1b0-3fee2cf331a2.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3895655/47d57ce5-56d9-d657-bffa-e7aedf6e74fb.png)



## 目次

#### 1. ユーザーのプロフィール情報を表示・編集
#### 2. ツイートの投稿や一覧表示の実践（フロント）
#### 3. Like機能の実装
#### 4. Comment機能の実装

## 1.ユーザーのプロフィール情報を表示・編集

#### :computer: .staticとmediaの作成

```

backend/              <-- プロジェクトルート（BASE_DIR）
├── manage.py
├── static/
│   └── images/
│         └── default_profile.png
├── media/            <-- ユーザーアップロード画像用
└── backend/          <-- settings.py 等があるディレクトリ


```

#### :computer: settings.pyの設定

```python

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"  # 本番用：collectstatic によって集約される場所

# 開発時に参照する static ファイルのディレクトリ（例）
STATICFILES_DIRS = [
    BASE_DIR / "static",  # ここに default_profile.png を含む "images" フォルダがあることを確認
]

```

#### :computer: backend/urls.pyに追記

```python

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

```

#### :computer: .envでBASE_URLの設定

```

# frontend/.env.local

REACT_APP_HUST_URL=http://localhost:8000

```




#### :computer: Profile.jsxの作成

ユーザーのプロフィール情報（プロフィール画像や自己紹介）を表示・編集するページを作成します。


```jsx

// frontend/src/pages/Profile.jsx

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

function Profile() {
  const { user, accessToken, logout } = useContext(AuthContext);
  const [bio, setBio] = useState(user ? user.bio : "");
  const [profileImage, setProfileImage] = useState(null);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(null);

  useEffect(() => {
    if (user) {
      setBio(user.bio);
    }
  }, [user]);

  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",
        },
      });

      // 更新されたユーザー情報をContextに反映
      // ここでは再度プロフィールを取得する方法もあります
      // または、AuthContextにsetUserを追加して直接更新することも可能

      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]);
    }
  };

  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">
      <form onSubmit={handleSubmit} className="bg-white p-6 rounded shadow-md w-full max-w-md">
        <h2 className="text-2xl font-bold mb-4 text-center">Your Profile</h2>
        
        {error && <div className="bg-red-100 text-red-700 p-2 mb-4 rounded">{error}</div>}
        {success && <div className="bg-green-100 text-green-700 p-2 mb-4 rounded">{success}</div>}
        
        <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 focus:outline-none focus:ring focus:border-blue-300"
            value={bio}
            onChange={(e) => setBio(e.target.value)}
            placeholder="Tell us about yourself"
            rows="3"
          ></textarea>
        </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>
        
        <button
          type="button"
          onClick={logout}
          className="w-full mt-2 bg-red-500 text-white py-2 rounded hover:bg-red-600 transition duration-200"
        >
          Logout
        </button>
      </form>
    </div>
  );
}

export default Profile;

```

#### :pencil2: 解説

#### 1. Contextの利用
- useContext(AuthContext)で現在のユーザー情報とlogout関数を取得。

#### 2. Stateの管理
- `bio:` ユーザーの自己紹介文。
- `profileImage:` 新しくアップロードするプロフィール画像。
- `error`, `success:` エラーメッセージと成功メッセージの表示管理。

#### 3. フォームのハンドリング
- handleSubmit: フォーム送信時にプロフィール情報を更新。
- FormDataを使用して、テキストと画像を一緒に送信。
- 成功時にはsuccessメッセージを表示。
- 失敗時にはerrorメッセージを表示。
- handleImageChange: 画像ファイルが選択されたときにprofileImageを更新。

#### 3. デザイン
- Tailwind CSSを使用して、フォームを中央に配置し、整ったデザインにしています。
- プロフィール画像が存在しない場合はデフォルト画像を表示。

#### 4. エラーハンドリング
- `try-catchブロック:` APIリクエスト時にエラーが発生した場合、catchブロックでエラーメッセージを設定し、ユーザーに通知。
- フォーム入力のバリデーション: HTMLのrequired属性を使用し、必須入力フィールドを強制。

## 2.ツイートの投稿や一覧表示の実践（フロント）

#### :computer: TweetList.jsxの作成

```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))
    };

    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}/>
                ))}
            </div>
        </div>
    );
}

export default TweetList;

```

#### :pencil2: 解説

#### 1. TweetListコンポーネントの定義
- accessToken → AuthContext から認証トークンを取得（ログイン状態の確認に使う）
- tweets → ツイート一覧のデータ（配列）
- error → API 取得時のエラー情報を格納
- loading → データをロード中かどうかの状態

#### 2. fetchTweetsでツイート一覧を取得
- loading を true に設定（データ取得中であることを UI に反映）
- エラー (error) をリセット
- API.get("tweets/") を実行して、サーバーからツイート一覧を取得
- 取得したデータ (res.data) を setTweets で tweets ステートに保存
- もしエラーが発生した場合は、エラーメッセージを setError に格納
- loading を false に変更し、データ取得終了

#### 3. 初回レンダリング時にツイートを取得

```jsx

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

```

- `useEffect`を使うことで、コンポーネントが マウントされたとき (`[]`のため一度だけ)に`fetchTweets()`を実行
- これにより、ページが表示されたときにAPIからツイート一覧を取得する

#### 4. 新しいツイートを追加

```jsx

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

```

- `TweetForm`でツイートが作成されたら、新しいツイート`(newTweet)`を`tweets`の先頭に追加
- これにより、リアルタイムでツイート一覧が更新される

#### 5. ツイート削除時のハンドラー

```jsx

const handleDeleteTweet = (deletedTweetId) => {
  setTweets(tweets.filter(tweet => tweet.id !== deletedTweetId));
};

```

ツイートを削除したら、その`id`に一致しないツイートだけを`setTweets`で更新
これにより、削除後に一覧から即座に消える

#### 6. UIのレンダリング

```jsx

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

```

- `accessToken`がある場合のみ`TweetForm`を表示（ログインしているとツイートできる）


```jsx

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

```

- `tweets.map()`を使って`TweetItem`を一覧表示
- `onDelete`に`handleDeleteTweet`を渡すことで、削除が実行されたときに一覧を更新できる

##

#### :computer: TweetForm.jsx作成

```jsx

// frontend/src/components/TweetForm.jsx

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

function TweetForm({ onTweetCreated }) {
    const{ accessToken } = useContext(AuthContext);
    const [content, setContent] = useState("");
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(false);

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

        // バリデーション、空白は送信しない
        if (!content.trim()) {
            setError("Tweet content cannot be empty!");
            return;
        }

        setLoading(true);
        try {
            const res = await API.post(
                "tweets/", 
                { content },
                {
                    headers: {
                        Authorization: `Bearer ${accessToken}`,
                    },
                }
            );
            onTweetCreated(res.data);
            setContent("");
        } catch (err) {
            console.error("Failed to post tweet", err);
            setError("Failed to post tweet.please try again.");
        }
        setLoading(false);
    };

    return (
        <form onSubmit={handleSubmit} className="flex flex-col gap-2">
            {error && <div className="bg-red-100 text-red-700 p-2 rounded">{error}</div>}

            <textarea
                className="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300"
                value={content}
                onChange={(e) => setContent(e.target.value)}
                placeholder="What's happening?"
                rows="3"
                required
            ></textarea>

            <button
                type="submit"
                disabled={loading}
                className={`w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition duration-200${
                    loading ? "opacity-50 cursor-not-allowed" : ""
                }`}
            >
                {loading ? "Posting..." : "Tweet"}
            </button>
        </form>
    );
}

export default TweetForm;

```
#### :pencil2: 解説

#### 1. `e.preventDefault();` → フォームのデフォルトの送信を防ぐ
#### 2. `setError(null);` → 送信前にエラーメッセージをリセット
#### 3. バリデーション
- `ontent.trim()`を使って、空白のツイートを防止
- 空の場合は`setError()`でエラーメッセージを表示
#### 4. APIリクエスト
- `API.post("tweets/", { content }, { headers })`を実行し、新しいツイートを送信
- 成功したら`onTweetCreated(res.data);`を実行し、新しいツイートを親コンポーネント (TweetList.jsx) に渡す
- `setContent("");`で入力欄をクリア
- `catch (err) {}`でエラーが発生した場合にエラーメッセージを `etError()`に格納

##

#### :computer: TweetItem.jsxの作成

```jsx

// frontend/src/components/TweetItem.jsx

// src/components/TweetItem.jsx
import React, { useState } from "react";
import CommentList from "./CommentList";　←後で作ります
import LikeToggle from "./LikeToggle";　←後で作ります

const TweetItem = ({ tweet, onDelete }) => {
  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">{tweet.username}</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}
        />

        <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;

```

## 3.Like機能の実装


#### :computer: 1. python manage.py startapp likes

#### :computer: 2. settings.pyでアプリを認識させる

#### :computer: 3. Likeモデルの設計

```python

# backend/likes/models.py

from django.db import models
from django.conf import settings
from tweets.models import Tweet

User = settings.AUTH_USER_MODEL

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

    class Meta:
        unique_together = ('user', 'tweet')  # 同じユーザーが同じツイートを複数回いいねできないように

    def __str__(self):
        return f"{self.user} likes Tweet {self.tweet.id}"

```

#### :computer: 4. シリアライザの作成

```python

# backend/likes/serializers.py

from rest_framework import serializers
from.models import Like

class LikeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Like
        fields = ["id", "user", "tweet", "created_at"]
        read_only_fields = ["id", "tweet", "created_at"]

```

- Read-only Fields: id, user, created_atはクライアントから送信される必要がないため、読み取り専用に設定。

#### :computer: 5. ビューの作成

```python

# backend/likes/views.py

from rest_framework.views import APIView
from rest_framework import permissions, status
from rest_framework.response import Response
from tweets.models import Tweet
from.serializers import LikeSerializer
from.models import Like


class LikeToggleAPIView(APIView):
    permission_classes = [permissions.IsAuthenticated]

    def post(self, request, tweet_id):

        try:
            tweet = Tweet.objects.get(id=tweet_id)
        except Tweet.DoesNotExist:
            return Response({"error": "Tweet not found."}, status=status.HTTP_404_NOT_FOUND)

        like, created = Like.objects.get_or_create(user=request.user, tweet=tweet)
        if created:
            serializer = LikeSerializer(like)
            return Response({
                "message": "Tweet liked successfully.",
                "likes_count": tweet.likes.count(),
                "like": serializer.data
            }, status=status.HTTP_201_CREATED)
        else:
            return Response({"detail": "Already liked."}, status=status.HTTP_200_OK)
        
    def delete(self, request, tweet_id):

        try:
            tweet = Tweet.objects.get(id=tweet_id)
            like = Like.objects.get(user=request.user, tweet=tweet)
            like.delete()
            return Response({
                "message":"Like removed.",
                "likes_count": tweet.likes.count(),
                }, status=status.HTTP_200_OK)
        except Tweet.DoesNotExist:
            return Response({"error": "Tweet not found."}, status=status.HTTP_404_NOT_FOUND)
        except Like.DoesNotExist:
            return Response({"error": "Like does not exist."}, status=status.HTTP_400_BAD_REQUEST)

```

#### :pencil2: 解説

#### 1. LikeSerializer(like) でシリアライズとは？

シリアライズ（Serialization）とは、 PythonのオブジェクトをJSON形式などに変換する処理 のこと。

```jsx

serializer = LikeSerializer(like)

```

このコードでは、`Like`モデルのインスタンス `(like)`を`LikeSerializer`を使って JSON形式に変換している。

#### 2. なぜシリアライズが必要なのか？
Django REST Framework (DRF) では、モデルのデータをそのままレスポンスとして返せない。
レスポンスをクライアント（フロントエンド）に送るためには、
`Pythonオブジェクト → JSON`に変換する必要がある。

```python

return Response({
    "message": "Tweet liked successfully.",
    "likes_count": tweet.likes.count(),
    "like": serializer.data
}, status=status.HTTP_201_CREATED)

```
この`serializer.data`が、
LikeSerializerによってJSON形式の辞書（Pythonのdict）に変換されたデータになる。

#### 3. 例

`LikeSerializer`がどのように動作するかを理解するために、
`serializers.py`にある`LikeSerializer`を想定してみる。

```python

from rest_framework import serializers
from .models import Like

class LikeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Like
        fields = ["id", "user", "tweet", "created_at"]  # どのフィールドをシリアライズするか指定

```

この`LikeSerializer`を使うことで、
モデルのデータが**JSONに変換**される。

例えば、
`like = Like.objects.create(user=request.user, tweet=tweet)`をシリアライズすると

```jsx

{
    "id": 3,
    "user": 5,
    "tweet": 12,
    "created_at": "2025-02-02T12:00:00Z"
}

```

これでクライアント側（Reactなど）が、
データをJSONとして受け取り「いいね」の情報を表示できるようになる。

##

#### :computer: 6. ルーティングの設定

```python

# backend/likes/urls.py

from django.urls import path
from.views import LikeToggleAPIView

urlpatterns = [
    path("tweets/<int:tweet_id>/like/", LikeToggleAPIView.as_view(), name="tweet_like"),
    path("tweets/<int:tweet_id>/unlike/", LikeToggleAPIView.as_view(), name="tweet_unlike"),
]

```

#### :computer: 7. プロジェクト全体のbackend/urls.pyにlikes/urls.pyをインクルード。

#### :computer: 8. LikeToggle.jsxを作成してTweetItem.jsxに埋め込む（上記ではすでに埋め込んでいます）

```jsx

// frontend/src/components/LikeToggle.jsx

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

// TweetItem.jsxからprops(tweetID, initialLiked, initialLikeCount)を受け取る
const LikeToggle = ({ tweetId, initialLiked, initialLikeCount }) => {
    const [liked, setLiked] = useState(initialLiked);
    const [likeCount, setLikeCount] = useState(initialLikeCount || 0);

    const handleLike = async () => {
        try {
            const res = await API.post(`tweets/${tweetId}/like/`);
            console.log("Like Response:", res.data);
            setLiked(true);
            setLikeCount((prev) => prev + 1);
        } catch (error) {
            console.error("Error liking tweet:", error);
        }
    };

    const handleUnlike = async () => {
        try {
            const res = await API.delete(`tweets/${tweetId}/unlike/`);
            console.log("Unlike Response:", res.data)
            setLiked(false);
            setLikeCount((prev) => Math.max(prev - 1, 0));
        } catch (error) {
            console.error("Error unliking tweet:", error);
        }
    };

    return (
        <div>
            <span>{likeCount} Likes</span>
            {liked ? (
                <button onClick={handleUnlike} className="text-red-500">
                    Unlike
                </button>
            ) : (
                <button onClick={handleLike} className="text-blue-500">
                    Like
                </button>
            )}
        </div>
    );
};

export default LikeToggle;

```

## 3.Comment機能の実装

#### :computer: 1. python manage.py startapp comments

#### :computer: 2. settings.pyでアプリを認識させる

#### :computer: 3. Commentsモデルの設計

```python

# backend/comments/models.py

from django.db import models
from django.conf import settings
from tweets.models import Tweet

User = settings.AUTH_USER_MODEL

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

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

```


#### :computer: 4. シリアライザの作成

```python

# backend/comments/serializers.py

from rest_framework import serializers
from .models import Comment
from tweets.models import Tweet

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ["id", "tweet", "user", "content", "created_at"]
        read_only_fields = ["user", "tweet", "created_at"]

```

#### :computer: 5. ビューの作成

```python

# backend/comments/views.py

from rest_framework import generics, permissions
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from.models import Comment
from tweets.models import Tweet
from.serializers import CommentSerializer

class TweetCommentViewSet(generics.ListCreateAPIView):

    # /api/tweets/<tweet_id>/comments/ に完全一致
    # GET: 特定ツイートのコメント一覧
    # POST: 特定ツイートにコメント追加
    
    serializer_class = CommentSerializer
    permission_classes = [permissions.IsAuthenticated]

    #   URLパラメータの tweet_id をもとにフィルタリング
    def get_queryset(self):
        
        tweet_id = self.kwargs["tweet_id"]
        return Comment.objects.filter(tweet_id=tweet_id).order_by("-created_at")

    def perform_create(self, serializer):
        
        # POST時にTweetを自動で紐付け、ユーザーも自動セット
        
        tweet_id = self.kwargs["tweet_id"]
        tweet = get_object_or_404(Tweet, id=tweet_id)
        serializer.save(usesr=self.request.user, tweet=tweet)

class TweetCommentDetailView(generics.RetrieveUpdateDestroyAPIView):

        # GET    /api/tweets/<tweet_id>/comments/<comment_id>/  → 単一コメント詳細
        # PATCH  /api/tweets/<tweet_id>/comments/<comment_id>/  → コメント編集
        # DELETE /api/tweets/<tweet_id>/comments/<comment_id>/  → コメント削除

    def get_queryset(self):
             
        tweet_id = self.kwargs["tweet_id"]
        return Comment.objects.filter(tweet_id=tweet_id)
    
    def get_object(self):
        queryset = self.get_queryset()
        comment_id = self.kwargs["comment_id"]
        return get_object_or_404(queryset, id=comment_id)
    
    def perform_update(self, serializer):
        serializer.save()

```

#### :pencil2: 解説 : `APIView` と `汎用ビュークラス` の違い**

 #### likes/views.py (LikeToggleAPIView)

このクラスは `APIView` を継承しており、メソッドのシグネチャで直接tweet_idを受け取るように定義。
つまり、URL定義で `<int:tweet_id>` として渡される`tweet_id`が、`post(self, request, tweet_id)` の引数として自動的にセットされます。
そのため、メソッド内で `tweet_id` を直接使えます。

#### comments/views.py (TweetCommentViewSet, TweetCommentDetailView)

こちらは `汎用ビュークラス`（`ListCreateAPIView`, `RetrieveUpdateDestroyAPIView`） を使っているため、URLパラメータはビューインスタンスの `self.kwargs` に格納されます。
そのため、`get_queryset` や `get_object` メソッド内では、`tweet_id = self.kwargs["tweet_id"]` として取得します。

#### Response と status について

汎用ビューは、リクエストを処理した結果を自動的に `Response` オブジェクトにラップし、適切な HTTP ステータスコード（例: 200, 201 など）を設定して返す仕組みが組み込まれています。そのため、ビュー内で個別に `Response()` を呼び出す必要がありません。

例えば、GET リクエストでコメント一覧を返す場合、`get_queryset()` の戻り値がシリアライザを通して `Response` に変換されます。
同様に、POST でコメント作成時は、`perform_create()` メソッド内で `serializer.save()` を呼び出した後、汎用ビューがシリアライズされたデータを `Response` として返すように処理します。

一方で、likes/views.py の `LikeToggleAPIView` のように、`APIView` を直接継承した場合は、各メソッド内で `Response` を明示的に返す必要があります。

#### エンドポイント

- `/api/tweets/<id>/comments/`に完全一致するURLで、特定のツイートに紐づくコメント一覧取得 + コメント投稿を行いたいという要件を実現するには、カスタムView + 独自URLを用意すると分かりやすいです。

 - DRFの`ModelViewSet`や`DefaultRouter`で「/api/comments/」を自動生成するのではなく、手動でルーティングして`/api/tweets/<id>/comments/`というURL構造に合わせます。

#### :computer: 6. ルーティングの設定

```python

# backend/comments/urls.py

from django.urls import path
from.views import TweetCommentViewSet,TweetCommentDetailView

urlpatterns = [
    path("tweets/<int:tweet_id>/comments/", TweetCommentViewSet.as_view(), name="tweet_comments_list_create"),
    path("tweets/<int:tweet_id>/comments/<int:comment_id>/", TweetCommentDetailView.as_view(), name="tweet_comment_detail"),
]

```

#### :computer: 7. プロジェクト全体のbackend/urls.pyにlikes/urls.pyをインクルード。

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

```jsx

// frontend/src/components/CommentList.jsx

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

const CommentList = ({ tweetId }) => {
  const [comments, setComments] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    if (!tweetId) return;
    setLoading(true);
    setError("");

    // GET /api/tweets/<tweetId>/comments/
    API.get(`tweets/${tweetId}/comments/`)
      .then((res) => {
        setComments(res.data);
      })
      .catch((err) => {
        console.error("Error fetching comments:", err);
        setError("Failed to load comments.");
      })
      .finally(() => {
        setLoading(false);
      });
  }, [tweetId]);

  // 新規コメントが追加されたとき
  const handleCommentAdded = (newComment) => {
    setComments((prev) => [newComment, ...prev]);
  };

  // コメントが更新されたとき
  const handleCommentUpdate = (updatedComment) => {
    setComments((prev) =>
      prev.map((c) => (c.id === updatedComment.id ? updatedComment : c))
    );
  };

  // コメントが削除されたとき
  const handleCommentDelete = (deletedId) => {
    setComments((prev) => prev.filter((c) => c.id !== deletedId));
  };

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

  return (
    <div className="mt-4 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg shadow">
      <h4 className="font-bold mb-2">Comments ({comments.length})</h4>

      {/* 新規投稿フォーム */}
      <CommentForm tweetId={tweetId} onCommentAdded={handleCommentAdded} />

      {comments.length > 0 ? (
        comments.map((comment) => (
          <CommentItem
            key={comment.id}
            tweetId={tweetId}
            comment={comment}
            onUpdate={handleCommentUpdate}
            onDelete={handleCommentDelete}
          />
        ))
      ) : (
        <p className="text-gray-500">No comments yet.</p>
      )}
    </div>
  );
};

export default CommentList;

```

#### :computer: 9. CommentItem.jsxの作成

```jsx

// frontend/src/components/CommentItem.jsx

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

const CommentItem = ({tweetId, comment, onUpdate, onDelete }) => {
    const [editing, setEditing] = useState(false);
    const [editContent, setEditContent] = useState(comment.content);

    // コメント編集
    const handleEditSave = async () => {
        try {
            const res = await API.patch(`tweets/${tweetId}/comments/${comment.id}/`, {
                content: editContent,
            });
            onUpdate(res.data);
            setEditing(false);
        } catch (err) {
            console.error("Failed to edit comment:", err);
            alert("Failed to edit comment.");
        }
    };

    const handleDelete = async () => {
        if (!window.confirm("Are you sure that deleting this comment?")) return;
        try {
            await API.delete(`tweets/${tweetId}/comments/${comment.id}/`);
            onDelete(comment.id);
        } catch (err) {
            console.error("Failed to delete comment:", err);
            alert("Failed to delete comment.");
        }
    };

    return (
        <div className="border-b p-2 flex justify-between items-center">
            {editing ? (
                // 編集モード
                <>
                    <textarea
                        className="w-full border"
                        value={editContent}
                        onChange={(e) => setEditContent(e.target.value)}
                    />
                    <div>
                        <button onClick={handleEditSave} className="text-green-500">Save</button>
                        <button onClick={() => setEditing(false)} className="text-gray-500">Cancel</button>
                    </div>
                </>
            ) : (
                // 表示モード
                <>
                    <div>
                        <p>
                            <strong>{comment.user.username}</strong>: {comment.content}
                        </p>
                        <small>{new Date(comment.created_at).toLocaleString()}</small>
                    </div>
                    <div>
                        <button onClick={() => setEditing(true)} className="text-blue-500">Edit</button>
                        <button onClick={handleDelete} className="text-red-500">Delete</button>
                    </div>
                </>
            )}
        </div>
    );
};

export default CommentItem;

```

#### :computer: 10. CommentForm.jsxの作成

```jsx

// frontend/src/components/CommentForm.jsx

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

const CommentForm = ({tweetId, onCommentAdded }) => {
    const [content, setContent] = useState("");
    const [error, setError] = useState("");

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

        if (!content.trim()) {
            setError("Comment cannot be empty. Please try again.");
            return;
        }

        console.log("POST to /api/tweets/<tweetId>/comments/:", { content });

        try {
            const res = await API.post (`tweets/${tweetId}/comments/`, { content });
            console.log("Commented Successfull.:", res.data);

            // 親コンポーネントへ新しいコメントを通知
            if (onCommentAdded) {
                onCommentAdded(res.data);
            }
            setContent(""); //入力フォームをクリア
        } catch (err) {
            console.error("Failed to post comment.:", err);
            setError("Failed to post comment. Please try again.");
        }
    };

    return (
        <div className="mt-4 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg shadow">
            <form onSubmit={handleSubmit} className="flex flex-col">
                {error && <p className="text-red-500 text-sm">a{error}</p>}
                <textarea
                    className="w-full p-2 border rounded dark:text-black"
                    placeholder="Write comment..."
                    value={content}
                    onChange={(e) => setContent(e.target.value)}
                    rows="2"
                    required
                />
                <button
                    type="submit"
                    className="mt-2 bg-blue-500 hover:bg-blue-600 text-white py-1 px-4 rounded"
                >
                    Comment
                </button>
            </form>
        </div>
    );
};

export default CommentForm;

```

### 今日はここまで:smile: