Skip to content

Commit

Permalink
Merge pull request #26 from dnd-side-project/feat/#23
Browse files Browse the repository at this point in the history
[#23] 내 방문기록 조회 API 추가
  • Loading branch information
youngreal authored Sep 1, 2024
2 parents ce96c13 + a4bbf42 commit 4a4acad
Show file tree
Hide file tree
Showing 16 changed files with 366 additions and 44 deletions.
31 changes: 27 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,39 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'javax.xml.bind:jaxb-api:2.3.0'

//querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
}

tasks.named('test') {
test {
useJUnitPlatform()
}

// querydsl 설정부
def generated = layout.buildDirectory.dir("generated/querydsl").get().asFile

// java source set에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs generated
}

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile).configureEach {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}

clean.doLast {
file(generated).deleteDir()
}
21 changes: 21 additions & 0 deletions src/main/java/com/dnd/dndtravel/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dnd.dndtravel.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@Configuration
public class JpaConfig {

@PersistenceContext
private EntityManager em;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
28 changes: 21 additions & 7 deletions src/main/java/com/dnd/dndtravel/map/controller/MapController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.dnd.dndtravel.map.controller;


import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

Expand All @@ -11,13 +12,16 @@

import com.dnd.dndtravel.map.controller.request.RecordRequest;
import com.dnd.dndtravel.map.controller.request.validation.PhotoValidation;
import com.dnd.dndtravel.config.AuthenticationMember;
import com.dnd.dndtravel.map.service.MapService;
import com.dnd.dndtravel.map.service.dto.response.AttractionRecordResponse;
import com.dnd.dndtravel.map.service.dto.response.RegionResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@Validated
@RestController
@RequiredArgsConstructor
public class MapController {
Expand All @@ -27,17 +31,27 @@ public class MapController {
@Tag(name = "MAP", description = "지도 API")
@Operation(summary = "전체 지역 조회", description = "전체 지역 방문 횟수를 조회합니다.")
@GetMapping("/maps")
public RegionResponse map() {
Long memberId = 1L;
return mapService.allRegions(memberId);
public RegionResponse map(AuthenticationMember authenticationMember) {
return mapService.allRegions(authenticationMember.id());
}

@PostMapping("/maps/record")
public void memo(
@PhotoValidation @RequestPart("photos") List<MultipartFile> photos,
@RequestPart("recordRequest") RecordRequest recordRequest
@RequestPart("recordRequest") RecordRequest recordRequest,
AuthenticationMember authenticationMember
) {
mapService.recordAttraction(recordRequest.toDto(photos), authenticationMember.id());
}

//커서를 0으로 주면 최신 게시글을을 displayPerPage별로 조회함
//서버에서 클라에 마지막 게시글의 ID를 줘야함
@GetMapping("/maps/history")
public List<AttractionRecordResponse> findRecords(
@RequestParam long cursorNo,
@RequestParam(defaultValue = "10") int displayPerPage,
AuthenticationMember authenticationMember
) {
Long memberId = 1L;
mapService.recordAttraction(recordRequest.toDto(photos), memberId);
return mapService.allRecords(authenticationMember.id(), cursorNo, displayPerPage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,14 @@ public class MemberAttraction {
private String memo; // 방문기록 메모
private LocalDate localDate; // 방문 날짜
private String region; // 지역
private int photosCount; // 사진 개수, 필요한가?
//todo 명소 이름도 필요할지도?

@Builder
private MemberAttraction(Member member, Attraction attraction, String memo, LocalDate localDate, String region,
int photosCount) {
private MemberAttraction(Member member, Attraction attraction, String memo, LocalDate localDate, String region) {
this.member = member;
this.attraction = attraction;
this.memo = memo;
this.localDate = localDate;
this.region = region;
this.photosCount = photosCount;
}

public static MemberAttraction of(Member member, Attraction attraction, String memo, LocalDate localDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.dnd.dndtravel.map.repository;

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

import org.springframework.data.jpa.repository.JpaRepository;

import com.dnd.dndtravel.map.domain.MemberAttraction;
import com.dnd.dndtravel.map.repository.custom.MemberAttractionRepositoryCustom;

public interface MemberAttractionRepository extends JpaRepository<MemberAttraction, Long> {
MemberAttraction findByAttractionIdAndMemberId(Long attractionId, Long memberId);
public interface MemberAttractionRepository extends JpaRepository<MemberAttraction, Long>,
MemberAttractionRepositoryCustom {

List<MemberAttraction> findByMemberId(long memberId);
}

Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.dnd.dndtravel.map.repository;

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

import org.springframework.data.jpa.repository.JpaRepository;

import com.dnd.dndtravel.map.domain.Photo;
import com.dnd.dndtravel.map.repository.custom.PhotoRepositoryCustom;

public interface PhotoRepository extends JpaRepository<Photo, Long> {
public interface PhotoRepository extends JpaRepository<Photo, Long>, PhotoRepositoryCustom {

List<Photo> findByMemberAttractionId(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dnd.dndtravel.map.repository.custom;

import java.util.List;

import com.dnd.dndtravel.map.repository.dto.projection.AttractionPhotoProjection;
import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection;

public interface MemberAttractionRepositoryCustom {
Long maxCursor(long memberId);

List<RecordProjection> findAttractionRecords(long memberId, long cursorNo, int displayPerPage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.dnd.dndtravel.map.repository.custom;

import static com.dnd.dndtravel.map.domain.QAttraction.attraction;
import static com.dnd.dndtravel.map.domain.QMemberAttraction.memberAttraction;
import static com.dnd.dndtravel.member.domain.QMember.member;

import java.util.List;

import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class MemberAttractionRepositoryImpl implements MemberAttractionRepositoryCustom {

private final JPAQueryFactory jpaQueryFactory;
private final NumberPath<Long> entireRecordCount = Expressions.numberPath(Long.class, "entireRecordCount");

/**
* select max(ma.id)
* from memberAttraction ma
*/
@Override
public Long maxCursor(long memberId) {
return jpaQueryFactory.select(memberAttraction.id.max())
.from(memberAttraction)
.fetchOne();
}

/**
* select *
* from memberAttraction ma
* join member m on ma.memberId = m.id
* where {cursor.no} < ma.id
* order by ma.id desc
* limit {displayPerPage}
*/

@Override
public List<RecordProjection> findAttractionRecords(long memberId, long cursorNo, int displayPerPage) {
return jpaQueryFactory.select(Projections.constructor(RecordProjection.class,
memberAttraction.id,
ExpressionUtils.as(JPAExpressions
.select(memberAttraction.id.count())
.from(memberAttraction)
.where(memberAttraction.member.id.eq(member.id)), entireRecordCount
),
memberAttraction.memo,
memberAttraction.localDate,
memberAttraction.region,
attraction
))
.from(memberAttraction)
.join(member).on(memberAttraction.member.id.eq(member.id))
.join(attraction).on(memberAttraction.attraction.id.eq(attraction.id))
.where(memberAttraction.id.lt(cursorNo))
.orderBy(memberAttraction.id.desc())
.limit(displayPerPage)
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.dndtravel.map.repository.custom;

import java.util.List;

import com.dnd.dndtravel.map.repository.dto.projection.AttractionPhotoProjection;
import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection;

public interface PhotoRepositoryCustom {

List<AttractionPhotoProjection> findByRecordDtos(List<RecordProjection> dtos);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.dnd.dndtravel.map.repository.custom;

import static com.dnd.dndtravel.map.domain.QMemberAttraction.memberAttraction;
import static com.dnd.dndtravel.map.domain.QPhoto.photo;

import java.util.List;

import com.dnd.dndtravel.map.repository.dto.projection.AttractionPhotoProjection;
import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class PhotoRepositoryImpl implements PhotoRepositoryCustom {

private final JPAQueryFactory jpaQueryFactory;

@Override
public List<AttractionPhotoProjection> findByRecordDtos(List<RecordProjection> recordDtos) {
List<Long> memberAttractionIds = recordDtos.stream()
.map(RecordProjection::getMemberAttractionId)
.toList();

return jpaQueryFactory.select(
Projections.constructor(AttractionPhotoProjection.class,
memberAttraction.id,
photo.url))
.from(photo)
.join(photo.memberAttraction, memberAttraction)
.where(photo.memberAttraction.id.in(memberAttractionIds))
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dnd.dndtravel.map.repository.dto.projection;

public record AttractionPhotoProjection(
long memberAttractionId,
String photoUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.dnd.dndtravel.map.repository.dto.projection;

import java.time.LocalDate;
import java.util.List;
import java.util.Objects;

import com.dnd.dndtravel.map.domain.Attraction;

import lombok.Getter;

/**
* record로 설계하려 했으나, photoUrls를 뒤늦게 주입시켜줘야하는 상황이므로 class로 설계
*/
@Getter
public class RecordProjection {
private final long memberAttractionId;
private final long entireRecordCount;
private String attractionName;
private final String memo;
private final LocalDate visitDate;
private final String region;
private final Attraction attraction;
private List<String> photoUrls;

public RecordProjection(long memberAttractionId, long entireRecordCount, String memo,
LocalDate visitDate, String region, Attraction attraction) {
this.memberAttractionId = memberAttractionId;
this.entireRecordCount = entireRecordCount;
this.memo = memo;
this.visitDate = visitDate;
this.region = region;
this.attraction = attraction;
}

public void inputPhotoUrls(List<AttractionPhotoProjection> attractionPhotoProjections) {
if (attractionPhotoProjections != null) {
this.photoUrls = attractionPhotoProjections.stream()
.map(AttractionPhotoProjection::photoUrl)
.filter(Objects::nonNull)
.toList();
}
}

public void inputAttractionNames() {
this.attractionName = this.attraction.getName();
}
}
Loading

0 comments on commit 4a4acad

Please sign in to comment.