Asignatura: Programación y Plataformas Web
Tecnología Backend: Spring Boot | Java | Gradle (Kotlin DSL) | H2 | application.yml
Tecnología Frontend: Angular 18+ | TypeScript | Standalone Components
Sistema de gestión de portafolio de desarrolladores y sus proyectos de software. La aplicación permite gestionar información de personas (desarrolladores), sus enlaces de contacto (GitHub, LinkedIn, etc.) y los proyectos de software que han desarrollado. El sistema incluye funcionalidades para listar, filtrar y gestionar el estado activo/inactivo de las personas.
Modelo de relaciones:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
runtimeOnly("com.h2database:h2")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
spring:
datasource:
url: jdbc:h2:mem:portfoliodb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: none
show-sql: true
properties:
hibernate:
format_sql: true
sql:
init:
mode: always
schema-locations: classpath:schema.sql
data-locations: classpath:data.sql
server:
port: 8080
# CORS para permitir peticiones desde Angular (puerto 4200)
spring:
web:
cors:
allowed-origins: "http://localhost:4200"
allowed-methods: "*"
allowed-headers: "*"
Ubicación obligatoria:
src/main/resources/schema.sqlsrc/main/resources/data.sqlTabla: persons
| Campo | Tipo | Descripción |
|---|---|---|
| id | BIGINT | Identificador único (PK) |
| first_name | VARCHAR(50) | Nombre de la persona |
| last_name | VARCHAR(50) | Apellido de la persona |
| VARCHAR(100) | Correo electrónico | |
| bio | TEXT | Biografía/descripción |
| profession | VARCHAR(100) | Profesión (ej: Full-Stack Developer) |
| location | VARCHAR(100) | Ubicación geográfica |
| active | CHAR(1) | Estado: "S" = activo, "N" = inactivo |
Tabla: contact_links
| Campo | Tipo | Descripción |
|---|---|---|
| id | BIGINT | Identificador único (PK) |
| name | VARCHAR(50) | Nombre del enlace (GitHub, LinkedIn, etc.) |
| url | VARCHAR(255) | URL del enlace |
| person_id | BIGINT | FK a persons (ManyToOne) |
Tabla: projects
| Campo | Tipo | Descripción |
|---|---|---|
| id | BIGINT | Identificador único (PK) |
| name | VARCHAR(100) | Nombre del proyecto |
| description | TEXT | Descripción del proyecto |
| technologies | TEXT | Lista de tecnologías separadas por comas |
| status | VARCHAR(50) | Estado: "Completed", "In Progress", "Planned" |
| cost | DECIMAL | Costo del proyecto |
| start_date | DATE | Fecha de inicio |
| end_date | DATE | Fecha de finalización (puede ser NULL) |
| person_id | BIGINT | FK a persons (ManyToOne) |
| active | CHAR(1) | Estado: "S" = activo, "N" = inactivo |
Tabla: project_links
| Campo | Tipo | Descripción |
|---|---|---|
| id | BIGINT | Identificador único (PK) |
| name | VARCHAR(50) | Nombre del enlace (Repository, Demo, Docs) |
| url | VARCHAR(255) | URL del enlace |
| project_id | BIGINT | FK a projects (OneToOne) |
Relaciones:
ALTER TABLE contact_links ADD FOREIGN KEY (person_id) REFERENCES persons(id);
ALTER TABLE projects ADD FOREIGN KEY (person_id) REFERENCES persons(id);
ALTER TABLE project_links ADD FOREIGN KEY (project_id) REFERENCES projects(id);
Eliminación lógica: Campo active ("S" = activo, "N" = eliminado)
en persons y projects
Ruta: GET /api/persons
Query Parameter:
activeOnly (Boolean, opcional) - Si es true, solo devuelve personas activas. Default:
false
DTO Response: Lista de PersonSummaryDto
[
{
"id": 1,
"firstName": "Juan",
"lastName": "Pérez",
"profession": "Full-Stack Developer",
"location": "Quito, Ecuador",
"active": true,
"contactLinks": [
{
"id": 1,
"name": "GitHub",
"url": "https://github.com/juanperez"
},
{
"id": 2,
"name": "LinkedIn",
"url": "https://linkedin.com/in/juanperez"
}
],
"projectCount": 8
},
{
"id": 2,
"firstName": "María",
"lastName": "González",
"profession": "Backend Developer",
"location": "Guayaquil, Ecuador",
"active": true,
"contactLinks": [
{
"id": 5,
"name": "GitHub",
"url": "https://github.com/mariagonzalez"
}
],
"projectCount": 0
}
]
Reglas:
activeOnly=true: solo personas con active = "S"activeOnly=false o no especificado: todas las personasprojectCount: número de proyectos activos de la personacontactLinks de cada personaCódigos HTTP:
200 OK – Siempre (incluso si lista vacía [])Ruta: PATCH /api/persons/{id}/toggle-active
Path Variable:
id (Long) - ID de la personaResponse Body: PersonStatusDto
{
"id": 1,
"firstName": "Juan",
"lastName": "Pérez",
"active": false,
"message": "Person deactivated successfully"
}
Reglas:
active = "S"), cambiarla a inactiva
(active = "N")Códigos HTTP:
200 OK – Cambio exitoso404 NOT FOUND – Persona no existeMensaje de error:
{
"message": "Person not found"
}
Ruta: GET /api/persons/{id}
Path Variable:
id (Long) - ID de la personaDTO Response: PersonDetailDto
{
"id": 1,
"firstName": "Juan",
"lastName": "Pérez",
"email": "juan.perez@example.com",
"bio": "Desarrollador full-stack con 5 años de experiencia...",
"profession": "Full-Stack Developer",
"location": "Quito, Ecuador",
"active": true,
"contactLinks": [
{
"id": 1,
"name": "GitHub",
"url": "https://github.com/juanperez"
},
{
"id": 2,
"name": "LinkedIn",
"url": "https://linkedin.com/in/juanperez"
},
{
"id": 3,
"name": "Portfolio",
"url": "https://juanperez.dev"
}
],
"projectCount": 8
}
Reglas:
projectCount: número de proyectos activosCódigos HTTP:
200 OK – Persona existe404 NOT FOUND – Persona no existeMensaje de error:
{
"message": "Person not found"
}
Ruta: GET /api/persons/{id}/projects
Path Variable:
id (Long) - ID de la personaQuery Parameter:
minCost (Double, opcional) - Costo mínimo para filtrar proyectos. Si no se especifica, devuelve
todos los proyectosDTO Response: PersonProjectsDto
{
"personId": 1,
"personName": "Juan Pérez",
"active": true,
"filterApplied": true,
"minCost": 5000.00,
"projectCount": 3,
"projects": [
{
"id": 1,
"name": "E-Commerce Platform",
"description": "Plataforma de comercio electrónico completa...",
"technologies": "Spring Boot, Angular, PostgreSQL, AWS",
"status": "Completed",
"cost": 15000.00,
"startDate": "2023-01-15",
"endDate": "2023-06-30",
"projectLink": {
"name": "Repository",
"url": "https://github.com/juanperez/ecommerce"
}
},
{
"id": 3,
"name": "Mobile Banking App",
"description": "Aplicación móvil para gestión bancaria...",
"technologies": "React Native, Node.js, MongoDB",
"status": "In Progress",
"cost": 12000.00,
"startDate": "2023-09-01",
"endDate": null,
"projectLink": {
"name": "Demo",
"url": "https://demo.banking.com"
}
}
]
}
Reglas:
active = "S"minCost está especificado: filtrar proyectos con cost >= minCostminCost no está especificado: devolver todos los proyectos activosfilterApplied: true si se aplicó filtro de costo, false si noprojects: [],
projectCount: 0
Códigos HTTP:
200 OK – Persona existe (incluso si no tiene proyectos)404 NOT FOUND – Persona no existeMensaje de error:
{
"message": "Person not found"
}
Caso especial - Persona sin proyectos:
{
"personId": 2,
"personName": "María González",
"active": true,
"filterApplied": false,
"minCost": null,
"projectCount": 0,
"projects": []
}
@Entity
@Table(name = "persons")
public class Person {
// Agregar annotations e imports necesarios
private Long id;
private String firstName;
private String lastName;
private String email;
private String bio;
private String profession;
private String location;
private Character active;
private List<ContactLink> contactLinks;
private List<Project> projects;
// Getters, setters, constructors
}
@Entity
@Table(name = "contact_links")
public class ContactLink {
// Agregar annotations e imports necesarios
private Long id;
private String name;
private String url;
private Person person;
// Getters, setters, constructors
}
@Entity
@Table(name = "projects")
public class Project {
// Agregar annotations e imports necesarios
private Long id;
private String name;
private String description;
private String technologies;
private String status;
private Double cost;
private LocalDate startDate;
private LocalDate endDate;
private Character active;
private Person person;
private ProjectLink projectLink;
// Getters, setters, constructors
}
@Entity
@Table(name = "project_links")
public class ProjectLink {
// Agregar annotations e imports necesarios
private Long id;
private String name;
private String url;
private Project project;
// Getters, setters, constructors
}
Cada módulo con sus paquetes correspondientes:
La estructura debe seguir buenas prácticas de arquitectura en capas.
VERSION DE ANGULAR: 19+ USO DE SEÑALES
Versión: Angular 18+
Arquitectura: Standalone Components
Estilo: CSS/SCSS a elección
Instalación:
ng new portfolio-frontend --standalone --routing
cd portfolio-frontend
ng serve
// app.routes.ts
export const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'proyectos/:id', component: ProjectsComponent },
{ path: 'proyectos', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', redirectTo: '/home' }
];
Rutas:
/ → Redirige a /home/home → Página principal con listado de personas/proyectos/:id → Página de proyectos de una persona específica/proyectos (sin ID) → Redirige a /home/homeComponente: HomeComponent
Funcionalidad:
Listado de personas:
Filtro de personas activas:
Íconos de redes sociales:
Botón Activar/Desactivar:
PATCH /api/persons/{id}/toggle-activeBotón Ver Proyectos:
/proyectos/{id}Diseño sugerido:
Endpoint utilizado:
GET /api/persons?activeOnly={true|false}PATCH /api/persons/{id}/toggle-activeComponente: ProjectsComponent
Funcionalidad:
Encabezado con información de la persona:
Filtro de proyectos por costo:
Listado de proyectos:
Manejo de casos especiales:
Navegación:
Diseño sugerido:
Endpoints utilizados:
GET /api/persons/{id} - Para obtener información de la personaGET /api/persons/{id}/projects?minCost={valor} - Para obtener proyectosCrear interfaces que coincidan con los DTOs del backend:
// person.models.ts
export interface PersonSummary {
id: number;
firstName: string;
lastName: string;
profession: string;
location: string;
active: boolean;
contactLinks: ContactLink[];
projectCount: number;
}
export interface PersonDetail {
id: number;
firstName: string;
lastName: string;
email: string;
bio: string;
profession: string;
location: string;
active: boolean;
contactLinks: ContactLink[];
projectCount: number;
}
export interface ContactLink {
id: number;
name: string;
url: string;
}
export interface PersonStatus {
id: number;
firstName: string;
lastName: string;
active: boolean;
message: string;
}
export interface PersonProjects {
personId: number;
personName: string;
active: boolean;
filterApplied: boolean;
minCost: number | null;
projectCount: number;
projects: Project[];
}
export interface Project {
id: number;
name: string;
description: string;
technologies: string;
status: string;
cost: number;
startDate: string;
endDate: string | null;
projectLink: ProjectLink;
}
export interface ProjectLink {
name: string;
url: string;
}
Estructura de componentes:
src/app/
├── components/
│ ├── home/
│ │ └── home.component.ts
│ ├── projects/
│ │ └── projects.component.ts
│ ├── person-card/ (opcional: componente reutilizable)
│ │ └── person-card.component.ts
│ └── project-card/ (opcional: componente reutilizable)
│ └── project-card.component.ts
├── services/
│ └── person.service.ts
├── models/
│ └── person.models.ts
└── app.routes.ts
| Criterio | Puntos |
|---|---|
| ENDPOINT 1 – GET /api/persons (con filtro activeOnly) | 6.0 |
| ENDPOINT 2 – PATCH /api/persons/{id}/toggle-active | 6.0 |
| ENDPOINT 3 – GET /api/persons/{id} | 6.0 |
| ENDPOINT 4 – GET /api/persons/{id}/projects (con filtro minCost) | 6.5 |
| Relaciones JPA correctas (OneToMany, ManyToOne, OneToOne) | 2.0 |
| Estructura del proyecto y arquitectura | 4.0 |
| SUBTOTAL BACKEND | 30.0 |
| Criterio | Puntos |
|---|---|
| Página Home: Listado de personas con cards | 6.0 |
| Página Home: Filtro activos y botón activar/desactivar | 2.0 |
| Página Home: Íconos de redes sociales y navegación | 1.5 |
| Página Proyectos: Información de persona y listado de proyectos | 6.0 |
| Página Proyectos: Filtro por costo | 2.0 |
| Manejo de casos especiales (sin proyectos, persona inexistente) | 2.0 |
| Validaciones de rutas (/proyectos sin ID, ID inválido) | 1.5 |
| SUBTOTAL FRONTEND | 20.0 |
Backend:
Frontend:
Datos de prueba:
Penalización total (0 puntos) si:
Backend:
src/main/resources/Frontend:
ng build) sin erroresng serve)Formato de entrega: