본문 바로가기
💻 뚝딱뚝딱/방통대CBT

[개발일지#004] 기출문제 목록조회 (검색조회 및 페이지네이션 포함)

by 뚜루리 2024. 6. 18.
728x90
320x100

 

[구현하고자 하는 화면]

소스 이해를 위해 구현하고자 하는 화면을 먼저 띄워보자면

상단의 검색조건을 선택할 때마다 그 검색조건에 맞는 결과값들이 하단에 바로바로 조회되는 방식을 만들고 싶었다.

검색조건 값들도 하드코딩이 아니라 동적으로 불러오는 방식으로 하고 싶었음. 


 

0. 기출문제 검색조건 조회

SearchCriteria.java

package knou.cbt.domain.exam;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

@Getter @Setter
@RequiredArgsConstructor
public class SearchCriteria {

    private String departmentId;
    private String subjectId;
    private String year;
    private String semester;
    private String category;

    public SearchCriteria(String departmentId, String subjectId, String year, String semester, String category) {
        this.departmentId = departmentId;
        this.subjectId = subjectId;
        this.year = year;
        this.semester = semester;
        this.category = category;
    }
}

 

  • 검색조건을 담기 위한 객체 생성했다.

 

SearchResult.java

package knou.cbt.domain.exam;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

@Getter @Setter
@RequiredArgsConstructor
public class SearchResult {

    private String searchKey;
    private String searchValue;
}

 

  • 조회된 검색조건을 key, value 형태로 조회하기 위해 객체를 하나 생성했다.

 

 

ExamSearchService.java

package knou.cbt.domain.exam;

import java.util.List;

public interface ExamSearchService {

    List<SearchResult> searchDepartmentId();

    List<SearchResult> searchSubjectId(String searchValue);

    List<SearchResult> searchYear(String searchValue);

    List<SearchResult> searchSemester(String searchValue);

    List<SearchResult> searchCategory(String searchValue);

}

 

 

ExamSearchServiceImpl.java

package knou.cbt.domain.exam;


import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ExamSearchServiceImpl implements ExamSearchService {

    private final ExamSearchMapper examSearchMapper;

    @Override
    public List<SearchResult> searchDepartmentId() {
        return examSearchMapper.searchDepartmentId();
    }

    @Override
    public List<SearchResult> searchSubjectId(String searchValue) {
        return examSearchMapper.searchSubjectId(searchValue);
    }

    @Override
    public List<SearchResult> searchYear(String searchValue) {
        return examSearchMapper.searchYear(searchValue);
    }

    @Override
    public List<SearchResult> searchSemester(String searchValue) {
        return examSearchMapper.searchSemester(searchValue);
    }

    @Override
    public List<SearchResult> searchCategory(String searchValue) {
        return examSearchMapper.searchCategory(searchValue);
    }
}

 

 

ExamSearchMapper.xml

package knou.cbt.domain.exam;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface ExamSearchMapper {

    List<SearchResult> searchDepartmentId();

    List<SearchResult> searchSubjectId(@Param("searchValue")String searchValue);

    List<SearchResult> searchYear(@Param("searchValue")String searchValue);

    List<SearchResult> searchSemester(@Param("searchValue")String searchValue);

    List<SearchResult> searchCategory(@Param("searchValue")String searchValue);


}

 

 

ExamSearchMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="knou.cbt.domain.exam.ExamSearchMapper">

    <select id="searchDepartmentId" resultType="knou.cbt.domain.exam.SearchResult">
        SELECT DISTINCT SUBSTR(EXAM_ID, 1, 1) AS SEARCH_KEY
                      , ( SELECT DISTINCT DEPARTMENT_NAME
                          FROM DEPARTMENT
                          WHERE USE_YN = 'Y'
                           AND DEPARTMENT_ID = SUBSTR(EXAM_ID, 1, 1) ) AS SEARCH_VALUE
        FROM EXAM
        WHERE USE_YN = 'Y'
    </select>

    <select id="searchSubjectId"
            parameterType="java.lang.String"
            resultType="knou.cbt.domain.exam.SearchResult">
        SELECT   SUBJECT_ID AS SEARCH_KEY
             , ( SELECT SUBJECT_NAME
                 FROM SUBJECT S
                 WHERE USE_YN = 'Y'
                   AND SUBJECT_ID = SUBSTR(EXAM_ID, 1, 4))  AS SEARCH_VALUE
        FROM EXAM
        WHERE USE_YN = 'Y'
          AND SUBSTR(EXAM_ID, 1, 1)  = #{searchValue}
    </select>

    <select id="searchYear"
            parameterType="java.lang.String"
            resultType="knou.cbt.domain.exam.SearchResult">
        SELECT DISTINCT  EXAM_YEAR AS SEARCH_KEY
                       , EXAM_YEAR AS SEARCH_VALUE
        FROM EXAM
        WHERE USE_YN = 'Y'
          AND SUBSTR(EXAM_ID, 1, 4)  = #{searchValue}
    </select>

    <select id="searchSemester"
            parameterType="java.lang.String"
            resultType="knou.cbt.domain.exam.SearchResult">
        SELECT DISTINCT   SEMESTER AS SEARCH_KEY
                        , SEMESTER AS SEARCH_VALUE
        FROM EXAM
        WHERE USE_YN = 'Y'
          AND SUBSTR(EXAM_ID, 1, 8)  = #{searchValue}
    </select>

    <select id="searchCategory"
            parameterType="java.lang.String"
            resultType="knou.cbt.domain.exam.SearchResult">
        SELECT DISTINCT EXAM_CATEGORY AS SEARCH_KEY
                        , CASE
                            WHEN EXAM_CATEGORY = 1 THEN '기말'
                            WHEN EXAM_CATEGORY = 2 THEN '동계계절'
                            ELSE '하계계절'
                          END AS SEARCH_VALUE
        FROM EXAM
        WHERE USE_YN = 'Y'
          AND SUBSTR(EXAM_ID, 1, 9) = #{searchValue}
    </select>

</mapper>
  • 이거 쿼리 짜는 데 상당히 많은 시간을 소요했다. 내가......데이터베이스 구조를 잘못 짰나 싶을 정도였다. 아직도 잘못짠거 같은 느낌적인 느낌.

 

 

ExamSearchController.java

package knou.cbt.web.exam;

import knou.cbt.domain.exam.ExamSearchService;
import knou.cbt.domain.exam.SearchResult;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequiredArgsConstructor
@RequestMapping("/search")
public class ExamSearchController {

    private final ExamSearchService examSearchService;

    @GetMapping("/api")
    public ResponseEntity<List<SearchResult>> searchApiList(
      @RequestParam("searchKey") String searchKey
    , @RequestParam(value = "searchValue", required = false) String searchValue) {

        List<SearchResult> resultList = switch (searchKey) {
            case "departmentId" -> examSearchService.searchDepartmentId();
            case "subjectId" -> examSearchService.searchSubjectId(searchValue);
            case "year" -> examSearchService.searchYear(searchValue);
            case "semester" -> examSearchService.searchSemester(searchValue);
            default -> examSearchService.searchCategory(searchValue);
        };

        return ResponseEntity.ok().body(resultList);
    }

}
  • 아예 컨트롤러도 따로 빼서 작업했다. 

 

 


 

1. 기출문제 목록조회

ExamService.java

package knou.cbt.domain.exam;


import java.util.List;

public interface ExamService {

    List<ExamInfo> allExamList(int page, int size, SearchCriteria searchCriteria);

    int countExams(int page, int size, SearchCriteria searchCriteria);

}

 

ExamServiceImpl.java

package knou.cbt.domain.exam;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ExamServiceImpl implements ExamService{

    private final ExamMapper examMapper;

    @Override
    public List<ExamInfo> allExamList(int page, int size, SearchCriteria searchCriteria) {
        int offset = (page - 1) * size;
        return examMapper.allExamList(offset, size, searchCriteria);
    }


    @Override
    public int countExams(int page, int size, SearchCriteria searchCriteria) {
        int offset = (page - 1) * size;
        return examMapper.countExams(offset, size, searchCriteria);
    }




}

 

 

ExamMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="knou.cbt.domain.exam.ExamMapper">

    <select id="allExamList"
                                parameterType="knou.cbt.domain.exam.SearchCriteria"
                                resultType="knou.cbt.domain.exam.ExamInfo">
        SELECT
                EXAM_ID
                , (SELECT DEPARTMENT_NAME
                    FROM DEPARTMENT
                    WHERE DEPARTMENT_ID = SUBSTR(EXAM_ID, 1, 1)) AS DEPARTMENT_NAME
                , SUBSTR(EXAM_ID, 1, 1) AS DEPARTMENT_ID
                , ( SELECT SUBJECT_NAME
                    FROM SUBJECT
                    WHERE SUBJECT_ID = E.SUBJECT_ID ) AS SUBJECT_NAME
                , SUBJECT_ID
                , EXAM_YEAR
                , GRADE
                , SEMESTER
                , EXAM_CATEGORY
        FROM EXAM E
        WHERE USE_YN = 'Y'
        <if test='searchCriteria.departmentId != null'>
            AND SUBSTR(EXAM_ID, 1, 1) = #{searchCriteria.departmentId}
        </if>
        <if test='searchCriteria.subjectId != null'>
            AND SUBJECT_ID = #{searchCriteria.subjectId}
        </if>
        <if test='searchCriteria.year != null'>
            AND EXAM_YEAR = #{searchCriteria.year}
        </if>
        <if test='searchCriteria.semester != null'>
            AND SEMESTER = #{searchCriteria.semester}
        </if>
        <if test='searchCriteria.category != null'>
            AND EXAM_CATEGORY = #{searchCriteria.category}
        </if>
        ORDER BY EXAM_ID
        LIMIT #{offset}, #{limit}
    </select>

    <select id="countExams" parameterType="knou.cbt.domain.exam.SearchCriteria" resultType="int">
        SELECT COUNT(*)
        FROM EXAM
        WHERE USE_YN = 'Y'
        <if test='searchCriteria.departmentId != null'>
            AND SUBSTR(EXAM_ID, 1, 1) = #{searchCriteria.departmentId}
        </if>
        <if test='searchCriteria.subjectId != null'>
            AND SUBJECT_ID = #{searchCriteria.subjectId}
        </if>
        <if test='searchCriteria.year != null'>
            AND EXAM_YEAR = #{searchCriteria.year}
        </if>
        <if test='searchCriteria.semester != null'>
            AND SEMESTER = #{searchCriteria.semester}
        </if>
        <if test='searchCriteria.category != null'>
            AND EXAM_CATEGORY = #{searchCriteria.category}
        </if>
        ORDER BY EXAM_ID
        LIMIT #{offset}, #{limit}
    </select>


</mapper>

 

 

ExamConroller.java

    @GetMapping
    public String allExamList(Model model
        , @RequestParam(defaultValue = "1") int page
        , @RequestParam(defaultValue = "10") int size
        , @RequestParam(required = false) String departmentId
        , @RequestParam(required = false) String subjectId
        , @RequestParam(required = false) String year
        , @RequestParam(required = false) String semester
        , @RequestParam(required = false) String category){

        SearchCriteria searchCriteria = new SearchCriteria(departmentId, subjectId, year, semester, category);
        List<ExamInfo> exams = examService.allExamList(page, size, searchCriteria);

        int totalExams = examService.countExams(page, size, searchCriteria);
        int totalPages = (int) Math.ceil((double) totalExams / size);
        model.addAttribute("exams", exams);
        model.addAttribute("currentPage", page);
        model.addAttribute("totalPages", totalPages);
        model.addAttribute("totalExams", totalExams);
        return "exam/allExamList";
    }

 

    @GetMapping("/api")
    @ResponseBody
    public ResponseEntity<Map<String, Object>> allExamListForSearch(
         @RequestParam(defaultValue = "1") int page
       , @RequestParam(defaultValue = "10") int size
       , @RequestParam(required = false) String departmentId
       , @RequestParam(required = false) String subjectId
       , @RequestParam(required = false) String year
       , @RequestParam(required = false) String semester
       , @RequestParam(required = false) String category) {

        SearchCriteria searchCriteria = new SearchCriteria(departmentId, subjectId, year, semester, category);
        List<ExamInfo> exams = examService.allExamList(page, size, searchCriteria);
        int totalExams = examService.countExams(page, size, searchCriteria);
        int totalPages = (int) Math.ceil((double) totalExams / size);

        Map<String, Object> result = new HashMap<>();
        result.put("exams", exams);
        result.put("currentPage", page);
        result.put("totalExams", totalExams);
        result.put("totalPages", totalPages);

        return ResponseEntity.ok(result);
    }
  • 조회를 위한 컨트롤러가 2개 인데 각자 용도가 다르다. 원래는 첫번째 컨트롤러로만 이용해 조회를 했는데, 내가 원하는 기능이 검색조건이 변경될 때마다 그에 따라 검색되어 조회 값이 변경되는 형태를 만들고 싶었는데 도저히 저 상태로는 불가능했다. 그래서 생각한게 첫 조회할 때는 첫번째 방법으로 불러오고 그 이후에 검색조건이 변경될 때마다 아래의 컨트롤러를 호출하는 방식으로 분리했다. 
  • 일단 이렇게 해서 해결은 했는데 중복되는 부분이 많아서.....좀더 호율적으로 변경할 수 있지 않을까 하는 나의 작은...고민?

 

 

allExamList.html

<!DOCTYPE html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8">
    <link href="/css/bootstrap.min.css" rel="stylesheet">
    <title>기출문제 목록</title>
    <script th:src="@{/js/searchSelect2.js}"></script>
</head>
<style>
    .table-group-divider th td {
        text-align: center;
    }
</style>
<body>
<div class="container" id="content">
    <div class="py-5 text-center">
        <h2>기출문제 목록</h2>
    </div>
    <div class="row">
        <div class="col">
            <select id="departmentId" class="form-select">
                <option value="">==학과==</option>
            </select>
        </div>
        <div class="col">
            <select id="subjectId" class="form-select" disabled>
                <option selected>== 과목 ==</option>
            </select>
        </div>
        <div class="col">
            <select id="year" class="form-select" disabled>
                <option selected>== 년도 ==</option>
            </select>
        </div>
        <div class="col">
            <select id="semester" class="form-select" disabled>
                <option selected>== 학기 ==</option>
            </select>
        </div>
        <div class="col">
            <select id="category" class="form-select" disabled>
                <option selected>== 구분 ==</option>
            </select>
        </div>
    </div>
    <hr class="my-4">
    <span id="count" style="font-weight: bold;" th:text="|총 ${totalExams} 건|"></span>
    <div>
        <table class="table">
            <thead>
            <tr>
                <th scope="col">학과</th>
                <th scope="col">과목</th>
                <th scope="col">학년</th>
                <th scope="col">시험년도</th>
                <th scope="col">학기</th>
                <th scope="col">구분</th>
                <th scope="col"></th>
            </tr>
            </thead>
            <tbody class="table-group-divider">
            <tr th:each="exam : ${exams}">
                <td><a th:text="${exam.departmentName}">학과</a></td>
                <td><a th:text="${exam.subjectName}">과목</a></td>
                <td><a th:text="|${exam.grade} 학년|">학년</a></td>
                <td><a th:text="|${exam.examYear} 년도|">시험년도</a></td>
                <td><a th:text="|${exam.semester} 학기|">학기</a></td>
                <td>
                    <a th:if="${exam.examCategory == 1}" th:text="'기말'">구분</a>
                    <a th:if="${exam.examCategory == 2}" th:text="'계절학기(동계)'">구분</a>
                    <a th:if="${exam.examCategory == 3}" th:text="'계절학기(하계)'">구분</a>
                </td>
                <td><button type="button" class="btn btn-primary">풀기</button></td>
            </tr>
            </tbody>
        </table>
    </div>
    <hr class="my-4">
    <nav aria-label="Page navigation example"
         th:fragment="pagenation" id="pagination">
        <ul class="pagination" style="display: flex; justify-content: center;">
            <li class="page-item" th:classappend="${currentPage > 1} ? '' : 'disabled'">
                <a class="page-link" th:href="@{/exam(page=1)}">First</a>
            </li>
            <li class="page-item" th:classappend="${currentPage > 1} ? '' : 'disabled'">
                <a class="page-link" th:href="@{/exam(page=${currentPage - 1})}">Previous</a>
            </li>
            <li class="page-item" th:classappend="${page == currentPage} ? 'active' : ''"
                th:each="page : ${#numbers.sequence(1, totalPages != null ? totalPages : 1)}">
                <a class="page-link" th:href="@{/exam(page=${page})}" th:text="${page}"></a>
            </li>
            <li class="page-item" th:classappend="${currentPage < totalPages} ? '' : 'disabled'">
                <a class="page-link" th:href="@{/exam(page=${currentPage + 1})}">Next</a>
            </li>
            <li class="page-item" th:classappend="${currentPage < totalPages} ? '' : 'disabled'">
                <a class="page-link" th:href="@{/exam(page=${totalPages})}">Last</a>
            </li>
        </ul>
    </nav>
</div> <!-- /container -->
</body>

 

 

searchSeelct.js

document.addEventListener('DOMContentLoaded', function() {
    document.querySelectorAll('select').forEach(function(select) {
        select.addEventListener('change', onChange);
    });
    searchSelectChange("departmentId", null);
});

function onChange() {
    const selectId = this.id;
    const selects = {
        departmentId: document.getElementById("departmentId").value,
        subjectId: document.getElementById("subjectId").value,
        year: document.getElementById("year").value,
        semester: document.getElementById("semester").value,
        category: document.getElementById("category").value
    };

    const resetSelect = (id, defaultText) => {
        const select = document.getElementById(id);
        select.innerHTML = `<option value="">== ${defaultText} ==</option>`;
        select.disabled = true;
    };

    const updateSelectOptions = (searchKey, searchValue, defaultText) => {
        searchSelectChange(searchKey, searchValue);
        document.getElementById(searchKey).disabled = false;
    };

    const handleFetch = (url) => {
        fetch(url, { method: "GET" })
            .then(response => {
                if (!response.ok) throw new Error("HTTP request failed");
                return response.json();
            })
            .then(data => {
                updateTable(data.exams, data.exams.length);
                createPagination(data.totalPages, data.currentPage);
            })
            .catch(error => console.error("Error: ", error));
    };

    switch (selectId) {
        case 'departmentId':
            resetSelect('subjectId', '과목');
            resetSelect('year', '년도');
            resetSelect('semester', '학기');
            resetSelect('category', '구분');
            if (this.value) {
                updateSelectOptions("subjectId", this.value, "과목");
                document.getElementById("subjectId").disabled = false;
                handleFetch(`/exam/api?departmentId=${this.value || ''}`);
            }  else {
                handleFetch(`/exam/api`)
            }
            break;

        case 'subjectId':
            resetSelect('year', '년도');
            resetSelect('semester', '학기');
            resetSelect('category', '구분');
            if (this.value) {
                updateSelectOptions("year", this.value, "년도");
                handleFetch(`/exam/api?departmentId=${selects.departmentId}&subjectId=${this.value || ''}`);
            } else {
                handleFetch(`/exam/api?departmentId=${selects.departmentId}`)
            }
            break;


        case 'year':
            resetSelect('semester', '학기');
            resetSelect('category', '구분');
            if (this.value) {
                updateSelectOptions("semester", selects.subjectId + this.value, "학기");
                handleFetch(`/exam?departmentId=${selects.departmentId}&subjectId=${selects.subjectId}&year=${this.value || ''}`);
            } else {
                handleFetch(`/exam?departmentId=${selects.departmentId}&subjectId=${selects.subjectId}`)
            }
            break;

        case 'semester':
            resetSelect('category', '구분');
            if (this.value) {
                updateSelectOptions("category", selects.subjectId + selects.year + this.value, "구분");
                handleFetch(`/exam?departmentId=${selects.departmentId}&subjectId=${selects.subjectId}&year=${selects.year}&semester=${this.value || ''}`);
            } else {
                handleFetch(`/exam?departmentId=${selects.departmentId}&subjectId=${selects.subjectId}&year=${selects.year}`)
            }
            break;

        case 'category':
            if (this.value) {
                handleFetch(`/exam?departmentId=${selects.departmentId}&subjectId=${selects.subjectId}&year=${selects.year}&semester=${selects.semester}&category=${this.value || ''}`);
            } else {
                handleFetch(`/exam?departmentId=${selects.departmentId}&subjectId=${selects.subjectId}&year=${selects.year}&semester=${selects.semester}`);
            }

            break;
    }
}

function searchSelectChange(searchKey, searchValue) {
    fetch(`/search/api?searchKey=${searchKey}&searchValue=${searchValue || ''}`, { method: "GET" })
        .then(response => {
            if (!response.ok) throw new Error("HTTP request failed");
            return response.json();
        })
        .then(datas => {
            const select = document.getElementById(searchKey);
            select.innerHTML = `<option value="">== ${getDefaultText(searchKey)} ==</option>`;
            datas.forEach(data => {
                const opt = document.createElement("option");
                opt.value = data.searchKey;
                opt.text = data.searchValue;
                select.appendChild(opt);
            });
        })
        .catch(error => console.error("Error: ", error));
}

function getDefaultText(key) {
    const texts = {
        departmentId: "학과",
        subjectId: "과목",
        year: "년도",
        semester: "학기",
        category: "구분"
    };
    return texts[key] || '';
}

function updateTable(exams, length) {
    const tbody = document.querySelector(".table-group-divider");
    tbody.innerHTML = exams.map(exam => `
        <tr>
            <td>${exam.departmentName}</td>
            <td>${exam.subjectName}</td>
            <td>${exam.grade} 학년</td>
            <td>${exam.examYear} 년도</td>
            <td>${exam.semester} 학기</td>
            <td>${getCategoryText(exam.examCategory)}</td>
            <td><button class="btn btn-primary">풀기</button></td>
        </tr>`).join('');
    document.getElementById("count").textContent = `총 ${length} 건`;
}

function getCategoryText(category) {
    const categories = {
        1: "기말",
        2: "계절학기(동계)",
        3: "계절학기(하계)"
    };
    return categories[category] || '';
}

function createPagination(totalPages, currentPage) {
    const pagination = document.getElementById("pagination");
    const paramArr = Array.from(document.querySelectorAll('select')).reduce((params, select) => {
        if (select.value) params[select.id] = select.value;
        return params;
    }, {});
    const paramString = Object.entries(paramArr).map(([key, value]) => `&${key}=${value}`).join('');
    const createPageItem = (text, page, isDisabled) => `
        <li class="page-item ${isDisabled ? 'disabled' : ''}">
            <a class="page-link" href="#" data-page="${page}">${text}</a>
        </li>`;

    const pageItems = [
        createPageItem("First", 1, currentPage === 1),
        createPageItem("Previous", currentPage - 1, currentPage === 1),
        ...Array.from({ length: totalPages }, (_, i) => createPageItem(i + 1, i + 1, false)).join(''),
        createPageItem("Next", currentPage + 1, currentPage === totalPages),
        createPageItem("Last", totalPages, currentPage === totalPages)
    ].join('');

    pagination.innerHTML = `<ul class="pagination" style="display: flex; justify-content: center;">${pageItems}</ul>`;

    pagination.querySelectorAll('a[data-page]').forEach(link => {
        link.addEventListener('click', function (event) {
            event.preventDefault();
            const page = this.getAttribute('data-page');
            fetch(`/exam?page=${page}${paramString}`)
                .then
                (response => response.json())
                .then(data => {
                    updateTable(data.exams, data.exams.length);
                    createPagination(data.totalPages, data.currentPage);
                })
                .catch(error => console.error("Error: ", error));
        });
    });
}
  • 가장 많은 시간과 많은 기능이 들어간 곳........
  • 검색조건을 동적으로 가져오면서 검색조건을 선택할 때마다 바로 하위의 검색조건이 열리고 나머지는 닫혀야 함.
  • 검색조건에 따른 조회결과가 하단에 바뀌어야 하면서 그 동시에 페이지네이션도 착착 되어야 함.
  • 이걸 다 하려니까......시간 제일 오래걸림. 그리고 하고나서 너무 중복 많아서 지피티의 힘을 빌려 한번 리팩토링 함....

 

 

[구현화면]

사실 맨 위의 화면과 같은 화면이지만;

암튼 구현 완.....후우....너무 힘들었다...조회 화면에 기능이 추가되지 않는 이상 더이상 손 안될 정도로 했다.....

728x90
320x100