티스토리 뷰

어찌저찌 페이지네이션만 빼고 검색 페이지에서 어느정도 구현했다...

 

삼촌의 도움이 가장 컸고 그걸 토대로 이것저것 수정해 나갔다.

 

우선 페이지네이션 빼고 프론트와 백의 코드다.

 

<백엔드>

// CourseListController

package ssac.LMS.controller;

import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
import ssac.LMS.domain.Course;
import ssac.LMS.domain.Enrollment;
import ssac.LMS.dto.CourseResponseDto;
import ssac.LMS.dto.CourseSearchResponseDto;
import ssac.LMS.dto.MyCourseResponseDto;
import ssac.LMS.service.CourseListService;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/course")
@RequiredArgsConstructor
@Slf4j
public class CourseListController {
    private final CourseListService courseListService;

    @Data
    static class Result<T> {
        private int count;
        private T data;

        public Result(int count, T data) {
            this.count = count;
            this.data = data;
        }
    }
    @GetMapping("/new")
    public ResponseEntity<Result> getCourseByNew() {
        log.info("new");
        List<Course> latestCourses = courseListService.getLatestCourses();

        List<CourseResponseDto> courseResponseDtoStream = latestCourses.stream()
                .map(m -> new CourseResponseDto(m.getTitle(), m.getDescription(), m.getStartedAt(), m.getPrice(),
                        m.getTags(), m.getThumbnailPath()))
                        .collect(Collectors.toList());
        return ResponseEntity.status(HttpServletResponse.SC_OK).body(new Result(courseResponseDtoStream.size(), courseResponseDtoStream));
    }

    @GetMapping("/best")
    public ResponseEntity<Result> getCourseByBest() {

        log.info("best");
        List<Course> bestCourses = courseListService.getCourseByBest();
        List<CourseResponseDto> courseResponseDtoStream = bestCourses.stream()
                .map(m -> new CourseResponseDto(m.getTitle(), m.getDescription(), m.getStartedAt(), m.getPrice(),
                        m.getTags(), m.getThumbnailPath()))
                .collect(Collectors.toList());
        return ResponseEntity.status(HttpServletResponse.SC_OK).body(new Result(courseResponseDtoStream.size(), courseResponseDtoStream));

    }

    @GetMapping("/{userId}")
    public ResponseEntity<?> getMyClass(@PathVariable String userId, @AuthenticationPrincipal Jwt jwt) {
        log.info("getUserId={}", userId);
        log.info("jwtUserId={}", jwt.getClaim("cognito:username").toString());

        if (!userId.equals(jwt.getClaim("cognito:username").toString())) {
            return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).body("Id가 맞지 않습니다.");
        }

        List<Enrollment> myClass = courseListService.getMyClass(userId);
        List<MyCourseResponseDto> myCourseResponseDto = myClass.stream()
                .map(m -> new MyCourseResponseDto(m.getCourse().getCourseId(), m.getCourse().getTitle(), m.getCourse().getUser().getUserName(), m.getEnrolledAt(), m.getCourse().getThumbnailPath()))
                .collect(Collectors.toList());
        return ResponseEntity.status(HttpServletResponse.SC_OK).body(new Result(myCourseResponseDto.size(), myCourseResponseDto));
    }

    @GetMapping("/search")
    public ResponseEntity<?> getSearchCourse(@RequestParam("keyword") String keyword) {
        List<Course> searchCourse = courseListService.getSearchCourse(keyword);

        // 각 강의 정보를 CourseSearchInfo로 변환하여 리스트에 추가
        List<CourseSearchResponseDto> responseDtoList = searchCourse.stream()
                .map(course -> new CourseSearchResponseDto(course.getTitle(), course.getDescription(), course.getCourseId(), course.getThumbnailPath(),  course.getTags(), course.getUser().getUserName()))
                .collect(Collectors.toList());

        return ResponseEntity.status(HttpStatus.OK).body(new Result(responseDtoList.size(), responseDtoList));
    }

    @GetMapping("/all")
    public ResponseEntity<?> getAllCourses() {
        List<Course> allCourses = courseListService.getAllCourses();

        List<CourseSearchResponseDto> responseDtoList = allCourses.stream()
                .map(course -> new CourseSearchResponseDto(course.getTitle(), course.getDescription(), course.getCourseId(), course.getThumbnailPath(),  course.getTags(), course.getUser().getUserName()))
                .collect(Collectors.toList());

        return ResponseEntity.status(HttpStatus.OK).body(new Result(responseDtoList.size(), responseDtoList));
    }

}

 

// CourseListService

package ssac.LMS.service;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import ssac.LMS.domain.Course;
import org.springframework.stereotype.Service;
import ssac.LMS.domain.Enrollment;
import ssac.LMS.domain.User;
import ssac.LMS.repository.CourseRepository;
import ssac.LMS.repository.EnrollmentRepository;
import ssac.LMS.repository.UserRepository;

import java.util.List;
import java.util.Optional;


@Service
@RequiredArgsConstructor
@Slf4j
public class CourseListService {

    private final CourseRepository courseRepository;
    private final EnrollmentRepository enrollmentRepository;
    private final EntityManager entityManager;
    private final UserRepository userRepository;

    public List<Course> getLatestCourses() {
        Sort sort = Sort.by(Sort.Direction.DESC, "startedAt");
        Pageable pageable = PageRequest.of(0, 5, sort);

        // 최신순으로 정렬된 최신 강의 목록을 가져옵니다.
        List<Course> latestCourses = courseRepository.findAll(pageable).getContent();
        return latestCourses;
    }

    public List<Course> getCourseByBest() {
        List<Course> bestCourses = enrollmentRepository.findTop5CoursesByEnrollmentCount();
        return bestCourses;
    }

    public List<Enrollment> getMyClass(String userId) {
        Optional<User> user = userRepository.findById(userId);
        List<Enrollment> EnrollmentByUser = enrollmentRepository.findByUser(user.get());
        return EnrollmentByUser;
    }

    public List<Course> getSearchCourse(String keyword) {
        List<Course> searchResult = courseRepository.findByKeyword("%" + keyword + "%");
        log.info("searchResult={}", searchResult);
        return searchResult;
    }

    public  List<Course> getAllCourses() {
        return courseRepository.findAll();
    }
}

 

// CourseRepository

package ssac.LMS.service;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import ssac.LMS.domain.Course;
import org.springframework.stereotype.Service;
import ssac.LMS.domain.Enrollment;
import ssac.LMS.domain.User;
import ssac.LMS.repository.CourseRepository;
import ssac.LMS.repository.EnrollmentRepository;
import ssac.LMS.repository.UserRepository;

import java.util.List;
import java.util.Optional;


@Service
@RequiredArgsConstructor
@Slf4j
public class CourseListService {

    private final CourseRepository courseRepository;
    private final EnrollmentRepository enrollmentRepository;
    private final EntityManager entityManager;
    private final UserRepository userRepository;

    public List<Course> getLatestCourses() {
        Sort sort = Sort.by(Sort.Direction.DESC, "startedAt");
        Pageable pageable = PageRequest.of(0, 5, sort);

        // 최신순으로 정렬된 최신 강의 목록을 가져옵니다.
        List<Course> latestCourses = courseRepository.findAll(pageable).getContent();
        return latestCourses;
    }

    public List<Course> getCourseByBest() {
        List<Course> bestCourses = enrollmentRepository.findTop5CoursesByEnrollmentCount();
        return bestCourses;
    }

    public List<Enrollment> getMyClass(String userId) {
        Optional<User> user = userRepository.findById(userId);
        List<Enrollment> EnrollmentByUser = enrollmentRepository.findByUser(user.get());
        return EnrollmentByUser;
    }

    public List<Course> getSearchCourse(String keyword) {
        List<Course> searchResult = courseRepository.findByKeyword("%" + keyword + "%");
        log.info("searchResult={}", searchResult);
        return searchResult;
    }

    public  List<Course> getAllCourses() {
        return courseRepository.findAll();
    }
}

 

 

 

<프론트엔드>

// SearchPage.tsx

import React, { ReactNode, useEffect, useState, useContext } from 'react';
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import Logger from 'console-log-level';
let log = Logger({ level: 'trace' });
import {
    Box,
    Container,
    Flex,
    SimpleGrid,
    Input,
    Button,
    IconButton,
    useColorModeValue,
    background,
} from '@chakra-ui/react';
import { SearchIcon } from '@chakra-ui/icons';
import { Text } from '@chakra-ui/react';
import axios from 'axios';
import ClassCard from '../../components/ClassCard';
import SectionTitle from '../../components/SectionTitle';
import ContentArea from '../../components/ContentArea';
import { API } from '../../../config';
import SearchBar from '../../components/SearchBar/SearchBar';

// 검색 정보에 대한 타입 정의
type CourseSearchInfo = {
    title: string;
    description: string;
    courseId: number;
    thumbnailPath: string;
    tags: string;
    userName: string;
};

const ClassCardList = ({ children }: { children: ReactNode }) => (
    <SimpleGrid columns={{ base: 1, sm: 2, md: 3 }} spacing={5} gridAutoRows={'1fr'} borderColor={'none'}>
        {children}
    </SimpleGrid>
);

function SearchPage() {
    const navigate = useNavigate();
    const [searchResults, setSearchResults] = useState<CourseSearchInfo[]>([]);
    // 검색 결과를 저장할 상태

    const location = useLocation();

    useEffect(() => {
      // search 속성에 접근하면 쿼리 스트링 값을 얻을 수 있다.
      const keyWord = decodeURI(location.search);
      console.log('keyWord===>', keyWord);
  
      // 검색어가 존재하는 경우에 API 경로에 쿼리 스트링으로 전달하여 fetch한다.
      const fetchLectures = async () => {
          try {
              const getValue = keyWord ? API.COURSE_LIST_BY_SEARCH + keyWord : API.COURSE_LIST_BY_ALL;
              console.log('getValue===>', getValue);
              const response = await axios.get(`${getValue}`);
              setSearchResults(response.data.data);
              console.log(searchResults);
          } catch (error) {
              console.error('검색 중 오류가 발생했습니다:', error);
          }
      };
  
      fetchLectures();
  }, [location]);
  

    return (
        <>
            <ContentArea>
                <Flex flexDirection={'column'} className={'content-wrapper'} p={4} width={'100%'} gap={4}>
                    <SearchBar placeholder="검색어를 입력하세요" purpose="search" />
                    {/* 검색 결과를 보여줄지 여부를 조건부 렌더링으로 설정 */}
                    {searchResults.length > 0 ? (
                        <Box>
                            <ClassCardList>
                                {searchResults.map((item, idx) => (
                                    <ClassCard
                                        key={idx}
                                        title={item.title}
                                        desc={item.description}
                                        onClick={() => navigate(`/class/${item.courseId}`)}
                                        imgSrc={item.thumbnailPath}
                                    />
                                ))}
                            </ClassCardList>
                            <Box mt={250} />
                        </Box>
                    ) : (
                        <Box>
                            <Text display="flex" 
                            color="white" 
                            justifyContent= "center"
                            alignItems="center"
                            height="50vh"
                            fontSize="4xl">검색 결과가 없습니다.</Text>
                           
                        </Box>
                    )}
                </Flex>
            </ContentArea>
        </>
    );
}

export default SearchPage;

 

// SearchBar.tsx

import React, { useState } from "react";
import { SearchIcon } from "@chakra-ui/icons"
import { useNavigate, useSearchParams } from "react-router-dom";
import { Box, Container, Flex, SimpleGrid, Input, Button, IconButton, useColorModeValue, background } from "@chakra-ui/react";

interface SearchBarPropsType {
  placeholder: string;
  purpose: string;
}

function SearchBar(props: SearchBarPropsType) {
  const [searchKeyWord, setSearchKeyWord] = useState("");

  // useSearchParams는 URL에 쿼리 스트링을 입력해준다.
  const [searchParams, setSearchParams] = useSearchParams();
  
  const navigate = useNavigate();

  const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearchKeyWord(e.target.value);
  };

  const searchSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    // 검색 키워드가 존재하는 경우에만 setState를 진행한다.
    if (!!searchKeyWord) {
      setSearchParams({
        keyword: searchKeyWord,
      });
    } else {
      // 검색 키워드가 존재하지 않는 경우, 쿼리 스트링이 없는 원래 URL을 보여주도록 navigate 처리한다.
      navigate(`${props.purpose === "search" ? "/search" : "/qa"}`);  // 이 부분 수정해야한다!!!!!!
    }
  };

  return (
    <form onSubmit={searchSubmitHandler}>
      <Box mx="auto" width="50%">
        <Flex align="center" justify="center">
          <input
            type="text"
            placeholder={props.placeholder}
            value={searchKeyWord}
            onChange={onChangeHandler}
            style={{ padding: "8px", fontSize: "16px", borderRadius: "999px", width: "100%", paddingLeft: "20px" }}
          />
          <Button 
            type="submit"
            borderRadius="full"
            bgColor={useColorModeValue("gray.200", "gray.700")}
            _hover={{ bgColor: useColorModeValue("gray.300", "gray.600")}}
            ml={2}
          >
            <SearchIcon />
          </Button>
        </Flex>
      </Box>
    </form>
    
  );
}

export default SearchBar;
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함