Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
11 KiB
11 KiB
Trang Hồ Sơ Công Khai Môi Giới — Hướng Dẫn Tham Khảo Nhanh
🎯 Tổng Quan Triển Khai
Cấu Trúc URL
/agents/[id] # Desktop
/agents/[id]?locale=vi # Với locale (i18n)
/en/agents/[id] # Locale tường minh
Vị Trí Trang
apps/web/app/[locale]/(public)/agents/[id]/page.tsx
📦 Cấu Hình Backend (Cần Thay Đổi API)
Endpoint Công Khai Mới
GET /agents/:agentId/profile
DTO Phản Hồi
{
id: string;
fullName: string;
avatarUrl: string | null;
licenseNumber: string | null;
agency: string | null;
qualityScore: number;
bio: string | null;
serviceAreas: string[];
isVerified: boolean;
totalListings: number;
activeListings: number;
avgReviewRating: number;
totalReviews: number;
phone?: string;
createdAt: string;
updatedAt: string;
}
Các File Query Handler Cần Tạo
apps/api/src/modules/agents/application/queries/get-agent-profile/
├── get-agent-profile.query.ts
└── get-agent-profile.handler.ts
apps/api/src/modules/agents/presentation/dto/
└── agent-public-profile.dto.ts
🎨 Kiến Trúc Frontend
Cấu Trúc Thư Mục Cần Tạo
apps/web/
├── app/[locale]/(public)/agents/
│ └── [id]/
│ ├── page.tsx ← Server component (metadata, ISR)
│ └── layout.tsx ← Layout dùng chung tùy chọn
│
├── components/agents/
│ ├── agent-detail-client.tsx ← Component tương tác chính
│ ├── agent-header.tsx ← Phần thông tin hồ sơ
│ ├── agent-listings-section.tsx ← Lưới danh sách bất động sản
│ └── agent-reviews-section.tsx ← Đánh giá với sao
│
└── lib/
├── agents-api.ts ← API client
└── agents-server.ts ← Fetch phía server (ISR)
🔄 Luồng Dữ Liệu
1. Trình duyệt yêu cầu: /agents/[id]
↓
2. Next.js tạo metadata (SEO)
- Title: "Agent Name — Real Estate Agent at GoodGo"
- Description: Bio + thống kê
- Image: Avatar
- Canonical URL
↓
3. Fetch dữ liệu hồ sơ môi giới phía server
- GET /api/v1/agents/:id/profile (ISR: revalidate 3600s)
↓
4. Render Server Component với metadata + JSON-LD
↓
5. Truyền dữ liệu sang Client Component để tương tác
- Fetch đánh giá song song
- Fetch danh sách bất động sản song song
↓
6. Client render:
- Header môi giới (avatar, tên, thống kê, huy hiệu)
- Phần đánh giá (xếp hạng sao, thẻ bình luận)
- Phần danh sách bất động sản (tái sử dụng component PropertyCard)
🎭 Kết Hợp Component
Server Component (page.tsx)
export async function generateMetadata({ params }): Promise<Metadata> {
// 1. Fetch môi giới dùng agents-server.ts
// 2. Xây dựng metadata SEO
// 3. Trả về title, description, OG, canonical
}
export default async function AgentProfilePage({ params }) {
// 1. Fetch hồ sơ môi giới (với ISR)
// 2. Tạo JSON-LD (schema LocalBusiness)
// 3. Render <JsonLd> cho dữ liệu có cấu trúc
// 4. Truyền agent sang client component
return <>
<JsonLd data={agentJsonLd} />
<AgentDetailClient agent={agent} />
</>
}
Client Component (agent-detail-client.tsx)
'use client';
export function AgentDetailClient({ agent }: Props) {
const [listings, setListings] = useState([]);
const [reviews, setReviews] = useState([]);
const [reviewStats, setReviewStats] = useState(null);
useEffect(() => {
// Fetch danh sách bất động sản đang hoạt động của môi giới
// Fetch đánh giá & thống kê
}, [agent.id]);
return <>
<AgentHeader agent={agent} />
<AgentReviewsSection reviews={reviews} stats={reviewStats} />
<AgentListingsSection listings={listings} />
</>
}
🔗 Các API Call Được Sử Dụng
Endpoint Công Khai Hiện Có (Không Cần Xác Thực)
# Lấy hồ sơ môi giới (MỚI - cần tạo)
GET /api/v1/agents/:agentId/profile
# Lấy danh sách bất động sản của môi giới (ĐÃ CÓ)
GET /api/v1/listings?agentId=:agentId&status=ACTIVE
# Lấy đánh giá môi giới (ĐÃ CÓ)
GET /api/v1/reviews?targetType=AGENT&targetId=:agentId&limit=20
# Lấy thống kê đánh giá môi giới (ĐÃ CÓ)
GET /api/v1/reviews/stats?targetType=AGENT&targetId=:agentId
🎨 Các Phần UI & Component Tái Sử Dụng
1. Phần Header Môi Giới
┌─────────────────────────────────┐
│ [Avatar] Tên │
│ Giấy phép: ABC123 │
│ Công ty: XYZ Agency │
│ ✓ Đã Xác Minh │
│ │
│ ⭐ 4.5 (120 đánh giá) │
│ 📍 Phục vụ: Quận 1, Quận 7, ...│
│ 🏠 45 tin đăng đang hoạt động │
└─────────────────────────────────┘
Components: Card, Badge, Image, Text
Styling: Tailwind (p-6, flex, gap-4)
2. Phần Đánh Giá
┌─────────────────────────────────┐
│ Đánh Giá Khách Hàng (120 tổng) │
├─────────────────────────────────┤
│ [Thẻ Đánh Giá] │
│ ⭐⭐⭐⭐⭐ John Doe │ 2 ngày trước│
│ "Môi giới tuyệt vời, rất chuyên nghiệp" │
├─────────────────────────────────┤
│ [Thẻ Đánh Giá] │
│ ⭐⭐⭐⭐ Jane Smith │ 1 tuần trước│
│ "Giao tiếp tốt" │
└─────────────────────────────────┘
Components: Card, Badge (xếp hạng), Avatar
Tái sử dụng: Kiểu padding/spacing của PropertyCard
3. Phần Danh Sách Bất Động Sản
┌─────────────────────────────────┐
│ Tin Đăng Đang Hoạt Động (45) │
├─────────────────────────────────┤
│ [PropertyCard] [PropertyCard] │
│ [PropertyCard] [PropertyCard] │
│ [PropertyCard] [PropertyCard] │
└─────────────────────────────────┘
Components: PropertyCard (tái sử dụng từ search/property-card.tsx)
Grid: grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4
🎯 Mẫu Copy-Paste
agents-api.ts
import { apiClient } from './api-client';
export interface AgentPublicProfile {
id: string;
fullName: string;
avatarUrl: string | null;
licenseNumber: string | null;
agency: string | null;
qualityScore: number;
bio: string | null;
serviceAreas: string[];
isVerified: boolean;
totalListings: number;
activeListings: number;
avgReviewRating: number;
totalReviews: number;
createdAt: string;
updatedAt: string;
}
export const agentsApi = {
getById: (id: string) =>
apiClient.get<AgentPublicProfile>(`/agents/${id}/profile`),
};
agents-server.ts
const API_BASE_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1';
export async function fetchAgentById(id: string) {
try {
const res = await fetch(`${API_BASE_URL}/agents/${id}/profile`, {
next: { revalidate: 3600 } // ISR: revalidate mỗi 1 giờ
});
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
🔍 SEO & Dữ Liệu Có Cấu Trúc
Gợi Ý Metadata
title: `${agent.fullName} — Real Estate Agent at GoodGo`
description: `${agent.bio}. Quality Score: ${agent.qualityScore}. ${agent.activeListings} active listings. ⭐ ${agent.avgReviewRating} (${agent.totalReviews} reviews)`
// OG Image: Dùng avatar hoặc ảnh thay thế
Schema JSON-LD
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Agent Full Name",
"description": "Bio",
"url": "https://goodgo.vn/en/agents/[id]",
"image": "avatarUrl",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4.5,
"reviewCount": 120
},
"areaServed": ["Quan 1", "Quan 7", "Thu Duc"],
"knowsAbout": "Real Estate"
}
🚀 Các Giai Đoạn Triển Khai
Giai Đoạn 1: Backend (1-2 giờ)
- Tạo GetAgentProfileQuery
- Tạo GetAgentProfileHandler
- Tạo AgentPublicProfileDto
- Thêm endpoint vào AgentsController
- Cập nhật interface AgentRepository
- Triển khai trong PrismaAgentRepository
- Kiểm tra với Postman/curl
Giai Đoạn 2: Cài Đặt Frontend (1 giờ)
- Tạo agents-api.ts
- Tạo agents-server.ts
- Tạo cấu trúc thư mục agents
- Tạo [id]/page.tsx (stub)
- Import kiểu từ agents-api.ts
Giai Đoạn 3: Các Component UI (2-3 giờ)
- Tạo component AgentHeader
- Tạo component AgentReviewsSection
- Tạo component AgentListingsSection
- Tạo các component RatingStars/ReviewCard
- Kết nối việc fetch dữ liệu
Giai Đoạn 4: SEO & Hoàn Thiện (1 giờ)
- Thêm generateMetadata()
- Tạo schema JSON-LD
- Kiểm tra xem trước OG
- Kiểm tra responsive trên di động
- Kiểm tra chế độ tối
Giai Đoạn 5: Kiểm Thử (1 giờ)
- Kiểm thử e2e thủ công
- Kiểm tra xử lý lỗi 404
- Xác minh việc revalidate ISR
- Kiểm tra phân trang (danh sách/đánh giá)
- Kiểm tra SEO (Lighthouse)
📊 Ví Dụ Cấu Trúc Phản Hồi
{
"id": "clu1x2y3z4a5b6c7d8e9f0",
"fullName": "Nguyễn Văn A",
"avatarUrl": "https://cdn.goodgo.vn/avatars/agent-123.jpg",
"licenseNumber": "DA123456",
"agency": "GoodGo Agency",
"qualityScore": 4.8,
"bio": "Specialized in high-end real estate in District 1 and 7",
"serviceAreas": ["quan-1", "quan-7", "thu-duc"],
"isVerified": true,
"totalListings": 45,
"activeListings": 32,
"avgReviewRating": 4.7,
"totalReviews": 120,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-04-10T15:45:00Z"
}
🎯 Tham Khảo Các File Chính
| Giai Đoạn | File Cần Tạo/Chỉnh Sửa |
|---|---|
| Backend | agents/application/queries/get-agent-profile/* |
| Backend | agents/presentation/controllers/agents.controller.ts |
| Backend | agents/presentation/dto/agent-public-profile.dto.ts |
| Frontend | lib/agents-api.ts |
| Frontend | lib/agents-server.ts |
| Frontend | app/[locale]/(public)/agents/[id]/page.tsx |
| Frontend | components/agents/agent-detail-client.tsx |
| Frontend | components/agents/agent-header.tsx |
| Frontend | components/agents/agent-reviews-section.tsx |
| Frontend | components/agents/agent-listings-section.tsx |