Ver3.5 라라벨 + 테일윈드로 NFT 대시보드 디자인 완전 정복하기 🎨🚀

라라벨 + 테일윈드로 NFT 대시보드 디자인 완전 정복하기 🎨🚀
Laravel & Tailwind CSS로 세련된 NFT 대시보드를 직접 만들어보자!
야, 드디어 왔구나! 😄 오늘은 진짜 핫한 조합으로 뭔가 멋진 걸 만들어볼 거야. 라라벨(Laravel)이라는 PHP 프레임워크랑 테일윈드 CSS(Tailwind CSS)를 합쳐서 NFT 대시보드를 만드는 거야. 블록체인이니 Web3니 하는 말이 낯설어도 괜찮아. 차근차근 설명해줄 테니까!
NFT 대시보드가 뭔지 잠깐 짚고 가자면, 내가 보유한 NFT 목록, 가격, 거래 내역, 지갑 연동 상태 같은 걸 한눈에 볼 수 있는 관리 화면이야. 마치 주식 앱의 포트폴리오 화면처럼! 이걸 라라벨 백엔드 + 테일윈드 프론트엔드로 직접 구현해보는 거지. 자, 시작해보자! 🚀
🧩 1. 왜 라라벨 + 테일윈드인가?
먼저 이 두 기술이 왜 NFT 대시보드에 딱 맞는 조합인지 알아보자.
🔥 라라벨(Laravel)이란?
라라벨은 PHP 기반의 웹 프레임워크야. 2011년에 Taylor Otwell이 만들었고, 지금은 PHP 생태계에서 압도적인 1위 프레임워크야. 특징을 보면:
🎨 테일윈드 CSS(Tailwind CSS)란?
테일윈드는 유틸리티 퍼스트(Utility-First) CSS 프레임워크야. Bootstrap처럼 미리 만들어진 컴포넌트를 쓰는 게 아니라, 작은 CSS 클래스들을 조합해서 디자인을 만드는 방식이야.
예를 들어 이런 식이야:
<div class="bg-purple-100 rounded-xl p-6 shadow-md hover:shadow-lg transition-all">
<h2 class="text-2xl font-bold text-purple-700">내 NFT 컬렉션</h2>
</div>
CSS 파일 따로 안 만들어도 HTML 안에서 바로 스타일링이 돼! 빠르고 일관성 있는 디자인을 만들 수 있어서 NFT 대시보드처럼 카드가 많은 UI에 딱이야. 🃏
🤝 둘의 시너지
라라벨의 Blade 템플릿과 테일윈드의 유틸리티 클래스가 만나면 진짜 강력해. Blade 컴포넌트로 NFT 카드를 재사용 가능한 컴포넌트로 만들고, 테일윈드로 빠르게 스타일링하면 개발 속도가 엄청 빨라져. 실제로 많은 Web3 스타트업들이 이 조합을 쓰고 있어!
⚙️ 2. 개발 환경 세팅하기
자, 이제 본격적으로 시작해보자. 먼저 개발 환경을 세팅해야 해. 필요한 것들을 체크해볼게! ✅
📋 필요한 것들
| 도구 | 버전 | 역할 |
|---|---|---|
| PHP | 8.2 이상 | 라라벨 실행 환경 |
| Composer | 최신 | PHP 패키지 매니저 |
| Node.js | 18 이상 | 테일윈드 빌드 도구 |
| npm / yarn | 최신 | JS 패키지 매니저 |
| MySQL / SQLite | - | NFT 데이터 저장 |
🚀 라라벨 프로젝트 생성
# Composer로 라라벨 프로젝트 생성
composer create-project laravel/laravel nft-dashboard
# 프로젝트 폴더로 이동
cd nft-dashboard
# 개발 서버 실행
php artisan serve
이렇게 하면 http://localhost:8000에서 라라벨 기본 화면이 뜰 거야! 🎉
🎨 테일윈드 CSS 설치
# npm으로 테일윈드 설치
npm install -D tailwindcss postcss autoprefixer
# 테일윈드 설정 파일 생성
npx tailwindcss init -p
그러면 tailwind.config.js 파일이 생겨. 이 파일을 열어서 라라벨 Blade 파일들을 감지하도록 설정해줘야 해:
// tailwind.config.js
module.exports = {
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
],
theme: {
extend: {
colors: {
// NFT 대시보드 커스텀 색상
'nft-purple': '#6c4fc4',
'nft-blue': '#4a7fc4',
'nft-pink': '#c44f8a',
},
fontFamily: {
'sans': ['Inter', 'Noto Sans KR', 'sans-serif'],
}
},
},
plugins: [],
}
그 다음 resources/css/app.css 파일에 테일윈드 디렉티브를 추가해:
/* resources/css/app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
npm run dev로 개발 서버를 켜면 핫 리로드가 지원돼서 스타일 변경이 즉시 반영돼!
# Vite 개발 서버 실행 (별도 터미널에서)
npm run dev
# 프로덕션 빌드
npm run build
🗂️ 3. 프로젝트 구조 설계하기
NFT 대시보드를 만들기 전에 어떤 기능이 필요한지 먼저 설계해보자. 좋은 설계가 좋은 코드를 만들어! 📐
📌 주요 기능 목록
📁 라라벨 디렉토리 구조
nft-dashboard/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── DashboardController.php # 메인 대시보드
│ │ │ ├── NftController.php # NFT 목록/상세
│ │ │ ├── WalletController.php # 지갑 연동
│ │ │ └── TransactionController.php # 거래 내역
│ │ └── Middleware/
│ ├── Models/
│ │ ├── Nft.php # NFT 모델
│ │ ├── Wallet.php # 지갑 모델
│ │ └── Transaction.php # 거래 모델
│ └── Services/
│ └── AlchemyService.php # 블록체인 API 서비스
├── resources/
│ ├── views/
│ │ ├── layouts/
│ │ │ └── app.blade.php # 기본 레이아웃
│ │ ├── components/
│ │ │ ├── nft-card.blade.php # NFT 카드 컴포넌트
│ │ │ ├── stat-card.blade.php # 통계 카드
│ │ │ └── sidebar.blade.php # 사이드바
│ │ ├── dashboard/
│ │ │ └── index.blade.php # 대시보드 메인
│ │ └── nfts/
│ │ ├── index.blade.php # NFT 목록
│ │ └── show.blade.php # NFT 상세
│ └── css/
│ └── app.css
├── routes/
│ └── web.php # 라우트 정의
└── database/
└── migrations/ # DB 마이그레이션
🗄️ 데이터베이스 마이그레이션
NFT 데이터를 저장할 테이블을 만들어보자:
# 마이그레이션 파일 생성
php artisan make:migration create_nfts_table
php artisan make:migration create_wallets_table
php artisan make:migration create_transactions_table
// database/migrations/xxxx_create_nfts_table.php
public function up(): void
{
Schema::create('nfts', function (Blueprint $table) {
$table->id();
$table->foreignId('wallet_id')->constrained()->onDelete('cascade');
$table->string('token_id'); // NFT 토큰 ID
$table->string('contract_address'); // 컨트랙트 주소
$table->string('name'); // NFT 이름
$table->text('description')->nullable();
$table->string('image_url')->nullable();
$table->string('collection_name')->nullable();
$table->decimal('floor_price', 18, 8)->default(0); // ETH 단위
$table->decimal('last_sale_price', 18, 8)->nullable();
$table->string('blockchain')->default('ethereum');
$table->json('traits')->nullable(); // NFT 속성 (희귀도 등)
$table->timestamps();
});
}
// database/migrations/xxxx_create_wallets_table.php
public function up(): void
{
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('address')->unique(); // 지갑 주소 (0x...)
$table->string('label')->nullable(); // 지갑 별명
$table->decimal('eth_balance', 18, 8)->default(0);
$table->boolean('is_primary')->default(false);
$table->timestamps();
});
}
# 마이그레이션 실행
php artisan migrate
🏗️ 4. 레이아웃 & 사이드바 만들기
이제 실제 UI를 만들어보자! 먼저 모든 페이지에서 공통으로 쓸 기본 레이아웃을 만들어. 🎨
📄 기본 레이아웃 (app.blade.php)
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFT 대시보드 - @yield('title', '홈')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50 font-sans">
<div class="flex h-screen overflow-hidden">
<!-- 사이드바 -->
@include('components.sidebar')
<!-- 메인 콘텐츠 영역 -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 상단 헤더 -->
@include('components.header')
<!-- 페이지 콘텐츠 -->
<main class="flex-1 overflow-y-auto p-6">
@yield('content')
</main>
</div>
</div>
</body>
</html>
🗂️ 사이드바 컴포넌트 (sidebar.blade.php)
<!-- resources/views/components/sidebar.blade.php -->
<aside class="w-64 bg-white shadow-lg flex flex-col">
<!-- 로고 영역 -->
<div class="p-6 border-b border-gray-100">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-purple-600 rounded-xl flex items-center justify-center">
<span class="text-white text-xl">💎</span>
</div>
<div>
<h1 class="text-lg font-bold text-gray-800">NFT Hub</h1>
<p class="text-xs text-gray-400">내 NFT 관리 센터</p>
</div>
</div>
</div>
<!-- 네비게이션 메뉴 -->
<nav class="flex-1 p-4 space-y-1">
<!-- 메뉴 아이템 -->
<a href="{{ route('dashboard') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl
{{ request()->routeIs('dashboard')
? 'bg-purple-100 text-purple-700 font-semibold'
: 'text-gray-600 hover:bg-gray-100' }}
transition-all duration-200">
<span class="text-lg">📊</span>
<span>대시보드</span>
</a>
<a href="{{ route('nfts.index') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl
{{ request()->routeIs('nfts.*')
? 'bg-purple-100 text-purple-700 font-semibold'
: 'text-gray-600 hover:bg-gray-100' }}
transition-all duration-200">
<span class="text-lg">🖼️</span>
<span>내 NFT</span>
</a>
<a href="{{ route('transactions.index') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl
{{ request()->routeIs('transactions.*')
? 'bg-purple-100 text-purple-700 font-semibold'
: 'text-gray-600 hover:bg-gray-100' }}
transition-all duration-200">
<span class="text-lg">📋</span>
<span>거래 내역</span>
</a>
<a href="{{ route('wallet.index') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl
{{ request()->routeIs('wallet.*')
? 'bg-purple-100 text-purple-700 font-semibold'
: 'text-gray-600 hover:bg-gray-100' }}
transition-all duration-200">
<span class="text-lg">👛</span>
<span>지갑 관리</span>
</a>
</nav>
<!-- 하단 지갑 정보 -->
<div class="p-4 border-t border-gray-100">
<div class="bg-purple-50 rounded-xl p-3">
<p class="text-xs text-gray-500 mb-1">연결된 지갑</p>
<p class="text-sm font-mono text-purple-700 truncate">
{{ auth()->user()->primaryWallet?->address ?? '지갑 미연결' }}
</p>
</div>
</div>
</aside>
request()->routeIs('dashboard')를 활용하면 현재 페이지에 따라 메뉴 활성화 스타일을 동적으로 적용할 수 있어. 삼항 연산자로 깔끔하게 처리하는 게 포인트!
📊 5. 대시보드 메인 페이지 만들기
이제 핵심인 대시보드 메인 페이지를 만들어보자! 통계 카드, NFT 갤러리 미리보기, 최근 거래 내역을 담을 거야. 💪
🎯 DashboardController 만들기
# 컨트롤러 생성
php artisan make:controller DashboardController
<?php
// app/Http/Controllers/DashboardController.php
namespace App\Http\Controllers;
use App\Models\Nft;
use App\Models\Transaction;
use App\Services\AlchemyService;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function __construct(
private AlchemyService $alchemyService
) {}
public function index()
{
$user = auth()->user();
$wallet = $user->primaryWallet;
// 통계 데이터 수집
$stats = [
'total_nfts' => $user->nfts()->count(),
'total_value' => $user->nfts()->sum('floor_price'),
'monthly_gain' => $this->calculateMonthlyGain($user),
'collections' => $user->nfts()->distinct('collection_name')->count(),
];
// 최근 NFT 4개 (대시보드 미리보기용)
$recentNfts = $user->nfts()
->orderBy('created_at', 'desc')
->take(4)
->get();
// 최근 거래 5건
$recentTransactions = $user->transactions()
->orderBy('created_at', 'desc')
->take(5)
->get();
return view('dashboard.index', compact(
'stats', 'recentNfts', 'recentTransactions', 'wallet'
));
}
private function calculateMonthlyGain($user): float
{
$thisMonth = $user->transactions()
->whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year)
->where('type', 'sale')
->sum('price');
$lastMonth = $user->transactions()
->whereMonth('created_at', now()->subMonth()->month)
->where('type', 'sale')
->sum('price');
return $thisMonth - $lastMonth;
}
}
🎨 대시보드 뷰 (dashboard/index.blade.php)
<!-- resources/views/dashboard/index.blade.php -->
@extends('layouts.app')
@section('title', '대시보드')
@section('content')
<!-- 페이지 헤더 -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-800">
안녕하세요, {{ auth()->user()->name }}님! 👋
</h1>
<p class="text-gray-500 mt-1">오늘도 NFT 포트폴리오를 확인해보세요.</p>
</div>
<!-- 통계 카드 그리드 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- 총 NFT 수 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100
hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
<span class="text-2xl">🖼️</span>
</div>
<span class="text-xs text-green-500 bg-green-50 px-2 py-1 rounded-full font-medium">
+3 이번달
</span>
</div>
<p class="text-sm text-gray-500 mb-1">총 보유 NFT</p>
<p class="text-3xl font-bold text-gray-800">{{ $stats['total_nfts'] }}</p>
</div>
<!-- 총 가치 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100
hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
<span class="text-2xl">💎</span>
</div>
<span class="text-xs text-blue-500 bg-blue-50 px-2 py-1 rounded-full font-medium">
ETH
</span>
</div>
<p class="text-sm text-gray-500 mb-1">총 포트폴리오 가치</p>
<p class="text-3xl font-bold text-gray-800">
{{ number_format($stats['total_value'], 2) }}
</p>
</div>
<!-- 이번달 수익 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100
hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-pink-100 rounded-xl flex items-center justify-center">
<span class="text-2xl">📈</span>
</div>
<span class="text-xs {{ $stats['monthly_gain'] >= 0 ? 'text-green-500 bg-green-50' : 'text-red-500 bg-red-50' }}
px-2 py-1 rounded-full font-medium">
{{ $stats['monthly_gain'] >= 0 ? '수익' : '손실' }}
</span>
</div>
<p class="text-sm text-gray-500 mb-1">이번달 손익</p>
<p class="text-3xl font-bold {{ $stats['monthly_gain'] >= 0 ? 'text-green-600' : 'text-red-600' }}">
{{ $stats['monthly_gain'] >= 0 ? '+' : '' }}{{ number_format($stats['monthly_gain'], 2) }}
</p>
</div>
<!-- 컬렉션 수 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100
hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-yellow-100 rounded-xl flex items-center justify-center">
<span class="text-2xl">🗂️</span>
</div>
</div>
<p class="text-sm text-gray-500 mb-1">보유 컬렉션</p>
<p class="text-3xl font-bold text-gray-800">{{ $stats['collections'] }}</p>
</div>
</div>
<!-- 최근 NFT 미리보기 -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 mb-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-800">최근 추가된 NFT</h2>
<a href="{{ route('nfts.index') }}"
class="text-purple-600 text-sm font-medium hover:text-purple-800 transition-colors">
전체보기 →
</a>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@forelse($recentNfts as $nft)
@include('components.nft-card', ['nft' => $nft, 'compact' => true])
@empty
<div class="col-span-4 text-center py-12 text-gray-400">
<span class="text-4xl block mb-3">🖼️</span>
<p>아직 NFT가 없어요. 지갑을 연동해보세요!</p>
</div>
@endforelse
</div>
</div>
@endsection
🃏 6. NFT 카드 컴포넌트 만들기
NFT 대시보드의 꽃은 역시 NFT 카드야! 이미지, 이름, 가격, 컬렉션 정보를 예쁘게 보여주는 카드 컴포넌트를 만들어보자. 🌸
🎴 NFT 카드 컴포넌트
<!-- resources/views/components/nft-card.blade.php -->
@props(['nft', 'compact' => false])
<div class="group bg-white rounded-2xl overflow-hidden border border-gray-100
hover:border-purple-200 hover:shadow-lg transition-all duration-300 cursor-pointer">
<!-- NFT 이미지 영역 -->
<div class="relative overflow-hidden {{ $compact ? 'h-36' : 'h-56' }} bg-gradient-to-br from-purple-100 to-blue-100">
@if($nft->image_url)
<img src="{{ $nft->image_url }}"
alt="{{ $nft->name }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy">
@else
<!-- 이미지 없을 때 플레이스홀더 -->
<div class="w-full h-full flex items-center justify-center">
<span class="text-6xl">🖼️</span>
</div>
@endif
<!-- 블록체인 배지 -->
<div class="absolute top-2 left-2">
<span class="bg-black/60 text-white text-xs px-2 py-1 rounded-full backdrop-blur-sm">
{{ strtoupper($nft->blockchain) }}
</span>
</div>
<!-- 즐겨찾기 버튼 -->
<button class="absolute top-2 right-2 w-8 h-8 bg-white/80 rounded-full
flex items-center justify-center backdrop-blur-sm
opacity-0 group-hover:opacity-100 transition-opacity">
❤️
</button>
</div>
<!-- NFT 정보 영역 -->
<div class="p-4">
<!-- 컬렉션 이름 -->
@if($nft->collection_name)
<p class="text-xs text-purple-500 font-medium mb-1 truncate">
{{ $nft->collection_name }}
</p>
@endif
<!-- NFT 이름 -->
<h3 class="font-bold text-gray-800 truncate {{ $compact ? 'text-sm' : 'text-base' }}">
{{ $nft->name }}
</h3>
@unless($compact)
<!-- 토큰 ID -->
<p class="text-xs text-gray-400 mt-1 font-mono">
#{{ $nft->token_id }}
</p>
@endunless
<!-- 가격 정보 -->
<div class="flex items-center justify-between mt-3">
<div>
<p class="text-xs text-gray-400">플로어 가격</p>
<p class="font-bold text-gray-800 {{ $compact ? 'text-sm' : 'text-base' }}">
Ξ {{ number_format($nft->floor_price, 3) }}
</p>
</div>
@unless($compact)
<a href="{{ route('nfts.show', $nft) }}"
class="bg-purple-600 text-white text-xs px-4 py-2 rounded-xl
hover:bg-purple-700 transition-colors font-medium">
상세보기
</a>
@endunless
</div>
</div>
</div>
@props(['nft', 'compact' => false])를 사용하면 컴포넌트에 기본값이 있는 속성을 정의할 수 있어. compact 모드로 대시보드 미리보기용 작은 카드와 전체 목록용 큰 카드를 하나의 컴포넌트로 처리할 수 있지!
🔍 NFT 목록 페이지 (필터 + 검색)
<!-- resources/views/nfts/index.blade.php -->
@extends('layouts.app')
@section('title', '내 NFT')
@section('content')
<!-- 검색 & 필터 바 -->
<div class="bg-white rounded-2xl p-4 shadow-sm border border-gray-100 mb-6">
<form method="GET" action="{{ route('nfts.index') }}"
class="flex flex-wrap gap-3 items-center">
<!-- 검색 인풋 -->
<div class="flex-1 min-w-48 relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">🔍</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="NFT 이름 검색..."
class="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-xl
focus:outline-none focus:ring-2 focus:ring-purple-300 text-sm">
</div>
<!-- 컬렉션 필터 -->
<select name="collection"
class="px-4 py-2.5 border border-gray-200 rounded-xl text-sm
focus:outline-none focus:ring-2 focus:ring-purple-300">
<option value="">모든 컬렉션</option>
@foreach($collections as $collection)
<option value="{{ $collection }}"
{{ request('collection') == $collection ? 'selected' : '' }}>
{{ $collection }}
</option>
@endforeach
</select>
<!-- 정렬 -->
<select name="sort"
class="px-4 py-2.5 border border-gray-200 rounded-xl text-sm
focus:outline-none focus:ring-2 focus:ring-purple-300">
<option value="newest">최신순</option>
<option value="price_high">가격 높은순</option>
<option value="price_low">가격 낮은순</option>
</select>
<button type="submit"
class="bg-purple-600 text-white px-6 py-2.5 rounded-xl text-sm
font-medium hover:bg-purple-700 transition-colors">
검색
</button>
</form>
</div>
<!-- NFT 그리드 -->
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
@forelse($nfts as $nft)
<x-nft-card :nft="$nft" />
@empty
<div class="col-span-4 text-center py-20 text-gray-400">
<span class="text-6xl block mb-4">🔍</span>
<p class="text-lg font-medium">검색 결과가 없어요</p>
<p class="text-sm mt-2">다른 키워드로 검색해보세요!</p>
</div>
@endforelse
</div>
<!-- 페이지네이션 -->
<div class="mt-8">
{{ $nfts->withQueryString()->links() }}
</div>
@endsection
🔗 7. 블록체인 API 연동하기 (Alchemy)
이제 진짜 블록체인 데이터를 가져와보자! Alchemy는 이더리움 블록체인 데이터를 쉽게 가져올 수 있는 API 서비스야. NFT 데이터 조회에 최적화된 API를 제공해줘. 🌐
🔑 Alchemy API 키 발급: alchemy.com에서 무료 계정을 만들고 API 키를 발급받아. 무료 플랜으로도 NFT 대시보드 개발에 충분해!
⚙️ AlchemyService 클래스
<?php
// app/Services/AlchemyService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class AlchemyService
{
private string $baseUrl;
private string $apiKey;
public function __construct()
{
$this->apiKey = config('services.alchemy.key');
$this->baseUrl = "https://eth-mainnet.g.alchemy.com/nft/v3/{$this->apiKey}";
}
/**
* 지갑 주소로 NFT 목록 가져오기
*/
public function getNftsForOwner(string $walletAddress, int $pageSize = 20): array
{
// 캐시 활용 (5분간 캐시)
$cacheKey = "nfts_{$walletAddress}_{$pageSize}";
return Cache::remember($cacheKey, 300, function () use ($walletAddress, $pageSize) {
$response = Http::get("{$this->baseUrl}/getNFTsForOwner", [
'owner' => $walletAddress,
'pageSize' => $pageSize,
'withMetadata' => true,
]);
if ($response->failed()) {
throw new \Exception('Alchemy API 호출 실패: ' . $response->status());
}
return $response->json();
});
}
/**
* 특정 NFT 메타데이터 가져오기
*/
public function getNftMetadata(string $contractAddress, string $tokenId): array
{
$response = Http::get("{$this->baseUrl}/getNFTMetadata", [
'contractAddress' => $contractAddress,
'tokenId' => $tokenId,
]);
return $response->json();
}
/**
* 컬렉션 플로어 가격 가져오기
*/
public function getFloorPrice(string $contractAddress): float
{
$cacheKey = "floor_price_{$contractAddress}";
return Cache::remember($cacheKey, 600, function () use ($contractAddress) {
$response = Http::get("{$this->baseUrl}/getFloorPrice", [
'contractAddress' => $contractAddress,
]);
$data = $response->json();
return $data['openSea']['floorPrice'] ?? 0.0;
});
}
/**
* API 응답을 NFT 모델 형식으로 변환
*/
public function transformNftData(array $rawNft): array
{
return [
'token_id' => $rawNft['tokenId'] ?? '',
'contract_address' => $rawNft['contract']['address'] ?? '',
'name' => $rawNft['name'] ?? 'Unknown NFT',
'description' => $rawNft['description'] ?? null,
'image_url' => $rawNft['image']['cachedUrl'] ?? null,
'collection_name' => $rawNft['contract']['name'] ?? null,
'traits' => json_encode($rawNft['raw']['metadata']['attributes'] ?? []),
'blockchain' => 'ethereum',
];
}
}
🔧 환경 설정 (.env)
# .env 파일에 추가
ALCHEMY_API_KEY=your_alchemy_api_key_here
# config/services.php에 추가
'alchemy' => [
'key' => env('ALCHEMY_API_KEY'),
],
🔄 지갑 연동 & NFT 동기화
<?php
// app/Http/Controllers/WalletController.php
namespace App\Http\Controllers;
use App\Models\Nft;
use App\Models\Wallet;
use App\Services\AlchemyService;
use Illuminate\Http\Request;
class WalletController extends Controller
{
public function __construct(
private AlchemyService $alchemyService
) {}
/**
* 지갑 주소 연동
*/
public function connect(Request $request)
{
$request->validate([
'address' => ['required', 'string', 'regex:/^0x[a-fA-F0-9]{40}$/'],
'label' => ['nullable', 'string', 'max:50'],
]);
$wallet = auth()->user()->wallets()->updateOrCreate(
['address' => strtolower($request->address)],
[
'label' => $request->label ?? '내 지갑',
'is_primary' => !auth()->user()->wallets()->exists(),
]
);
// NFT 동기화 작업 큐에 추가
dispatch(new \App\Jobs\SyncWalletNfts($wallet));
return redirect()->route('dashboard')
->with('success', '지갑이 연동되었어요! NFT를 불러오는 중이에요 🎉');
}
/**
* NFT 수동 새로고침
*/
public function syncNfts(Wallet $wallet)
{
$this->authorize('update', $wallet);
$rawNfts = $this->alchemyService->getNftsForOwner($wallet->address);
foreach ($rawNfts['ownedNfts'] as $rawNft) {
$nftData = $this->alchemyService->transformNftData($rawNft);
$floorPrice = $this->alchemyService->getFloorPrice(
$nftData['contract_address']
);
$wallet->nfts()->updateOrCreate(
[
'token_id' => $nftData['token_id'],
'contract_address' => $nftData['contract_address'],
],
array_merge($nftData, ['floor_price' => $floorPrice])
);
}
return back()->with('success', 'NFT 목록이 업데이트되었어요! ✨');
}
}
dispatch(new SyncWalletNfts($wallet))처럼 라라벨 큐(Queue)를 활용하면 백그라운드에서 처리할 수 있어. 사용자는 기다리지 않아도 돼! 라라벨의 큐 시스템은 Redis나 데이터베이스를 드라이버로 쓸 수 있어.
✨ 8. 테일윈드로 고급 UI 효과 추가하기
기본 기능은 다 만들었으니, 이제 테일윈드의 고급 기능으로 대시보드를 더 세련되게 만들어보자! 🎨
🌈 그라디언트 배너 카드
<!-- 그라디언트 배너 - 포트폴리오 요약 -->
<div class="relative overflow-hidden bg-gradient-to-r from-purple-600 via-purple-500 to-blue-500
rounded-2xl p-8 text-white mb-6">
<!-- 배경 장식 원 -->
<div class="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"></div>
<div class="absolute -bottom-6 -left-6 w-24 h-24 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-purple-200 text-sm font-medium mb-2">총 포트폴리오 가치</p>
<h2 class="text-4xl font-bold mb-1">Ξ {{ number_format($stats['total_value'], 4) }}</h2>
<p class="text-purple-200 text-sm">
≈ ${{ number_format($stats['total_value'] * 3500, 0) }} USD
</p>
<div class="flex gap-6 mt-6">
<div>
<p class="text-purple-200 text-xs">보유 NFT</p>
<p class="text-2xl font-bold">{{ $stats['total_nfts'] }}</p>
</div>
<div>
<p class="text-purple-200 text-xs">컬렉션</p>
<p class="text-2xl font-bold">{{ $stats['collections'] }}</p>
</div>
<div>
<p class="text-purple-200 text-xs">이번달 수익</p>
<p class="text-2xl font-bold">
{{ $stats['monthly_gain'] >= 0 ? '+' : '' }}Ξ {{ number_format($stats['monthly_gain'], 3) }}
</p>
</div>
</div>
</div>
</div>
⚡ 거래 내역 테이블
<!-- 거래 내역 테이블 -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-6 border-b border-gray-100">
<h2 class="text-xl font-bold text-gray-800">최근 거래 내역</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">NFT</th>
<th class="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">유형</th>
<th class="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">가격</th>
<th class="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">날짜</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@foreach($recentTransactions as $tx)
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-purple-100 overflow-hidden flex-shrink-0">
@if($tx->nft?->image_url)
<img src="{{ $tx->nft->image_url }}" class="w-full h-full object-cover">
@else
<div class="w-full h-full flex items-center justify-center">🖼️</div>
@endif
</div>
<div>
<p class="font-medium text-gray-800 text-sm">{{ $tx->nft?->name ?? 'Unknown' }}</p>
<p class="text-xs text-gray-400 font-mono">#{{ $tx->nft?->token_id }}</p>
</div>
</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 rounded-full text-xs font-medium
{{ $tx->type === 'buy' ? 'bg-blue-100 text-blue-700' : '' }}
{{ $tx->type === 'sale' ? 'bg-green-100 text-green-700' : '' }}
{{ $tx->type === 'transfer' ? 'bg-gray-100 text-gray-700' : '' }}">
{{ match($tx->type) {
'buy' => '구매',
'sale' => '판매',
'transfer' => '전송',
default => $tx->type
} }}
</span>
</td>
<td class="px-6 py-4 font-mono font-medium text-gray-800">
Ξ {{ number_format($tx->price, 4) }}
</td>
<td class="px-6 py-4 text-sm text-gray-500">
{{ $tx->created_at->diffForHumans() }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
🎭 다크 모드 지원
테일윈드는 다크 모드를 기본 지원해! tailwind.config.js에서 설정하고 클래스를 추가하면 돼:
// tailwind.config.js - 다크 모드 설정
module.exports = {
darkMode: 'class', // 'class' 또는 'media'
// ...
}
// HTML에서 사용 예시
<div class="bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100
rounded-2xl p-6 shadow-sm">
<h2 class="text-xl font-bold">NFT 컬렉션</h2>
</div>
// 다크 모드 토글 버튼 (Alpine.js 활용)
<button @click="$store.theme.toggle()"
class="p-2 rounded-xl bg-gray-100 dark:bg-gray-700
hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
<span x-show="$store.theme.dark">☀️</span>
<span x-show="!$store.theme.dark">🌙</span>
</button>
📱 9. 모바일 반응형 최적화
NFT 대시보드는 모바일에서도 잘 보여야 해! 테일윈드의 반응형 프리픽스를 활용해서 모바일 최적화를 해보자. 📱
📐 테일윈드 반응형 브레이크포인트
| 프리픽스 | 최소 너비 | 대상 기기 |
|---|---|---|
sm: |
640px | 큰 모바일 / 소형 태블릿 |
md: |
768px | 태블릿 |
lg: |
1024px | 노트북 |
xl: |
1280px | 데스크탑 |
2xl: |
1536px | 대형 모니터 |
📲 모바일 사이드바 처리
<!-- 모바일에서는 사이드바를 숨기고 햄버거 메뉴로 대체 -->
<!-- 모바일 헤더 -->
<header class="lg:hidden bg-white border-b border-gray-100 px-4 py-3
flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-2xl">💎</span>
<span class="font-bold text-gray-800">NFT Hub</span>
</div>
<!-- 햄버거 버튼 (Alpine.js) -->
<button @click="sidebarOpen = !sidebarOpen"
class="p-2 rounded-xl bg-gray-100 hover:bg-gray-200 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</header>
<!-- 모바일 오버레이 사이드바 -->
<div x-show="sidebarOpen"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
class="lg:hidden fixed inset-0 z-50 bg-black/50"
@click="sidebarOpen = false">
<div class="w-64 h-full bg-white shadow-xl" @click.stop>
@include('components.sidebar')
</div>
</div>
<!-- 데스크탑 사이드바 -->
<aside class="hidden lg:flex w-64 ...">
@include('components.sidebar')
</aside>
📊 반응형 그리드 패턴
<!-- NFT 그리드 - 모바일 1열, 태블릿 2열, 데스크탑 3-4열 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
@foreach($nfts as $nft)
<x-nft-card :nft="$nft" />
@endforeach
</div>
<!-- 통계 카드 - 모바일 2열, 데스크탑 4열 -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<!-- 통계 카드들 -->
</div>
🎯 모바일 UX 팁: NFT 이미지는 모바일에서 터치하면 상세 페이지로 이동하도록 전체 카드를 링크로 감싸는 게 좋아. 작은 버튼보다 큰 터치 영역이 모바일 UX에 훨씬 유리해!
🚀 10. 라우트 설정 & 인증
대시보드는 로그인한 사용자만 볼 수 있어야 해. 라라벨의 인증 미들웨어와 라우트 설정을 해보자! 🔐
🗺️ 라우트 정의 (web.php)
<?php
// routes/web.php
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\NftController;
use App\Http\Controllers\WalletController;
use App\Http\Controllers\TransactionController;
// 인증 필요 라우트 그룹
Route::middleware(['auth', 'verified'])->group(function () {
// 대시보드
Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard');
// NFT 관련
Route::resource('nfts', NftController::class)->only(['index', 'show']);
// 지갑 관련
Route::prefix('wallet')->name('wallet.')->group(function () {
Route::get('/', [WalletController::class, 'index'])->name('index');
Route::post('/connect', [WalletController::class, 'connect'])->name('connect');
Route::post('/{wallet}/sync', [WalletController::class, 'syncNfts'])->name('sync');
Route::delete('/{wallet}', [WalletController::class, 'destroy'])->name('destroy');
});
// 거래 내역
Route::get('/transactions', [TransactionController::class, 'index'])
->name('transactions.index');
});
// 홈 리다이렉트
Route::get('/', function () {
return auth()->check()
? redirect()->route('dashboard')
: redirect()->route('login');
});
// 라라벨 Breeze/Jetstream 인증 라우트
require __DIR__.'/auth.php';
🔐 라라벨 Breeze로 인증 설치
# 라라벨 Breeze 설치 (가장 간단한 인증 스타터킷)
composer require laravel/breeze --dev
# Breeze 설치 (Blade + Tailwind 선택)
php artisan breeze:install blade
# 의존성 설치 및 빌드
npm install && npm run build
# 마이그레이션 실행
php artisan migrate
Breeze를 설치하면 로그인, 회원가입, 비밀번호 재설정 페이지가 자동으로 생성돼. 그리고 이미 테일윈드로 스타일링되어 있어서 우리 대시보드 디자인과 자연스럽게 어울려! 🎉
🎯 11. 성능 최적화 & 배포 준비
대시보드가 완성됐으면 이제 성능 최적화와 배포를 준비해보자! 🚀
⚡ 라라벨 성능 최적화
# 설정 캐시
php artisan config:cache
# 라우트 캐시
php artisan route:cache
# 뷰 캐시
php artisan view:cache
# 이벤트 캐시
php artisan event:cache
# 모든 캐시 한번에
php artisan optimize
🎨 테일윈드 프로덕션 빌드
# 프로덕션 빌드 (CSS 최소화 + 사용하지 않는 클래스 제거)
npm run build
# 결과: public/build/ 폴더에 최적화된 파일 생성
# 테일윈드 PurgeCSS로 사용하지 않는 CSS 자동 제거
# 개발 시 수 MB → 프로덕션 수 KB로 줄어들어!
🗄️ N+1 쿼리 문제 해결
<?php
// ❌ N+1 문제 있는 코드
$nfts = Nft::all();
foreach ($nfts as $nft) {
echo $nft->wallet->address; // 각 NFT마다 쿼리 발생!
}
// ✅ Eager Loading으로 해결
$nfts = Nft::with(['wallet', 'transactions'])->get();
// 쿼리 2번으로 모든 데이터 로드!
// 컨트롤러에서 적용
public function index()
{
$nfts = auth()->user()
->nfts()
->with(['wallet'])
->when(request('search'), function ($query, $search) {
$query->where('name', 'like', "%{$search}%");
})
->when(request('collection'), function ($query, $collection) {
$query->where('collection_name', $collection);
})
->orderBy(
request('sort') === 'price_high' ? 'floor_price' :
(request('sort') === 'price_low' ? 'floor_price' : 'created_at'),
request('sort') === 'price_low' ? 'asc' : 'desc'
)
->paginate(12);
return view('nfts.index', compact('nfts'));
}
☁️ 배포 체크리스트
APP_ENV=production, APP_DEBUG=false 확인php artisan migrate --forcephp artisan storage:linkphp artisan queue:work (Supervisor로 관리)🎓 12. 더 발전시키기 - 추가 기능 아이디어
기본 NFT 대시보드를 완성했으면, 이제 더 발전시켜볼 수 있는 아이디어들을 소개할게! 🌟
🔮 추가 기능 아이디어
📚 참고할 만한 라이브러리들
| 라이브러리 | 용도 | 특징 |
|---|---|---|
| Livewire | 실시간 UI 업데이트 | PHP로 Vue.js 같은 반응형 UI 구현 |
| Alpine.js | 경량 JS 인터랙션 | 모달, 드롭다운, 토글 등 |
| Ethers.js | 이더리움 연동 | 지갑 연결, 트랜잭션 서명 |
| Chart.js | 데이터 시각화 | 가격 차트, 포트폴리오 그래프 |
| Spatie Permission | 권한 관리 | 역할 기반 접근 제어 |
📝 오늘 배운 것 총정리
자, 오늘 엄청 많은 걸 배웠지? 😄 핵심만 다시 정리해볼게!
라라벨과 테일윈드는 Web3 세계에서도 충분히 강력한 도구야. 복잡한 블록체인 데이터를 아름다운 UI로 표현하는 건 생각보다 어렵지 않아. 오늘 배운 내용을 바탕으로 나만의 NFT 대시보드를 만들어봐! 🎨✨
궁금한 점이 있으면 언제든지 물어봐. 같이 Web3 세계를 정복해보자고! 🚀💎
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.