풀스택 반려동물 산책 기록장 - 바이브코딩 레시피
입문 PHP + JavaScript Laravel + React AI: Claude

풀스택 반려동물 산책 기록장

리포지토리명 pet-walk-logger-fullstack
조회수 110 0건 제출

문제 설명

🐾 풀스택 반려동물 산책 기록장

반려동물과 함께한 산책을 기록하고 관리하는 웹 애플리케이션입니다. Laravel 13.x로 RESTful API 백엔드를 구축하고, React 19로 사용자 인터페이스를 만듭니다. 프로그래밍 입문자도 AI 도구의 도움으로 풀스택 개발 경험을 쌓을 수 있습니다.

필수 기능

  • 반려동물 프로필 등록 (이름, 종류, 나이)
  • 산책 기록 추가 (날짜, 시간, 거리, 메모)
  • 산책 기록 목록 조회 (최신순 정렬)
  • 산책 기록 수정 및 삭제
  • 반려동물별 총 산책 거리 통계
  • 반응형 디자인 (모바일/데스크톱)

보너스 기능

  • 산책 날씨 아이콘 표시 (맑음/흐림/비)
  • 주간/월간 산책 통계 차트
  • 산책 기록 사진 첨부 기능
  • 산책 경로 지도 표시 (구글맵 연동)

기술 스택

  • 백엔드: Laravel 13.x (RESTful API)
  • 프론트엔드: React 19 (Vite)
  • 데이터베이스: SQLite
  • 스타일링: Tailwind CSS
  • HTTP 클라이언트: Axios

제약 조건

  • • Laravel 13.x와 React 19 최신 버전 사용
  • • 백엔드와 프론트엔드는 별도 폴더로 분리 (backend/, frontend/)
  • • Laravel은 API 전용 (routes/api.php 사용)
  • • React는 Vite로 프로젝트 생성
  • • CORS 설정 필수 (Laravel에서 프론트엔드 origin 허용)
  • • SQLite 데이터베이스 사용 (개발 편의성)
  • • RESTful API 설계 원칙 준수 (GET, POST, PUT, DELETE)
  • • React에서 useState만 사용 (복잡한 상태관리 라이브러리 금지)
  • • Tailwind CSS로 깔끔한 UI 구현
  • • 모든 API 응답은 JSON 형식
  • • 에러 핸들링 포함 (400, 404, 500 등)
  • • 반응형 디자인 필수

프롬프트 레시피

아래 프롬프트를 순서대로 AI에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.

1
Step 1
mkdir pet-walk-logger-fullstack && cd pet-walk-logger-fullstack

이 폴더에서 풀스택 프로젝트를 시작합니다.

# Laravel 13.x API 백엔드 생성

backend 폴더에 Laravel 13.x 프로젝트를 생성해주세요.

```bash
composer create-project laravel/laravel:^13.0 backend
cd backend
```

## 데이터베이스 설정

1. .env 파일을 SQLite로 설정:
```
DB_CONNECTION=sqlite
```

2. database/database.sqlite 파일 생성:
```bash
touch database/database.sqlite
```

## 모델 및 마이그레이션 생성

### Pet 모델 (반려동물)
```bash
php artisan make:model Pet -m
```

`database/migrations/xxxx_create_pets_table.php`:
```php
public function up()
{
    Schema::create('pets', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('species'); // 종류 (강아지, 고양이 등)
        $table->integer('age')->nullable();
        $table->timestamps();
    });
}
```

### Walk 모델 (산책 기록)
```bash
php artisan make:model Walk -m
```

`database/migrations/xxxx_create_walks_table.php`:
```php
public function up()
{
    Schema::create('walks', function (Blueprint $table) {
        $table->id();
        $table->foreignId('pet_id')->constrained()->onDelete('cascade');
        $table->date('walk_date');
        $table->integer('duration_minutes'); // 산책 시간(분)
        $table->decimal('distance_km', 5, 2); // 산책 거리(km)
        $table->text('memo')->nullable();
        $table->timestamps();
    });
}
```

## 마이그레이션 실행
```bash
php artisan migrate
```

## CORS 설정

`config/cors.php`에서 프론트엔드 origin 허용:
```php
'paths' => ['api/*'],
'allowed_origins' => ['http://localhost:5173'],
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
```

프로젝트 구조와 데이터베이스가 준비되었는지 확인해주세요.

기대 결과: Laravel 13.x 프로젝트가 backend 폴더에 생성되고, SQLite 데이터베이스에 pets와 walks 테이블이 마이그레이션됨. CORS 설정 완료.

2
Step 2
# Laravel API 컨트롤러 및 라우트 구현

## PetController 생성

```bash
php artisan make:controller Api/PetController --api
```

`app/Http/Controllers/Api/PetController.php`:
```php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Pet;
use Illuminate\Http\Request;

class PetController extends Controller
{
    public function index()
    {
        return Pet::with('walks')->get();
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'species' => 'required|string|max:255',
            'age' => 'nullable|integer|min:0',
        ]);

        $pet = Pet::create($validated);
        return response()->json($pet, 201);
    }

    public function show(Pet $pet)
    {
        return $pet->load('walks');
    }

    public function update(Request $request, Pet $pet)
    {
        $validated = $request->validate([
            'name' => 'string|max:255',
            'species' => 'string|max:255',
            'age' => 'nullable|integer|min:0',
        ]);

        $pet->update($validated);
        return response()->json($pet);
    }

    public function destroy(Pet $pet)
    {
        $pet->delete();
        return response()->json(null, 204);
    }
}
```

## WalkController 생성

```bash
php artisan make:controller Api/WalkController --api
```

`app/Http/Controllers/Api/WalkController.php`:
```php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Walk;
use Illuminate\Http\Request;

class WalkController extends Controller
{
    public function index()
    {
        return Walk::with('pet')->orderBy('walk_date', 'desc')->get();
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'pet_id' => 'required|exists:pets,id',
            'walk_date' => 'required|date',
            'duration_minutes' => 'required|integer|min:1',
            'distance_km' => 'required|numeric|min:0',
            'memo' => 'nullable|string',
        ]);

        $walk = Walk::create($validated);
        return response()->json($walk->load('pet'), 201);
    }

    public function show(Walk $walk)
    {
        return $walk->load('pet');
    }

    public function update(Request $request, Walk $walk)
    {
        $validated = $request->validate([
            'walk_date' => 'date',
            'duration_minutes' => 'integer|min:1',
            'distance_km' => 'numeric|min:0',
            'memo' => 'nullable|string',
        ]);

        $walk->update($validated);
        return response()->json($walk->load('pet'));
    }

    public function destroy(Walk $walk)
    {
        $walk->delete();
        return response()->json(null, 204);
    }
}
```

## 모델 관계 설정

`app/Models/Pet.php`:
```php
public function walks()
{
    return $this->hasMany(Walk::class);
}
```

`app/Models/Walk.php`:
```php
protected $fillable = ['pet_id', 'walk_date', 'duration_minutes', 'distance_km', 'memo'];

public function pet()
{
    return $this->belongsTo(Pet::class);
}
```

## API 라우트 등록

`routes/api.php`:
```php
use App\Http\Controllers\Api\PetController;
use App\Http\Controllers\Api\WalkController;

Route::apiResource('pets', PetController::class);
Route::apiResource('walks', WalkController::class);
```

## 서버 실행
```bash
php artisan serve
```

API가 http://localhost:8000/api 에서 작동하는지 확인해주세요.

기대 결과: Laravel API 엔드포인트가 완성됨. /api/pets와 /api/walks에서 CRUD 작업 가능. 모델 관계(Pet-Walk)가 설정되어 데이터 연동 준비 완료.

3
Step 3
# React 19 프론트엔드 생성

프로젝트 루트로 돌아가서 frontend 폴더에 React 프로젝트를 생성합니다.

```bash
cd ..
npm create vite@latest frontend -- --template react
cd frontend
npm install
```

## 필요한 패키지 설치

```bash
npm install axios
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

## Tailwind CSS 설정

`tailwind.config.js`:
```js
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

`src/index.css`:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

## Axios 설정

`src/api/axios.js`:
```js
import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:8000/api',
  headers: {
    'Content-Type': 'application/json',
  },
});

export default api;
```

## 기본 컴포넌트 구조 생성

`src/App.jsx`:
```jsx
import { useState } from 'react';
import PetList from './components/PetList';
import WalkList from './components/WalkList';
import AddPetForm from './components/AddPetForm';
import AddWalkForm from './components/AddWalkForm';

function App() {
  const [activeTab, setActiveTab] = useState('pets');
  const [refreshKey, setRefreshKey] = useState(0);

  const handleRefresh = () => {
    setRefreshKey(prev => prev + 1);
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
      <div className="container mx-auto px-4 py-8">
        <header className="text-center mb-8">
          <h1 className="text-4xl font-bold text-indigo-900 mb-2">🐾 반려동물 산책 기록장</h1>
          <p className="text-gray-600">우리 반려동물의 건강한 산책을 기록하세요</p>
        </header>

        <div className="flex justify-center mb-6">
          <div className="bg-white rounded-lg shadow-md p-1 inline-flex">
            <button
              onClick={() => setActiveTab('pets')}
              className={`px-6 py-2 rounded-md transition-colors ${
                activeTab === 'pets'
                  ? 'bg-indigo-600 text-white'
                  : 'text-gray-700 hover:bg-gray-100'
              }`}
            >
              반려동물 관리
            </button>
            <button
              onClick={() => setActiveTab('walks')}
              className={`px-6 py-2 rounded-md transition-colors ${
                activeTab === 'walks'
                  ? 'bg-indigo-600 text-white'
                  : 'text-gray-700 hover:bg-gray-100'
              }`}
            >
              산책 기록
            </button>
          </div>
        </div>

        <div className="max-w-4xl mx-auto">
          {activeTab === 'pets' ? (
            <div>
              <AddPetForm onSuccess={handleRefresh} />
              <PetList key={refreshKey} onUpdate={handleRefresh} />
            </div>
          ) : (
            <div>
              <AddWalkForm onSuccess={handleRefresh} />
              <WalkList key={refreshKey} onUpdate={handleRefresh} />
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

export default App;
```

빈 컴포넌트 파일들을 생성해주세요:
- `src/components/PetList.jsx`
- `src/components/WalkList.jsx`
- `src/components/AddPetForm.jsx`
- `src/components/AddWalkForm.jsx`

각 파일에 기본 export 구조만 만들어주세요.

기대 결과: React 프로젝트가 frontend 폴더에 생성되고, Tailwind CSS와 Axios가 설치됨. App.jsx에 탭 전환 UI와 기본 레이아웃이 구현됨. 컴포넌트 파일 구조 준비 완료.

4
Step 4
# 반려동물 관리 컴포넌트 구현

## AddPetForm 컴포넌트

`src/components/AddPetForm.jsx`:
```jsx
import { useState } from 'react';
import api from '../api/axios';

function AddPetForm({ onSuccess }) {
  const [formData, setFormData] = useState({
    name: '',
    species: '',
    age: '',
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      await api.post('/pets', {
        ...formData,
        age: formData.age ? parseInt(formData.age) : null,
      });
      setFormData({ name: '', species: '', age: '' });
      onSuccess();
    } catch (err) {
      setError(err.response?.data?.message || '반려동물 등록에 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="bg-white rounded-lg shadow-md p-6 mb-6">
      <h2 className="text-2xl font-bold text-gray-800 mb-4">🐶 반려동물 등록</h2>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">이름</label>
          <input
            type="text"
            required
            value={formData.name}
            onChange={(e) => setFormData({ ...formData, name: e.target.value })}
            className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
            placeholder="예: 뭉치"
          />
        </div>
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">종류</label>
          <input
            type="text"
            required
            value={formData.species}
            onChange={(e) => setFormData({ ...formData, species: e.target.value })}
            className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
            placeholder="예: 골든 리트리버"
          />
        </div>
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">나이 (선택)</label>
          <input
            type="number"
            min="0"
            value={formData.age}
            onChange={(e) => setFormData({ ...formData, age: e.target.value })}
            className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
            placeholder="예: 3"
          />
        </div>
        {error && <p className="text-red-600 text-sm">{error}</p>}
        <button
          type="submit"
          disabled={loading}
          className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 disabled:bg-gray-400 transition-colors"
        >
          {loading ? '등록 중...' : '반려동물 등록'}
        </button>
      </form>
    </div>
  );
}

export default AddPetForm;
```

## PetList 컴포넌트

