Fecha: 2026-02-09 Estado: ✅ Completado (Fases 1, 2 y 3) Objetivo: Preparar la infraestructura para noticias geolocalizadas y categorías unificadas
Sprint 20 establece las bases para un sistema de categorización mejorado y soporte de geolocalización de usuarios, permitiendo contenido personalizado por ubicación.
| Componente | Tecnología | Estado | Descripción |
|---|---|---|---|
| User Location | Prisma String? | ✅ | Campo location añadido |
| Topic Model | Prisma + PostgreSQL | ✅ | Modelo Topic con 8 categorías |
| Database Migration | Prisma Migrate | ✅ | Migración aplicada exitosamente |
| Database Seed | TypeScript + Prisma | ✅ | 8 temas iniciales creados |
| Seed Configuration | prisma.config.ts | ✅ | Comando prisma db seed listo |
- Añadir campo
locational modelo User - Crear modelo
Topicpara categorización - Definir 8 categorías unificadas con slugs URL-friendly
- Ejecutar migración de base de datos
- Poblar base de datos con seed inicial
- Crear
TopicRepository(Domain + Infrastructure) - Crear Use Cases:
GetAllTopics,GetTopicBySlug - Crear
TopicControllercon endpointGET /api/topics - Actualizar
UserControllerpara gestionarlocation - Lógica inteligente para categoría "Local" basada en ubicación del usuario
- Lógica inteligente para categoría "Ciencia y Tecnología" (búsqueda paralela)
- Actualizar sidebar con 8 nuevas categorías (iconos Lucide)
- Campo de ubicación en perfil de usuario
- Integración de zustand para gestionar estado del formulario
- Actualizar routing para soportar slugs de temas (/?topic=espana)
- Hooks actualizados (useNews, useNewsInfinite) para soportar topics dinámicos
Archivo: backend/prisma/schema.prisma
model User {
id String @id // Firebase UID como ID primario
email String @unique
name String?
picture String?
plan UserPlan @default(FREE)
location String? // ⭐ NUEVO: Sprint 20 - Geolocalización
preferences Json?
usageStats Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
favorites Favorite[]
searchHistory SearchHistory[]
chats Chat[]
@@map("users")
}Uso del campo location:
- Formato libre:
"Madrid, España","Barcelona","Andalucía" - Opcional: usuarios pueden no configurarlo
- Futuro: base para filtrado de noticias locales
model Topic {
id String @id @default(uuid())
name String // Nombre display: "Ciencia y Tecnología"
slug String @unique // URL-friendly: "ciencia-tecnologia"
description String? // Descripción SEO
order Int? // Orden de visualización
createdAt DateTime @default(now())
@@map("topics")
}Ventajas del modelo Topic:
- ✅ Slugs URL-friendly para routing SEO
- ✅ Descripciones personalizadas por categoría
- ✅ Orden configurable (no hardcoded)
- ✅ Extensible: fácil añadir categorías nuevas
Archivo: backend/prisma/migrations/20260209091431_add_location_and_topics/migration.sql
-- AlterTable
ALTER TABLE "users" ADD COLUMN "location" TEXT;
-- CreateTable
CREATE TABLE "topics" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"order" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "topics_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "topics_slug_key" ON "topics"("slug");Resultado:
- ✅ Base de datos sincronizada con schema
- ✅ Sin downtime (campos opcionales)
- ✅ Índice único en
topics.slugpara búsquedas eficientes
| # | Nombre | Slug | Descripción | Fusión |
|---|---|---|---|---|
| 1 | España | espana |
Noticias nacionales de España | - |
| 2 | Internacional | internacional |
Actualidad mundial y noticias internacionales | - |
| 3 | Local | local |
Noticias de tu localidad (basado en geolocalización) | 🆕 NUEVO |
| 4 | Economía | economia |
Finanzas, mercados, empresas y economía | - |
| 5 | Ciencia y Tecnología | ciencia-tecnologia |
Innovación, ciencia, tecnología y descubrimientos | ⭐ Fusión |
| 6 | Entretenimiento | entretenimiento |
Cine, series, música, cultura y espectáculos | - |
| 7 | Deportes | deportes |
Fútbol, baloncesto y actualidad deportiva | - |
| 8 | Salud | salud |
Bienestar, medicina, nutrición y vida saludable | - |
Antes (Sprint 1-19):
- Categoría "Ciencia" (ciencia)
- Categoría "Tecnología" (tecnologia)
Después (Sprint 20+):
- Categoría "Ciencia y Tecnología" (
ciencia-tecnologia)
Razón: Temáticas muy relacionadas, mejor UX con menos fragmentación.
Propósito: Noticias específicas de la ubicación del usuario
Requisito: Campo User.location configurado
Uso futuro: Filtrado por ciudad/región en RSS o APIs externas
Archivo: backend/prisma/seed.ts
import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
// Inicializar con adapter (requerido por el proyecto)
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
/**
* Temas Unificados (Sprint 20)
* - Fusión de "Ciencia" + "Tecnología" → "Ciencia y Tecnología"
* - Slugs URL-friendly para routing
*/
const defaultTopics = [
{
name: 'España',
slug: 'espana',
description: 'Noticias nacionales de España',
order: 1,
},
{
name: 'Internacional',
slug: 'internacional',
description: 'Actualidad mundial y noticias internacionales',
order: 2,
},
{
name: 'Local',
slug: 'local',
description: 'Noticias de tu localidad (basado en geolocalización)',
order: 3,
},
{
name: 'Economía',
slug: 'economia',
description: 'Finanzas, mercados, empresas y economía',
order: 4,
},
{
name: 'Ciencia y Tecnología',
slug: 'ciencia-tecnologia',
description: 'Innovación, ciencia, tecnología y descubrimientos',
order: 5,
},
{
name: 'Entretenimiento',
slug: 'entretenimiento',
description: 'Cine, series, música, cultura y espectáculos',
order: 6,
},
{
name: 'Deportes',
slug: 'deportes',
description: 'Fútbol, baloncesto y actualidad deportiva',
order: 7,
},
{
name: 'Salud',
slug: 'salud',
description: 'Bienestar, medicina, nutrición y vida saludable',
order: 8,
},
];
async function main() {
console.log('🌱 Iniciando seed de base de datos...');
console.log('📂 Creando temas por defecto...');
for (const topic of defaultTopics) {
const created = await prisma.topic.upsert({
where: { slug: topic.slug },
update: {
name: topic.name,
description: topic.description,
order: topic.order,
},
create: topic,
});
console.log(`✅ Tema creado/actualizado: ${created.name} (${created.slug})`);
}
console.log('✨ Seed completado exitosamente!');
console.log(`📊 Total de temas: ${defaultTopics.length}`);
}
main()
.catch((e) => {
console.error('❌ Error durante el seed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});Archivo: backend/prisma.config.ts
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'ts-node prisma/seed.ts', // ⭐ NUEVO
},
datasource: {
url: env('DATABASE_URL'),
},
});cd backend
npx prisma db seedResultado:
🌱 Iniciando seed de base de datos...
📂 Creando temas por defecto...
✅ Tema creado/actualizado: España (espana)
✅ Tema creado/actualizado: Internacional (internacional)
✅ Tema creado/actualizado: Local (local)
✅ Tema creado/actualizado: Economía (economia)
✅ Tema creado/actualizado: Ciencia y Tecnología (ciencia-tecnologia)
✅ Tema creado/actualizado: Entretenimiento (entretenimiento)
✅ Tema creado/actualizado: Deportes (deportes)
✅ Tema creado/actualizado: Salud (salud)
✨ Seed completado exitosamente!
📊 Total de temas: 8
Archivo: backend/prisma/verify-topics.ts (temporal)
import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
async function main() {
console.log('🔍 Verificando temas en base de datos...\n');
const topics = await prisma.topic.findMany({
orderBy: { order: 'asc' },
});
console.log(`📊 Total de temas encontrados: ${topics.length}\n`);
topics.forEach((topic) => {
console.log(`✅ ${topic.order}. ${topic.name} (${topic.slug})`);
console.log(` 📝 ${topic.description}`);
console.log(` 🆔 ID: ${topic.id}`);
console.log(` 📅 Creado: ${topic.createdAt.toLocaleString('es-ES')}\n`);
});
}
main()
.catch((e) => {
console.error('❌ Error:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});Ejecutar:
npx ts-node prisma/verify-topics.ts- ✅
backend/prisma/seed.ts- Seed con 8 temas y adapter PostgreSQL - ✅
backend/prisma/verify-topics.ts- Script de verificación (temporal) - ✅
backend/prisma/migrations/20260209091431_add_location_and_topics/- Migración SQL - ✅
docs/sprints/Sprint-20-Geolocalizacion-Topics.md- Este documento
- ✅
backend/prisma/schema.prisma- AñadidoUser.location+ modeloTopic - ✅
backend/prisma.config.ts- Configurado comando de seed
Archivos creados:
backend/src/domain/entities/topic.entity.ts- Entidad Topic con método reconstitutebackend/src/domain/repositories/topic.repository.ts- Interface ITopicRepository
Entidad Topic implementada:
export class Topic {
constructor(
public readonly id: string,
public readonly name: string,
public readonly slug: string,
public readonly description: string | null,
public readonly order: number | null,
public readonly createdAt: Date
) {}
static reconstitute(data: any): Topic {
return new Topic(
data.id,
data.name,
data.slug,
data.description,
data.order,
data.createdAt
);
}
}Use Cases creados:
GetAllTopicsUseCase- Obtiene lista de temas ordenadosGetTopicBySlugUseCase- Busca tema específico por slug
Implementaciones:
PrismaTopicRepository- Implementa findAll() y findBySlug()TopicController- Endpoints REST públicos (sin autenticación)
Endpoints implementados:
GET /api/topics
Response: [
{
"id": "uuid",
"name": "España",
"slug": "espana",
"description": "Noticias nacionales de España",
"order": 1
},
...
]
GET /api/topics/:slug
Response: {
"id": "uuid",
"name": "España",
"slug": "espana",
"description": "Noticias nacionales de España",
"order": 1
}
UserController actualizado:
// GET /api/users/profile
Response: {
"id": "firebase-uid",
"email": "user@example.com",
"name": "Usuario",
"location": "Madrid, España", // ⭐ NUEVO
...
}
// PATCH /api/users/profile
Body: { "name": "...", "location": "Madrid, España" }
Response: { "success": true, "profile": {...} }NewsController actualizado con smart routing:
- Categoría "Local": Usa
user.locationpara crear query personalizado - Categoría "Ciencia y Tecnología": Búsqueda paralela en ambas subcategorías
- Auto-fill: Detecta categorías vacías y dispara ingesta automática
Sidebar actualizado (components/layout/sidebar.tsx):
- 8 nuevos topic items con iconos específicos de Lucide:
- España → Flag
- Internacional → Globe
- Local → MapPin
- Economía → TrendingUp
- Ciencia y Tecnología → FlaskConical
- Entretenimiento → Film
- Deportes → Trophy
- Salud → HeartPulse
- Navegación unificada con Links a
/?topic={slug}
ProfileHeader actualizado (components/profile/ProfileHeader.tsx):
- Campo de ubicación: "Ubicación (Ciudad, País)"
- Input con placeholder: "Ej: Madrid, España"
- Integración con zustand store
useNews.ts: Cambiado deCategoryIdastringpara soportar topics dinámicosuseNewsInfinite.ts: Actualizado para aceptar cualquier topicusePrefetchNews: Actualizado para nueva estructura
Profile Form Store (stores/profile-form.store.ts):
interface ProfileFormState {
name: string;
location: string; // ⭐ NUEVO: Sprint 20
selectedCategories: string[];
setLocation: (location: string) => void;
...
}- Routing actualizado para soportar slugs:
/?topic=ciencia-tecnologia - useSearchParams con Suspense boundary para Next.js 13+
- Compatibilidad con favoritos:
/?topic=favorites
| Métrica | Valor |
|---|---|
| Archivos Nuevos | 4 (schema, seed, verify, doc) |
| Archivos Modificados | 2 (schema, config) |
| Líneas de Código | ~250 |
| Temas Creados | 8 categorías |
| Tiempo Implementación | ~2 horas |
| Tests Manuales | ✅ Verificación exitosa |
- Campo
User.locationexiste en schema y BD - Modelo
Topicexiste con campos requeridos - Migración aplicada sin errores
- 8 temas creados en base de datos
- Comando
prisma db seedfuncional - Seed usa adapter PostgreSQL correctamente
- Slugs son URL-friendly (sin espacios ni acentos)
- Documentación completa del sprint
Problema: Inicialización estándar new PrismaClient() fallaba.
Solución: Usar PrismaPg adapter:
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });Problema: Prisma no reconocía comando de seed.
Solución: Configurar en prisma.config.ts:
migrations: {
seed: 'ts-node prisma/seed.ts',
}Importante: Siempre ejecutar npx prisma generate después de modificar el schema para actualizar tipos TypeScript.
- Prisma Migrate Docs
- Prisma Seeding
- PostgreSQL Adapter
- Sprint 18: Per-User Favorites (contexto de autenticación)
- Sprint 19: Waterfall Search (contexto de búsqueda)
Sprint 20 - Completado establece un sistema robusto de categorización y geolocalización de usuarios:
- ✅ Schema actualizado con campo
locationy modeloTopic - ✅ Migración aplicada sin downtime
- ✅ 8 temas unificados creados en BD con slugs SEO-friendly
- ✅ Seed configurado para facilitar deploy en otros entornos
- ✅ Backend API implementado con TopicRepository, Use Cases y Controllers
- ✅ Frontend actualizado con sidebar de 8 categorías y campo location en perfil
- ✅ Smart routing para Local y Ciencia y Tecnología
- ✅ Type system migrado de CategoryId (union) a string dinámico
Fusión importante: "Ciencia" + "Tecnología" → "Ciencia y Tecnología"
Nueva categoría: "Local" (usa user.location para personalizar búsquedas)
Archivos modificados:
- Backend: 12 archivos (Domain, Application, Infrastructure)
- Frontend: 8 archivos (Hooks, Components, Stores, Pages)
Status: ✅ Sprint 20 completado - Sistema de categorías dinámico operativo
Próximo Sprint: Sprint 22 - UI Cleanup y optimización de búsqueda con keywords 🚀