Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 실시간 검색어 반응 API 구현 #70

Merged
merged 3 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 33 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
# sijak-api

## Exception Code Number
|code|message|data|
|:--:|:-------:|:--:|
|OK|||
|0|success|Object|
|Lecture|||
|4000|크롤링 작업 도중 실패했습니다.|null|
|4001|강의 ID가 존재하지 않습니다.|null|
|User|||
|5000|로그인에 실패했습니다.|null|
|5001|유저 ID가 존재하지 않습니다.|null|
|5002|유저 email이 존재하지 않습니다.|null|
|5003|사용자 주소를 얻어오는 데 실패했습니다.|null|
|5004|해당 위도, 경도의 행정구역을 알 수 없습니다.|null|
|Nickname||
|6000|띄어쓰기 없이 2자 ~ 12자까지 가능해요.|null|
|6001|자음, 모음은 닉네임 설정 불가합니다.|null|
|6002|한글, 영문, 숫자만 입력해주세요.|null|
|6003|이미 사용중인 닉네임이에요.|null|
|Heart||
|7000|찜클래스 삭제에 실패했습니다.|null|
|7001|이미 찜을 해놓았습니다.|null|
| code |message|data|
|:-------:|:-------:|:--:|
| OK |||
| 0 |success|Object|
| Lecture |||
| 4000 |크롤링 작업 도중 실패했습니다.|null|
| 4001 |강의 ID가 존재하지 않습니다.|null|
| User |||
| 5000 |로그인에 실패했습니다.|null|
| 5001 |유저 ID가 존재하지 않습니다.|null|
| 5002 |유저 email이 존재하지 않습니다.|null|
| 5003 |사용자 주소를 얻어오는 데 실패했습니다.|null|
| 5004 |해당 위도, 경도의 행정구역을 알 수 없습니다.|null|
| 5005 |잘못된 ACCESS_JWT 서명입니다.|null|
| 5006 |만료된 ACCESS_JWT 토큰입니다.|null|
| 5007 |잘못된 REFRESH_JWT 서명입니다.|null|
| 5008 |만료된 REFRESH_JWT 토큰입니다.|null|
| 5009 |ACCESS_TOKEN: 유효한 자격증명을 제공하지 않습니다.|null|
| 5010 |REFRESH_TOKEN: 유효한 자격증명을 제공하지 않습니다.|null|
| 5011 |ACCESS_TOKEN: 일치하지 않습니다.|null|
| 5012 |REFRESH_TOKEN: 일치하지 않습니다.|null|
| 5013 |지원되지 않는 JWT 토큰입니다.|null|
| 5014 |JWT 토큰이 잘못되었습니다.|null|
| 5015 |인증되지 않은 사용자입니다.|null|
| 5016 |권한이 없는 사용자입니다.|null|
| Nickname |||
| 6000 |띄어쓰기 없이 2자 ~ 12자까지 가능해요.|null|
| 6001 |자음, 모음은 닉네임 설정 불가합니다.|null|
| 6002 |한글, 영문, 숫자만 입력해주세요.|null|
| 6003 |이미 사용중인 닉네임이에요.|null|
| Heart ||
| 7000 |찜클래스 삭제에 실패했습니다.|null|
| 7001 |이미 찜을 해놓았습니다.|null|
53 changes: 53 additions & 0 deletions src/main/generated/zerobase/sijak/persist/domain/QCategory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package zerobase.sijak.persist.domain;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.PathInits;


/**
* QCategory is a Querydsl query type for Category
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QCategory extends EntityPathBase<Category> {

private static final long serialVersionUID = -1853362359L;

private static final PathInits INITS = PathInits.DIRECT2;

public static final QCategory category = new QCategory("category");

public final NumberPath<Integer> id = createNumber("id", Integer.class);

public final QLecture lecture;

public final StringPath name = createString("name");

public QCategory(String variable) {
this(Category.class, forVariable(variable), INITS);
}

public QCategory(Path<? extends Category> path) {
this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
}

public QCategory(PathMetadata metadata) {
this(metadata, PathInits.getFor(metadata, INITS));
}

public QCategory(PathMetadata metadata, PathInits inits) {
this(Category.class, metadata, inits);
}

public QCategory(Class<? extends Category> type, PathMetadata metadata, PathInits inits) {
super(type, metadata, inits);
this.lecture = inits.isInitialized("lecture") ? new QLecture(forProperty("lecture")) : null;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class QLecture extends EntityPathBase<Lecture> {

public final NumberPath<Integer> capacity = createNumber("capacity", Integer.class);

public final ListPath<Category, QCategory> categories = this.<Category, QCategory>createList("categories", Category.class, QCategory.class, PathInits.DIRECT2);

public final StringPath category = createString("category");

public final StringPath centerName = createString("centerName");
Expand Down Expand Up @@ -72,6 +74,8 @@ public class QLecture extends EntityPathBase<Lecture> {

public final ListPath<Teacher, QTeacher> teachers = this.<Teacher, QTeacher>createList("teachers", Teacher.class, QTeacher.class, PathInits.DIRECT2);

public final StringPath tel = createString("tel");

public final StringPath textBookName = createString("textBookName");

public final StringPath textBookPrice = createString("textBookPrice");
Expand Down
19 changes: 15 additions & 4 deletions src/main/java/zerobase/sijak/controller/IndexController.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package zerobase.sijak.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import zerobase.sijak.dto.CommonResponse;
import zerobase.sijak.service.LectureService;

@Controller
@RestController
public class IndexController {

private final LectureService lectureService;

public IndexController(LectureService lectureService) {
this.lectureService = lectureService;
}

@GetMapping("/")
public String index() {
return "index";
}

@GetMapping("/api/seo")
public CommonResponse<?> seo() {
return CommonResponse.of(lectureService.getClassIds());
}

}
11 changes: 7 additions & 4 deletions src/main/java/zerobase/sijak/controller/SearchController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.*;
import zerobase.sijak.dto.CategoryRequest;
import zerobase.sijak.dto.CommonResponse;
import zerobase.sijak.dto.LectureHomeResponse;
import zerobase.sijak.dto.SearchRequest;
import zerobase.sijak.dto.*;
import zerobase.sijak.service.SearchService;

import java.util.HashMap;
Expand All @@ -23,6 +20,12 @@ public class SearchController {

private final SearchService searchService;

@PostMapping("/")
public CommonResponse<?> getMatchedKeyword(@RequestParam String keyword) {
SearchResponse searchResponse = searchService.getMatchedKeyword(keyword);
return CommonResponse.of(searchResponse);
}

@GetMapping("/keyword")
public CommonResponse<?> searchKeyword(@RequestHeader("Authorization") String token,
@RequestBody SearchRequest searchRequest,
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/zerobase/sijak/dto/SearchResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package zerobase.sijak.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SearchResponse {

@Builder.Default
List<String> names = new ArrayList<>();

}
26 changes: 26 additions & 0 deletions src/main/java/zerobase/sijak/persist/domain/Category.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package zerobase.sijak.persist.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Category {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "lecture_id")
@JsonIgnore
private Lecture lecture;

private String name;

}
7 changes: 5 additions & 2 deletions src/main/java/zerobase/sijak/persist/domain/Lecture.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import zerobase.sijak.dto.crawling.LectureCreateRequest;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

@Entity
Expand All @@ -32,6 +30,8 @@ public class Lecture {
@Size(max = 1000)
private String description;

private String tel;

private String price;

@OneToMany(mappedBy = "lecture")
Expand Down Expand Up @@ -117,4 +117,7 @@ public class Lecture {
@Builder.Default
private List<Educate> educates = new ArrayList<>();

@OneToMany(mappedBy = "lecture")
private List<Category> categories = new ArrayList<>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import org.springframework.stereotype.Repository;
import zerobase.sijak.persist.domain.Lecture;

import java.util.List;

@Repository
public interface LectureQueryDslRepository {

Slice<Lecture> searchKeywordAndLocation(Pageable pageable, String keyword, String location);

Slice<Lecture> searchCategoryAndLocation(Pageable pageable, String category, String location);

List<Lecture> searchNamesByPrefix(String keyword);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ Slice<Lecture> findLecturesByDistance(@Param("latitude") double latitude,
"GROUP BY l.address, l.latitude, l.longitude, l.centerName ")
List<Object[]> findGroupedLecturesByLocation();

List<Lecture> findAllByStatusTrue();

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zerobase.sijak.persist.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import io.micrometer.common.util.StringUtils;
import lombok.RequiredArgsConstructor;
Expand All @@ -9,6 +10,7 @@
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import zerobase.sijak.persist.domain.Lecture;
import zerobase.sijak.persist.domain.QCategory;
import zerobase.sijak.persist.domain.QLecture;

import java.util.List;
Expand Down Expand Up @@ -41,11 +43,13 @@ public Slice<Lecture> searchKeywordAndLocation(Pageable pageable, String keyword
}

@Override
public Slice<Lecture> searchCategoryAndLocation(Pageable pageable, String category, String location) {
public Slice<Lecture> searchCategoryAndLocation(Pageable pageable, String categoryName, String location) {
QLecture lecture = QLecture.lecture;
QCategory category = QCategory.category;

List<Lecture> lectures = jpaQueryFactory.selectFrom(lecture)
.where(eqCategory(category), eqLocation(location), eqStatusTrue())
.innerJoin(category).on(lecture.id.eq(category.lecture.id))
.where(eqCategory(categoryName), eqLocation(location), eqStatusTrue())
.orderBy(lecture.deadline.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
Expand All @@ -61,9 +65,27 @@ public Slice<Lecture> searchCategoryAndLocation(Pageable pageable, String catego
return new SliceImpl<>(lectures, pageable, hasNext);
}

@Override
public List<Lecture> searchNamesByPrefix(String keyword) {

QLecture lecture = QLecture.lecture;

return jpaQueryFactory.selectFrom(lecture)
.where(startsWithKeyword(keyword), eqStatusTrue())
.orderBy(lecture.deadline.asc())
.limit(9)
.fetch();
}

private BooleanExpression startsWithKeyword(String keyword) {
if (StringUtils.isBlank(keyword)) return Expressions.FALSE;
return QLecture.lecture.name.startsWith(keyword);
}

// 제목과 설명에 해당 keyword 가 포함되어있는 강좌를 모두 찾는다.
private BooleanExpression containsKeyword(String keyword) {
if (StringUtils.isBlank(keyword)) return null;
else if (keyword.contains("무료")) return QLecture.lecture.price.eq("0원");
return QLecture.lecture.name.contains(keyword)
.or(QLecture.lecture.description.contains(keyword));
}
Expand All @@ -77,7 +99,8 @@ private BooleanExpression eqLocation(String location) {
// 해당 카테고리와 일치하는 강좌를 모두 찾는다.
private BooleanExpression eqCategory(String category) {
if (StringUtils.isBlank(category)) return null;
return QLecture.lecture.category.eq(category);
else if (category.equals("무료")) return QLecture.lecture.price.eq("0원");
return QCategory.category.name.eq(category);
}

// 활성화된 강좌를 모두 찾는다.
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/zerobase/sijak/service/LectureService.java
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,15 @@ public List<MarkerResponse> getMarkerClass() {
return markerResponseList;
}

public List<Integer> getClassIds() {

List<Lecture> lectures = lectureRepository.findAllByStatusTrue();

return lectures.stream()
.map(Lecture::getId)
.toList();
}


private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
// 위도와 경도를 라디안 단위로 변환
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/zerobase/sijak/service/SearchService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import zerobase.sijak.dto.CategoryRequest;
import zerobase.sijak.dto.LectureHomeResponse;
import zerobase.sijak.dto.SearchRequest;
import zerobase.sijak.dto.SearchResponse;
import zerobase.sijak.exception.Code;
import zerobase.sijak.exception.CustomException;
import zerobase.sijak.jwt.JwtTokenProvider;
Expand Down Expand Up @@ -181,4 +182,17 @@ public Slice<LectureHomeResponse> searchCategory(String token, Pageable pageable
}
}

public SearchResponse getMatchedKeyword(String keyword) {

List<Lecture> lectures = lectureRepository.searchNamesByPrefix(keyword);

List<String> names = lectures.stream()
.map(Lecture::getName)
.toList();

return SearchResponse.builder()
.names(names)
.build();
}

}
Loading
Loading