`src/components/PetList.jsx`:
```jsx
import { useState, useEffect } from 'react';
import api from '../api/axios';

function PetList({ onUpdate }) {
  const [pets, setPets] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

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

  const fetchPets = async () => {
    try {
      const response = await api.get('/pets');
      setPets(response.data);
    } catch (err) {
      setError('반려동물 목록을 불러오는데 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  const handleDelete = async (id) => {
    if (!confirm('정말 삭제하시겠습니까?')) return;

    try {
      await api.delete(`/pets/${id}`);
      onUpdate();
    } catch (err) {
      alert('삭제에 실패했습니다.');
    }
  };

  const getTotalDistance = (walks) => {
    return walks.reduce((sum, walk) => sum + parseFloat(walk.distance_km), 0).toFixed(2);
  };

  if (loading) return <div className="text-center py-8">로딩 중...</div>;
  if (error) return <div className="text-red-600 text-center py-8">{error}</div>;

  return (
    <div className="bg-white rounded-lg shadow-md p-6">
      <h2 className="text-2xl font-bold text-gray-800 mb-4">등록된 반려동물</h2>
      {pets.length === 0 ? (
        <p className="text-gray-500 text-center py-8">등록된 반려동물이 없습니다.</p>
      ) : (
        <div className="grid gap-4 md:grid-cols-2">
          {pets.map((pet) => (
            <div key={pet.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-lg transition-shadow">
              <div className="flex justify-between items-start mb-2">
                <h3 className="text-xl font-bold text-indigo-900">{pet.name}</h3>
                <button
                  onClick={() => handleDelete(pet.id)}
                  className="text-red-600 hover:text-red-800 text-sm"
                >
                  삭제
                </button>
              </div>
              <p className="text-gray-600 mb-1">종류: {pet.species}</p>
              {pet.age && <p className="text-gray-600 mb-2">나이: {pet.age}살</p>}
              <div className="mt-3 pt-3 border-t border-gray-200">
                <p className="text-sm text-gray-500">총 산책 횟수: {pet.walks?.length || 0}회</p>
                <p className="text-sm text-gray-500">총 산책 거리: {getTotalDistance(pet.walks || [])}km</p>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default PetList;
```

컴포넌트가 제대로 작동하는지 확인해주세요. 반려동물을 등록하고 목록에 표시되는지 테스트해주세요.

기대 결과: 반려동물 등록 폼과 목록이 작동함. 반려동물을 추가하면 실시간으로 목록에 표시되고, 삭제 기능도 작동함. 총 산책 통계가 표시됨.

5
Step 5
# 산책 기록 컴포넌트 구현

## AddWalkForm 컴포넌트

`src/components/AddWalkForm.jsx`:
```jsx
import { useState, useEffect } from 'react';
import api from '../api/axios';

function AddWalkForm({ onSuccess }) {
  const [pets, setPets] = useState([]);
  const [formData, setFormData] = useState({
    pet_id: '',
    walk_date: new Date().toISOString().split('T')[0],
    duration_minutes: '',
    distance_km: '',
    memo: '',
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

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

  const fetchPets = async () => {
    try {
      const response = await api.get('/pets');
      setPets(response.data);
    } catch (err) {
      console.error('반려동물 목록 조회 실패:', err);
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      await api.post('/walks', {
        ...formData,
        pet_id: parseInt(formData.pet_id),
        duration_minutes: parseInt(formData.duration_minutes),
        distance_km: parseFloat(formData.distance_km),
      });
      setFormData({
        pet_id: '',
        walk_date: new Date().toISOString().split('T')[0],
        duration_minutes: '',
        distance_km: '',
        memo: '',
      });
      onSuccess();
    } catch (err) {
      setError(err.response?.data?.message || '산책 기록 추가에 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  if (pets.length === 0) {
    return (
      <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 mb-6">
        <p className="text-yellow-800">⚠️ 먼저 반려동물을 등록해주세요.</p>
      </div>
    );
  }

  return (
    <div className="bg-white rounded-lg shadow-md p-6 mb-6">
      <h2 className="text-2xl font-bold text-gray-800 mb-4">🚶 산책 기록 추가</h2>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">반려동물 선택</label>
          <select
            required
            value={formData.pet_id}
            onChange={(e) => setFormData({ ...formData, pet_id: e.target.value })}
            className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
          >
            <option value="">선택하세요</option>
            {pets.map((pet) => (
              <option key={pet.id} value={pet.id}>
                {pet.name} ({pet.species})
              </option>
            ))}
          </select>
        </div>
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">산책 날짜</label>
          <input
            type="date"
            required
            value={formData.walk_date}
            onChange={(e) => setFormData({ ...formData, walk_date: e.target.value })}
            className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
          />
        </div>
        <div className="grid grid-cols-2 gap-4">
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">산책 시간 (분)</label>
            <input
              type="number"
              required
              min="1"
              value={formData.duration_minutes}
              onChange={(e) => setFormData({ ...formData, duration_minutes: e.target.value })}
              className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
              placeholder="예: 30"
            />
          </div>
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">산책 거리 (km)</label>
            <input
              type="number"
              required
              min="0"
              step="0.1"
              value={formData.distance_km}
              onChange={(e) => setFormData({ ...formData, distance_km: e.target.value })}
              className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
              placeholder="예: 2.5"
            />
          </div>
        </div>
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-1">메모 (선택)</label>
          <textarea
            value={formData.memo}
            onChange={(e) => setFormData({ ...formData, memo: e.target.value })}
            className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
            rows="3"
            placeholder="오늘 산책 메모를 남겨보세요..."
          />
        </div>
        {error && <p className="text-red-600 text-sm">{error}</p>}
        <button
          type="submit"
          disabled={loading}
          className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 disabled:bg-gray-400 transition-colors"
        >
          {loading ? '기록 중...' : '산책 기록 추가'}
        </button>
      </form>
    </div>
  );
}

export default AddWalkForm;
```

