Skip to content

Faq funcionalidad #16

@alansastre

Description

@alansastre

Objetivo

Crear una sección de preguntas frecuentes (FAQ) en la aplicación, accesible en http://localhost:8080/faqs. Es una entidad
nueva, completamente independiente del resto del proyecto: no se relaciona con casas, reservas, reseñas ni usuarios. Por eso
no necesitas coordinarte con nadie y no vas a tener conflictos de Git con tus compañeros.

¿Por qué esta tarea?

El objetivo es que practiques a tu ritmo el ciclo completo de Spring MVC con una pieza aislada y manejable:

  1. Entidad (@Entity) → cómo se define una tabla
  2. Repositorio (extends JpaRepository) → cómo se accede a la base de datos
  3. DataInitializer (CommandLineRunner) → cómo se cargan datos de ejemplo
  4. Controller (@Controller, @GetMapping) → cómo se atiende una petición HTTP
  5. Plantilla Thymeleaf (th:each, th:text) → cómo se pinta el HTML

Cada paso está en un archivo distinto y los cinco son archivos nuevos. No tienes que modificar absolutamente nada de lo que
ya existe.

Lo que vas a crear

# Archivo Carpeta
1 Faq.java src/main/java/com/demo/model/
2 FaqRepository.java src/main/java/com/demo/repository/
3 FaqDataInitializer.java src/main/java/com/demo/config/
4 FaqController.java src/main/java/com/demo/controller/
5 faq-list.html src/main/resources/templates/faq/ (crea también la carpeta faq/)

Paso 1 — Crear la entidad Faq

Crea src/main/java/com/demo/model/Faq.java:

package com.demo.model;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "faqs")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Faq {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 300)
    private String question;

    @Column(nullable = false, length = 2000)
    private String answer;

    @Column(length = 100)
    private String category;
}

¿Qué significa cada anotación?

  • @Entity → le dice a JPA que esta clase representa una tabla.
  • @Table(name = "faqs") → la tabla se llamará faqs en la base de datos.
  • @Id + @GeneratedValue(...IDENTITY) → el campo id es la clave primaria autoincremental.
  • @Column(nullable = false, length = ...) → restricciones de la columna en BD.
  • @Getter, @Setter, @Builder, @NoArgsConstructor, @AllArgsConstructor → Lombok te genera automáticamente los
    getters/setters, el patrón builder y los dos constructores.

Paso 2 — Crear el repositorio

Crea src/main/java/com/demo/repository/FaqRepository.java:

package com.demo.repository;

import com.demo.model.Faq;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FaqRepository extends JpaRepository<Faq, Long> {
}

Solo eso. Es una interfaz vacía. Heredando de JpaRepository<Faq, Long> ya tienes gratis: findAll(), findById(),
save(), deleteById(), count(), etc. Spring genera la implementación automáticamente.

Paso 3 — Crear el DataInitializer

Crea src/main/java/com/demo/config/FaqDataInitializer.java:

package com.demo.config;

import com.demo.model.Faq;
import com.demo.repository.FaqRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("!test")
public class FaqDataInitializer implements CommandLineRunner {

    private final FaqRepository faqRepository;

    public FaqDataInitializer(FaqRepository faqRepository) {
        this.faqRepository = faqRepository;
    }

