티스토리 뷰
어찌저찌 페이지네이션만 빼고 검색 페이지에서 어느정도 구현했다...
삼촌의 도움이 가장 컸고 그걸 토대로 이것저것 수정해 나갔다.
우선 페이지네이션 빼고 프론트와 백의 코드다.
<백엔드>
// 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;
'SeSAC_도봉캠퍼스 > 새싹_도봉캠퍼스_프로젝트 4' 카테고리의 다른 글
2024.04.21_프로젝트 4 (20 일차) (1) | 2024.04.22 |
---|---|
2024.04.20_프로젝트 4 (19 일차) (0) | 2024.04.20 |
2024.04.19_프로젝트 4 (18 일차) (0) | 2024.04.19 |
2024.04.18_프로젝트 4 (17 일차) (0) | 2024.04.18 |
2024.04.17_프로젝트 4 (16 일차) (0) | 2024.04.18 |