## WalkList 컴포넌트

`src/components/WalkList.jsx`:
```jsx
import { useState, useEffect } from 'react';
import api from '../api/axios';

function WalkList({ onUpdate }) {
  const [walks, setWalks] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

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

  const fetchWalks = async () => {
    try {
      const response = await api.get('/walks');
      setWalks(response.data);
    } catch (err) {
      setError('산책 기록을 불러오는데 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  const handleDelete = async (id) => {
    if (!confirm('이 산책 기록을 삭제하시겠습니까?')) return;

    try {
      await api.delete(`/walks/${id}`);
      onUpdate();
    } catch (err) {
      alert('삭제에 실패했습니다.');
    }
  };

  const formatDate = (dateString) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' });
  };

  if (loading) return <div className="text-center py-8">로딩 중...</div>;
  if (error) return <div className="text-red-600 text-center py-8">{error}</div>;

  return (
    <div className="bg-white rounded-lg shadow-md p-6">
      <h2 className="text-2xl font-bold text-gray-800 mb-4">산책 기록 목록</h2>
      {walks.length === 0 ? (
        <p className="text-gray-500 text-center py-8">아직 산책 기록이 없습니다.</p>
      ) : (
        <div className="space-y-4">
          {walks.map((walk) => (
            <div key={walk.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-lg transition-shadow">
              <div className="flex justify-between items-start mb-2">
                <div>
                  <h3 className="text-lg font-bold text-indigo-900">{walk.pet?.name}</h3>
                  <p className="text-sm text-gray-500">{walk.pet?.species}</p>
                </div>
                <button
                  onClick={() => handleDelete(walk.id)}
                  className="text-red-600 hover:text-red-800 text-sm"
                >
                  삭제
                </button>
              </div>
              <div className="grid grid-cols-3 gap-4 mb-3">
                <div>
                  <p className="text-xs text-gray-500">날짜</p>
                  <p className="font-semibold">{formatDate(walk.walk_date)}</p>
                </div>
                <div>
                  <p className="text-xs text-gray-500">시간</p>
                  <p className="font-semibold">{walk.duration_minutes}분</p>
                </div>
                <div>
                  <p className="text-xs text-gray-500">거리</p>
                  <p className="font-semibold">{walk.distance_km}km</p>
                </div>
              </div>
              {walk.memo && (
                <div className="mt-3 pt-3 border-t border-gray-200">
                  <p className="text-sm text-gray-700">{walk.memo}</p>
                </div>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default WalkList;
```

## 개발 서버 실행

프론트엔드 서버를 실행해주세요:
```bash
npm run dev
```

http://localhost:5173 에서 애플리케이션이 작동하는지 확인해주세요. 백엔드(http://localhost:8000)도 함께 실행되어야 합니다.

전체 기능을 테스트해주세요:
1. 반려동물 등록
2. 산책 기록 추가
3. 목록 조회 및 삭제
4. 통계 확인

기대 결과: 산책 기록 추가 폼과 목록이 완성됨. 반려동물을 선택하여 산책 기록을 추가하고, 날짜/시간/거리가 카드 형태로 표시됨. 전체 풀스택 애플리케이션이 작동함.

제출된 작품 (0)

로그인 후 제출

아직 제출된 작품이 없습니다. 첫 번째 도전자가 되어보세요!

댓글 (0)

로그인 후 댓글을 남길 수 있습니다.
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!