    @Override
    public void run(String... args) {
        if (faqRepository.count() > 0) return;

        faqRepository.save(Faq.builder()
                .question("¿Cómo reservo una casa?")
                .answer("Entra al detalle de la casa, elige las fechas de entrada y salida y pulsa 'Reservar'. Recibirás una
confirmación cuando el anfitrión acepte la reserva.")
                .category("Reservas")
                .build());

        faqRepository.save(Faq.builder()
                .question("¿Puedo cancelar una reserva?")
                .answer("Sí, puedes cancelar desde la sección 'Mis reservas' siempre que la reserva esté en estado PENDING o
CONFIRMED.")
                .category("Reservas")
                .build());

        faqRepository.save(Faq.builder()
                .question("¿Cómo me convierto en anfitrión?")
                .answer("Cualquier usuario registrado puede publicar casas. Solo tienes que ir a 'Nueva casa' desde tu panel y
rellenar los datos del alojamiento.")
                .category("Anfitriones")
                .build());

        faqRepository.save(Faq.builder()
                .question("¿Qué hago si tengo un problema con mi estancia?")
                .answer("Contacta primero con el anfitrión. Si no se resuelve, puedes escribir al equipo de soporte desde el
panel de usuario.")
                .category("Soporte")
                .build());

        faqRepository.save(Faq.builder()
                .question("¿Las mascotas están permitidas?")
                .answer("Depende de cada casa. Filtra por el equipamiento 'Admite mascotas' para ver las que sí las aceptan.")
                .category("General")
                .build());
    }
}

¿Qué hace cada anotación?

  • @Component → Spring registra esta clase como un bean (la instancia y la gestiona él).
  • @Profile("!test") → este código NO se ejecuta cuando se corren los tests (importante para no contaminar los tests).
  • implements CommandLineRunner → al arrancar la aplicación, Spring llama al método run() automáticamente.
  • if (faqRepository.count() > 0) return; → si ya hay datos, no inserta duplicados al reiniciar.

💡 Date cuenta de que este DataInitializer es propio tuyo: no toca el HouseDataInitializer ni el
BookingDataInitializer de tus compañeros. Conviven sin problema.

Paso 4 — Crear el controller

Crea src/main/java/com/demo/controller/FaqController.java:

package com.demo.controller;

import com.demo.model.Faq;
import com.demo.repository.FaqRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class FaqController {

    private final FaqRepository faqRepository;

    public FaqController(FaqRepository faqRepository) {
        this.faqRepository = faqRepository;
    }

    @GetMapping("/faqs")
    public String faqList(Model model) {
        List<Faq> faqs = faqRepository.findAll();
        model.addAttribute("faqs", faqs);
        return "faq/faq-list";
    }
}

¿Qué hace cada línea?

  • @Controller → Spring sabe que esta clase atiende peticiones HTTP y devuelve vistas Thymeleaf.
  • Constructor con FaqRepository → Spring inyecta el repositorio automáticamente (inyección por constructor, no hace falta
    @Autowired).
  • @GetMapping("/faqs") → cuando alguien entra a http://localhost:8080/faqs se ejecuta este método.
  • faqRepository.findAll() → trae todos los registros de la tabla faqs.
  • model.addAttribute("faqs", faqs) → pasa la lista a la plantilla con el nombre faqs.
  • return "faq/faq-list" → renderiza el archivo templates/faq/faq-list.html.

Paso 5 — Crear la plantilla HTML

Primero crea la carpeta src/main/resources/templates/faq/, y dentro el archivo faq-list.html:

<!DOCTYPE html>
<html lang="es" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layout/head :: head('Preguntas frecuentes')}"></head>
<body>

<nav th:replace="~{layout/navbar :: navbar}"></nav>

<main class="container flex-grow-1">

    <h1 class="mb-4">
        <i class="fa-solid fa-circle-question text-primary me-2"></i> Preguntas frecuentes
    </h1>

    <div th:if="${faqs.isEmpty()}" class="alert alert-light border">
        Aún no hay preguntas cargadas.
    </div>

    <div class="accordion" id="faqAccordion">
        <div class="accordion-item" th:each="faq, iter : ${faqs}">
            <h2 class="accordion-header">
                <button class="accordion-button collapsed"
                        type="button"
                        data-bs-toggle="collapse"
                        th:attr="data-bs-target='#collapse-' + ${iter.index}"
                        aria-expanded="false">
                    <span class="badge bg-secondary me-2" th:text="${faq.category}">Categoría</span>
                    <span th:text="${faq.question}">¿Pregunta?</span>
                </button>
            </h2>
            <div th:id="'collapse-' + ${iter.index}"
                 class="accordion-collapse collapse"
                 data-bs-parent="#faqAccordion">
                <div class="accordion-body" th:text="${faq.answer}">
                    Respuesta.
                </div>
            </div>
        </div>
    </div>

</main>

<footer th:replace="~{layout/footer :: footer}"></footer>
<script th:replace="~{layout/footer :: scripts}"></script>

</body>
</html>

¿Qué hace cada parte?

  • th:replace="~{layout/head :: head('Preguntas frecuentes')}" → reutiliza el <head> común (con Bootstrap, FontAwesome,
    etc.).
  • th:replace="~{layout/navbar :: navbar}" → mete el navbar arriba.
  • th:if="${faqs.isEmpty()}" → si no hay FAQs, muestra el mensaje.
  • th:each="faq, iter : ${faqs}" → recorre la lista. iter.index es el índice (0, 1, 2...), lo usamos para que cada item del
    acordeón tenga un id único.
  • th:text="${faq.question}" → reemplaza el texto del botón con la pregunta.
  • th:attr="data-bs-target='#collapse-' + ${iter.index}" → genera dinámicamente el atributo HTML que Bootstrap usa para
    abrir/cerrar el panel.

El resultado es un acordeón clásico: pulsas una pregunta y se despliega su respuesta.


Paso 6 — Probar en el navegador

  1. Arranca la aplicación (./mvnw spring-boot:run desde terminal, o el botón verde de IntelliJ).
  2. Abre http://localhost:8080/faqs directamente en el navegador.
  3. Debes ver 5 preguntas con su categoría (Reservas, Anfitriones, Soporte, General).
  4. Pulsa en una pregunta y debe desplegarse la respuesta.
  5. Bonus: abre http://localhost:8080/h2-console y haz SELECT * FROM faqs; para ver tus datos en la BD.

Criterios de aceptación

  • Existen los 5 archivos nuevos en sus carpetas correspondientes.
  • NO se ha modificado ningún archivo previamente existente.
  • La aplicación compila y arranca (./mvnw clean compile pasa).
  • Al entrar a /faqs se ven las 5 preguntas en formato acordeón.
  • Al pulsar una pregunta se despliega la respuesta.
  • En h2-console, SELECT * FROM faqs; devuelve 5 filas.

Errores comunes

Error Causa Solución
Whitelabel Error Page al ir a /faqs El return del controller no coincide con la ruta del HTML Verifica que devuelves
exactamente "faq/faq-list" y que el HTML está en templates/faq/faq-list.html
Cannot resolve symbol 'Faq' en el repositorio o controller Falta el import com.demo.model.Faq; Añádelo arriba del
archivo
BeanCreationException al arrancar Probablemente el package del archivo no coincide con su carpeta El package del Java
debe coincidir con la carpeta: model/Faq.javapackage com.demo.model;
La tabla faqs no aparece en h2-console El DataInitializer no se ejecuta porque falta @Component Asegúrate de tener
@Component y @Profile("!test") arriba de la clase
El acordeón no se abre al pulsar Falta el script de Bootstrap Verifica que el `<script th:replace="~{layout/footer ::
scripts}"></script>` está al final del HTML

Lo que NO tienes que hacer

  • NO toques ningún archivo que ya existía antes. Solo creas archivos nuevos.
  • NO añadas asociaciones a otras entidades (@ManyToOne, @ManyToMany, etc.).
  • NO modifiques el navbar.html (el profesor añadirá el enlace después).
  • NO crees formularios todavía (esa puede ser una tarea siguiente).
  • NO escribas tests por ahora.

Si te quedas atascada

Mira estos archivos del proyecto como referencia (hacen lo mismo pero con otras entidades):

  • src/main/java/com/demo/model/Amenity.java → estructura de una entidad simple sin relaciones.
  • src/main/java/com/demo/repository/AmenityRepository.java → repositorio vacío.
  • src/main/java/com/demo/controller/ReviewController.java líneas 22-27 → patrón de listado.
  • src/main/resources/templates/review/review-list.html → patrón de plantilla con th:each.

El patrón se repite. Una vez entiendas esta cadena (entidad → repositorio → controller → plantilla) para FAQ, lo verás aplicado
igual en todas las demás partes del proyecto.

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions