무한 스크롤 게시글 개발 과정을 소개한다.
- github: https://github.com/scribnote5/lab
github commit: https://github.com/scribnote5/lab/commit/2efc38ac4ddb4344d3537e7e478fbed9f9adefcd
- 최신 프로젝트 코드와 형상이 다를 수 있습니다. 게시글 코드는 참고만 하시되, 최신 코드는 github에서 확인 부탁드립니다.
무한 스크롤 게시판
- 페이스북의 타임 라인처럼 스크롤이 특정 위치에 도달하면 다음 데이터를 가져와 view에 출력하는 게시판으로서, 모바일 사용자에게 친화적인 UI를 제공한다. 특정 위치에 스크롤이 도달할 때 ajax를 통하여 데이터를 요청하는 과정이 필요하다.
- 리스트 페이지에서 논문 출판 지역(국내, 국제)과 논문 종류(Regular, Poster, Journal 등) 데이터를 검색하는 기능을 radio button으로 제공하며, 해당 기능은 QueryDsl의 동적쿼리(BooleanExpression)로 구현하였다.
- 무한 스크롤 게시판을 구현할 때 가장 중요한 것은 조회한 리스트의 마지막 요소의 idx(pk)다. 스크롤 이벤트가 발생할 때 조회하는 데이터의 시작을 리스트의 마지막 요소의 idx를 사용하여 알 수 있기 때문이다.
출처: https://victorydntmd.tistory.com/194
Table 설계
- 프로젝트에서 사용할 논문 게시판 table을 생성한다.
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
<publication table>
CREATE TABLE publication
(
idx bigint auto_increment primary key,
created_by varchar(255) null,
created_date datetime(6) null,
last_modified_by varchar(255) null,
last_modified_date datetime(6) null,
active_status varchar(255) null,
title varchar(255) null,
authors varchar(255) null,
publication_type varchar(255) null,
publishing_area varchar(255) null,
published_in varchar(255) null,
impact_factor varchar(255) null,
published_date datetime(6) null,
pages varchar(255) null,
volume varchar(255) null,
number varchar(255) null,
doi varchar(255) null,
uri varchar(255) null,
isbn_issn varchar(255) null,
remark varchar(255) null
);
- 프로젝트에서 사용할 논문 게시판 첨부 파일 table을 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<publication_attached_file table>
CREATE TABLE publication_attached_file (
idx bigint auto_increment primary key,
created_by varchar(255) null,
created_date datetime(6) null,
file_name varchar(255) null,
saved_file_name varchar(255) null,
publication_idx bigint null,
file_size varchar(255) null
);
ALTER TABLE publication_attached_file AUTO_INCREMENT=1;
DROP TABLE publication_attached_file;
Config
- View에서 전달한 문자열을 enum 자료형으로 매핑하는 converter을 사용하여, 논문 검색 조건을 문자열 대신 PublicationSearchType enum 자료형으로 받으려고 한다.
- 해당 클래스는 view에서 전달한 문자열을 enum 자료형으로 바인딩하는 사용자 정의 converter다.(View String type -> Controller enum type)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<module-domain-core/src/main/java/kr/ac/univ/common/converter/StringToPublicationSearchType.java>
package kr.ac.univ.common.converter;
import kr.ac.univ.publication.dto.enums.PublicationSearchType;
import org.springframework.core.convert.converter.Converter;
public class StringToPublicationSearchType implements Converter<String, PublicationSearchType> {
@Override
public PublicationSearchType convert(String source) {
return PublicationSearchType.valueOf(source.toUpperCase());
}
}
- 앞에서 구현한 사용자 정의 StringToPublicationSearchType 클래스(converter)를 module-app-web 모듈과 module-app-api 모듈의 WebConfig 설정 파일에 등록한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<module-app-api/src/main/java/kr/ac/univ/config/WebConfig.java>
package kr.ac.univ.config;
import kr.ac.univ.common.converter.StringToPublicationSearchType;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// PublicationSearchType
registry.addConverter(new StringToPublicationSearchType());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<module-app-web/src/main/java/kr/ac/univ/config/WebConfig.java>
package kr.ac.univ.config;
import kr.ac.univ.common.converter.StringToPublicationSearchType;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// PublicationSearchType
registry.addConverter(new StringToPublicationSearchType());
}
}
Domain 및 DTO
- Publication에서 사용하는 Domain과 Domain의 enum 자료형이다.
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
<module-domain-core/src/main/java/kr/ac/univ/publication/domain/enums/PublicationType.java>
package kr.ac.univ.publication.domain.enums;
public enum PublicationType {
JOURNAL("Journal"),
CONFERENCE("Conference"),
// Journal은 KCI, SCOPUS, SCIE로 구분된다.
JOURNAL_KCI("Journal - KCI"),
JOURNAL_SCOPUS("Journal - SCOPUS"),
JOURNAL_SCIE("Journal - SCIE"),
// Conference는 Poster, Regular, Demo, Workshop으로 구분된다.
CONFERENCE_POSTER("Conference - Poster"),
CONFERENCE_REGULAR("Conference - Regular"),
CONFERENCE_DEMO("Conference - Demo"),
CONFERENCE_WORKSHOP("Conference - Workshop");
private String publicationType;
private PublicationType(String publicationType) {
this.publicationType = publicationType;
}
public String getPublicationType() {
return this.publicationType;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<module-domain-core/src/main/java/kr/ac/univ/publication/domain/enums/PublishingArea.java>
package kr.ac.univ.publication.domain.enums;
public enum PublishingArea {
INTERNATIONAL("International"),
DOMESTIC("Domestic");
private String publishingArea;
private PublishingArea(String publishingArea) {
this.publishingArea = publishingArea;
}
public String getPublishingArea() {
return this.publishingArea;
}
}
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<module-domain-core/src/main/java/kr/ac/univ/publication/domain/Publication.java>
package kr.ac.univ.publication.domain;
import java.time.LocalDate;
import javax.persistence.*;
import kr.ac.univ.common.domain.CommonAudit;
import kr.ac.univ.common.domain.enums.ActiveStatus;
import kr.ac.univ.publication.domain.enums.PublicationType;
import kr.ac.univ.publication.domain.enums.PublishingArea;
import kr.ac.univ.publication.listener.PublicationListener;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@NoArgsConstructor
@Entity
@Table
@ToString
@EntityListeners(PublicationListener.class)
public class Publication extends CommonAudit {
private String title;
private String authors;
@Enumerated(EnumType.STRING)
private PublicationType publicationType;
@Enumerated(EnumType.STRING)
private PublishingArea publishingArea;
private String publishedIn;
private String impactFactor;
private LocalDate publishedDate;
private String pages;
private String volume;
private String number;
private String doi;
private String uri;
private String isbnIssn;
private String remark;
@Builder
public Publication(Long idx, String createdBy, String lastModifiedBy, String title, ActiveStatus activeStatus,
String authors, PublicationType publicationType, PublishingArea publishingArea,
String publishedIn, String impactFactor, LocalDate publishedDate, String pages, String volume,
String number, String doi, String uri, String isbnIssn, String remark) {
setIdx(idx);
setCreatedBy(createdBy);
setLastModifiedBy(lastModifiedBy);
setActiveStatus(activeStatus);
this.title = title;
this.authors = authors;
this.publicationType = publicationType;
this.publishingArea = publishingArea;
this.publishedIn = publishedIn;
this.impactFactor = impactFactor;
this.publishedDate = publishedDate;
this.pages = pages;
this.volume = volume;
this.number = number;
this.doi = doi;
this.uri = uri;
this.isbnIssn = isbnIssn;
this.remark = remark;
}
public void update(Publication publication) {
setActiveStatus(publication.getActiveStatus());
this.title = publication.getTitle();
this.authors = publication.getAuthors();
this.publicationType = publication.getPublicationType();
this.publishingArea = publication.getPublishingArea();
this.publishedIn = publication.getPublishedIn();
this.impactFactor = publication.getImpactFactor();
this.publishedDate = publication.getPublishedDate();
this.pages = publication.getPages();
this.volume = publication.getVolume();
this.number = publication.getNumber();
this.doi = publication.getDoi();
this.uri = publication.getUri();
this.isbnIssn = publication.getIsbnIssn();
this.remark = publication.getRemark();
}
}
- NoticeBoard DTO <-> Entity간 객체 mapping 소스 코드가 Mapstruct에 의해 생성되도록 메소드를 선언 및 정의하는 클래스다.
- default 메소드는 사용자가 정의한 메소드로, Entity 파일 리스트를 DTO의 파일 리스트로 매핑한다.
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
<module-domain-core/src/main/java/kr/ac/univ/publication/dto/mapper/PublicationMapper.java>
package kr.ac.univ.publication.dto.mapper;
import kr.ac.univ.common.dto.mapper.EntityMapper;
import kr.ac.univ.publication.domain.Publication;
import kr.ac.univ.publication.domain.PublicationAttachedFile;
import kr.ac.univ.publication.dto.PublicationDto;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper(componentModel = "spring")
public interface PublicationMapper extends EntityMapper<PublicationDto, Publication> {
PublicationMapper INSTANCE = Mappers.getMapper(PublicationMapper.class);
default PublicationDto toDto(PublicationDto publicationDto, List<PublicationAttachedFile> attachedFileList) {
for (PublicationAttachedFile attachedFile : attachedFileList) {
publicationDto.getAttachedFileList().add(attachedFile);
}
return publicationDto;
}
}
- Publication에서 사용하는 DTO다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
<module-domain-core/src/main/java/kr/ac/univ/publication/dto/PublicationDto.java>
package kr.ac.univ.publication.dto;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import kr.ac.univ.common.dto.CommonDto;
import kr.ac.univ.publication.domain.PublicationAttachedFile;
import kr.ac.univ.publication.domain.enums.PublicationType;
import kr.ac.univ.publication.domain.enums.PublishingArea;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@NoArgsConstructor
@ToString
public class PublicationDto extends CommonDto {
/* CommonDto: JPA Audit */
/* 기본 정보 */
private String title;
private String authors;
private PublicationType publicationType;
private PublishingArea publishingArea;
private String publishedIn;
private String impactFactor;
private LocalDate publishedDate;
private String pages;
private String volume;
private String number;
private String doi;
private String uri;
private String isbnIssn;
private String remark;
/* 첨부 파일 */
private List<PublicationAttachedFile> attachedFileList = new ArrayList<PublicationAttachedFile>();
}
- PublicationSearchType enum 자료형은 검색에서 사용되며, View String type -> Controller enum type로 앞에서 등록한 사용자 정의 converter에 의해 변환된다.
- Publication domain의 논문 종류(PublicationType enum 자료형)와 논문 출판 지역(PublishingArea enum 자료형)의 모든 경우의 수를 하나의 enum 자료형으로 정의하였다.
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
31
32
33
34
35
36
37
38
39
40
41
<module-domain-core/src/main/java/kr/ac/univ/publication/dto/enums/PublicationSearchType.java>
package kr.ac.univ.publication.dto.enums;
public enum PublicationSearchType {
SHOW_ALL("Show All"),
// International Journal은 KCI, SCOPUS, SCIE로 구분된다.
INTERNATIONAL_JOURNAL("International Journal"),
INTERNATIONAL_JOURNAL_SCOPUS("International Journal - SCOPUS"),
INTERNATIONAL_JOURNAL_SCIE("International Journal - SCIE"),
// Conference는 Poster, Regular, Demo, Workshop으로 구분된다.
INTERNATIONAL_CONFERENCE("International Conference"),
INTERNATIONAL_POSTER("International Conference - Poster"),
INTERNATIONAL_REGULAR("International Conference - Regular"),
INTERNATIONAL_DEMO("International Conference - Demo"),
INTERNATIONAL_WORKSHOP("International Conference - Workshop"),
INTERNATIONAL_WORKINPROCESSS("International Conference - Work In Process"),
// Domestic Journal은 KCI로 구분된다.
DOMESTIC_JOURNAL("Domestic Journal"),
DOMESTIC_JOURNAL_KCI("International Journal - KCI"),
// Conference는 Poster, Regular, Demo, Workshop으로 구분된다.
DOMESTIC_CONFERENCE("Domestic Conference - Conference"),
DOMESTIC_POSTER("Domestic Conference - Poster"),
DOMESTIC_REGULAR("Domestic Conference - Regular"),
DOMESTIC_DEMO("Domestic Conference - Demo"),
DOMESTIC_WORKSHOP("Domestic Conference - Workshop");
private String publicationSearchType;
private PublicationSearchType(String publicationSearchType) {
this.publicationSearchType = publicationSearchType;
}
public String getSearchPublicationType() {
return this.publicationSearchType;
}
}
- Publication 검색에서 사용하는 DTO다.
- 검색 종류, 검색 키워드 데이터가 있는 SearchDto를 상속 받았으며, publicationSearchType 멤버 필드는 논문 출판 지역과 논문 종류를 정의한 enum 자료형이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<module-domain-core/src/main/java/kr/ac/univ/publication/dto/PublicationSearchDto.java>
package kr.ac.univ.publication.dto;
import kr.ac.univ.common.dto.SearchDto;
import kr.ac.univ.publication.dto.enums.PublicationSearchType;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@NoArgsConstructor
@ToString
public class PublicationSearchDto extends SearchDto {
/* 검색 정보 */
private PublicationSearchType publicationSearchType = PublicationSearchType.SHOW_ALL;
}
Repository
- QueryDsl를 사용하여 다음과 같은 쿼리를 작성하였다.
- findTop10: lastIdx(조회한 리스트의 마지막 요소의 idx)보다 작거나 같은 데이터를 10개 조회한다.(테스트 용도)
- findMaxPublicationIdx: 논문 idx의 최대값을 조회한다.
- findTop15ByPublicationSearchDto: lastIdx(조회한 리스트의 마지막 요소의 idx)보다 작거나 같고 PublicationSearchDto를 비교하여 조건에 일치하는 리스트를 15개 조회한다. QueryDsl은 BooleanExpression을 통하여 동적 쿼리를 사용할 수 있으며, 해당 쿼리에 적용된 동적 조건은 2가지다. eqSearchType는 검색 종류와 검색 키워드를, eqPublicationSearchType는 논문 출판 지역과 논문 종류를 각 조건을 판별한다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<module-domain-core/src/main/java/kr/ac/univ/publication/repository/PublicationRepositoryImpl.java>
package kr.ac.univ.publication.repository;
import java.util.List;
import javax.transaction.Transactional;
import com.querydsl.core.types.dsl.BooleanExpression;
import kr.ac.univ.publication.domain.Publication;
import kr.ac.univ.publication.domain.QPublication;
import kr.ac.univ.publication.domain.enums.PublicationType;
import kr.ac.univ.publication.domain.enums.PublishingArea;
import kr.ac.univ.publication.dto.PublicationSearchDto;
import kr.ac.univ.publication.dto.enums.PublicationSearchType;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;
import static kr.ac.univ.publication.domain.QPublication.publication;
import com.querydsl.jpa.impl.JPAQueryFactory;
@Repository
@Transactional
public class PublicationRepositoryImpl extends QuerydslRepositorySupport {
private final JPAQueryFactory queryFactory;
public PublicationRepositoryImpl(JPAQueryFactory queryFactory) {
super(Publication.class);
this.queryFactory = queryFactory;
}
public List<Publication> findTop10(Long lastIdx) {
/* SELECT *
* FROM publication
* WHERE idx < 'lastIdx'
* LIMIT 10
*/
return queryFactory
.selectFrom(publication)
.where(publication.idx.loe(lastIdx))
.orderBy(publication.idx.desc())
.limit(10)
.fetch();
}
public Publication findMaxPublicationIdx() {
/*
* SELECT MAX(idx)
* FROM publication
*/
return queryFactory
.selectFrom(publication)
.orderBy(publication.idx.desc())
.fetchFirst();
}
private BooleanExpression eqSearchType(PublicationSearchDto publicationSearchDto) {
/*
* SELECT *
* FROM publication
* WHERE searchType LIKE '%keyword%';
*/
BooleanExpression result = null;
switch (publicationSearchDto.getSearchType()) {
case "TITLE":
result = publication.title.contains(publicationSearchDto.getKeyword());
break;
case "AUTHORS":
result = publication.authors.contains(publicationSearchDto.getKeyword());
break;
case "PUBLISHED_IN":
result = publication.publishedIn.contains(publicationSearchDto.getKeyword());
break;
default:
break;
}
return result;
}
private BooleanExpression eqPublicationSearchType(PublicationSearchDto publicationSearchDto) {
/*
* SELECT *
* FROM publication
* WHERE publishing_area = 'publishingArea'
* AND publication_type = 'publicationType'
*/
BooleanExpression result = null;
if(publicationSearchDto.getPublicationSearchType() == PublicationSearchType.INTERNATIONAL_JOURNAL) {
result = publication.publishingArea.eq(PublishingArea.INTERNATIONAL).and(publication.publicationType.eq(PublicationType.JOURNAL));
}
else if (publicationSearchDto.getPublicationSearchType() == PublicationSearchType.INTERNATIONAL_CONFERENCE){
result = publication.publishingArea.eq(PublishingArea.INTERNATIONAL).and(publication.publicationType.ne(PublicationType.JOURNAL));
}
else if (publicationSearchDto.getPublicationSearchType() == PublicationSearchType.DOMESTIC_JOURNAL){
result = publication.publishingArea.eq(PublishingArea.DOMESTIC).and(publication.publicationType.eq(PublicationType.JOURNAL));
}
else if (publicationSearchDto.getPublicationSearchType() == PublicationSearchType.DOMESTIC_CONFERENCE){
result = publication.publishingArea.eq(PublishingArea.DOMESTIC).and(publication.publicationType.ne(PublicationType.JOURNAL));
}
return result;
}
public List<Publication> findTop10ByPublicationSearchDto(Long lastIdx, PublicationSearchDto publicationSearchDto) {
QPublication publication = QPublication.publication;
/*
* SELECT *
* FROM publication
* WHERE idx <= lastIdx
* AND publishing_area = 'INTERNATIONAL'
* AND publication_type = 'JOURNAL'
* AND title LIKE '%'
* LIMIT 10;
*/
return queryFactory
.selectFrom(publication)
.where(publication.idx.loe(lastIdx),
eqSearchType(publicationSearchDto),
eqPublicationSearchType(publicationSearchDto))
.orderBy(publication.idx.desc())
.limit(10)
.fetch();
}
}
Service
- Publication의 비즈니스 로직이다.
- findPublicationListScroll: 무한 스크롤 게시판에서 스크롤 이벤트가 발생할 때 사용된다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<module-domain-core/src/main/java/kr/ac/univ/publication/service/PublicationService.java>
package kr.ac.univ.publication.service;
import java.util.List;
import kr.ac.univ.common.dto.SearchDto;
import kr.ac.univ.noticeBoard.domain.NoticeBoard;
import kr.ac.univ.noticeBoard.dto.NoticeBoardDto;
import kr.ac.univ.noticeBoard.dto.mapper.NoticeBoardMapper;
import kr.ac.univ.publication.domain.Publication;
import kr.ac.univ.publication.domain.enums.PublicationType;
import kr.ac.univ.publication.domain.enums.PublishingArea;
import kr.ac.univ.publication.dto.PublicationDto;
import kr.ac.univ.publication.dto.PublicationSearchDto;
import kr.ac.univ.publication.dto.enums.PublicationSearchType;
import kr.ac.univ.publication.dto.mapper.PublicationMapper;
import kr.ac.univ.publication.repository.PublicationRepository;
import kr.ac.univ.publication.repository.PublicationRepositoryImpl;
import kr.ac.univ.user.repository.UserRepository;
import kr.ac.univ.util.AccessCheck;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@Service
public class PublicationService {
private final PublicationRepository publicationRepository;
private final PublicationRepositoryImpl publicationRepositoryImpl;
private final UserRepository userRepository;
public PublicationService(PublicationRepository publicationRepository, PublicationRepositoryImpl publicationRepositoryImpl, UserRepository userRepository) {
this.publicationRepository = publicationRepository;
this.publicationRepositoryImpl = publicationRepositoryImpl;
this.userRepository = userRepository;
}
public Page<PublicationDto> findPublicationList(Pageable pageable, PublicationSearchDto publicationSearchDto) {
Page<Publication> publicationList = null;
Page<PublicationDto> publicationDtoList = null;
pageable = PageRequest.of(pageable.getPageNumber() <= 0 ? 0 : pageable.getPageNumber() - 1, pageable.getPageSize(), Sort.Direction.DESC, "idx");
if("Show All".equals(publicationSearchDto.getPublicationSearchType().getSearchPublicationType())) {
switch (publicationSearchDto.getSearchType()) {
case "TITLE":
publicationList = publicationRepository.findAllByTitleContaining(pageable, publicationSearchDto.getKeyword());
break;
case "AUTHORS":
publicationList = publicationRepository.findAllByAuthorsContaining(pageable, publicationSearchDto.getKeyword());
break;
case "PUBLISHED_IN":
publicationList = publicationRepository.findAllByPublishedInContaining(pageable, publicationSearchDto.getKeyword());
break;
default:
publicationList = publicationRepository.findAll(pageable);
break;
}
} else {
String[] str = publicationSearchDto.getPublicationSearchType().getSearchPublicationType().split(" ");
PublishingArea publishingArea = PublishingArea.valueOf(str[0].toUpperCase());
PublicationType publicationType = PublicationType.valueOf(str[1].toUpperCase());
switch (publicationSearchDto.getSearchType()) {
case "TITLE":
publicationList = publicationRepository.findAllByTitleContainingAndPublicationTypeAndPublishingArea(pageable, publicationSearchDto.getKeyword(), publicationType, publishingArea);
break;
case "AUTHORS":
publicationList = publicationRepository.findAllByAuthorsContainingAndPublicationTypeAndPublishingArea(pageable, publicationSearchDto.getKeyword(), publicationType, publishingArea);
break;
case "PUBLISHED_IN":
publicationList = publicationRepository.findAllByPublishedInContainingAndPublicationTypeAndPublishingArea(pageable, publicationSearchDto.getKeyword(), publicationType, publishingArea);
break;
default:
publicationList = publicationRepository.findAllByPublicationTypeAndPublishingArea(pageable, publicationType, publishingArea);
break;
}
}
publicationDtoList = new PageImpl<PublicationDto>(PublicationMapper.INSTANCE.toDto(publicationList.getContent()), pageable, publicationList.getTotalElements());
return publicationDtoList;
}
public List<PublicationDto> findPublicationListScroll(Long lastIdx, PublicationSearchDto publicationSearchDto) {
List<Publication> publicationList = null;
List<PublicationDto> publicationDtoList = null;
publicationList = publicationRepositoryImpl.findTop10ByPublicationSearchDto(lastIdx, publicationSearchDto);
// Publication -> PublicationDto
publicationDtoList = PublicationMapper.INSTANCE.toDto(publicationList);
return publicationDtoList;
}
public Long insertPublication(Publication publication) {
return publicationRepository.save(publication).getIdx();
}
public PublicationDto findPublicationByIdx(Long idx) {
PublicationDto publicationDto = PublicationMapper.INSTANCE.toDto(publicationRepository.findById(idx).orElse(new Publication()));
// 권한 설정
// Register: 로그인한 사용자 Register 접근 가능
if (idx == 0) {
publicationDto.setAccess(true);
}
// Update: isAccess 메소드에 따라 접근 가능 및 불가
else if (AccessCheck.isAccess(publicationDto.getCreatedBy(), userRepository.findByUsername(publicationDto.getCreatedBy()).getAuthorityType().getAuthorityType())) {
publicationDto.setAccess(true);
} else {
publicationDto.setAccess(false);
}
return publicationDto;
}
@Transactional
public Long updatePublication(Long idx, PublicationDto publicationDto) {
Publication persistPublication = publicationRepository.getOne(idx);
Publication publication = PublicationMapper.INSTANCE.toEntity(publicationDto);
persistPublication.update(publication);
return publicationRepository.save(publication).getIdx();
}
public Long findMaxPublicationIdx() {
return publicationRepositoryImpl.findMaxPublicationIdx().getIdx();
}
public void deletePublicationByIdx(Long idx) {
publicationRepository.deleteById(idx);
}
}
Controller
- Publication 관련 클라이언트의 요청을 view로 매핑한다.
- publicationHomeList: 무한 스크롤 게시판에 접근할 때 응답한다.
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
31
32
33
34
35
36
37
38
39
40
41
<module-app-web/src/main/java/kr/ac/univ/controller/PublicationController.java>
package kr.ac.univ.controller;
import kr.ac.univ.publication.dto.PublicationDto;
import kr.ac.univ.publication.dto.PublicationSearchDto;
import kr.ac.univ.publication.service.PublicationAttachedFileService;
import kr.ac.univ.publication.service.PublicationService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/publication")
public class PublicationController {
private final PublicationService publicationService;
private final PublicationAttachedFileService publicationAttachedFileService;
public PublicationController(PublicationService publicationService, PublicationAttachedFileService publicationAttachedFileService) {
this.publicationService = publicationService;
this.publicationAttachedFileService = publicationAttachedFileService;
}
...
// List Scroll
@GetMapping("/list_scroll")
public String publicationHomeList(PublicationSearchDto publicationSearchDto,
Model model) {
// 방어 코드: lastIdx는 충분히 큰 값을 전달하면 된다.
Long lastIdx = publicationService.findMaxPublicationIdx();
model.addAttribute("publicationDtoList", publicationService.findPublicationListScroll(lastIdx, publicationSearchDto));
return "/publication/list_scroll";
}
}
RestController
- Publication 관련 클라이언트의 요청을 json 타입으로 응답한다.
- publicationListScroll: 스크롤 이벤트가 발생할 때 응답하며, lastIdx(조회한 리스트의 마지막 요소의 idx)를 기준으로 publication 데이터를 검색한다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<module-app-api/src/main/java/kr/ac/univ/controller/PublicationRestController.java>
package kr.ac.univ.controller;
import kr.ac.univ.publication.domain.Publication;
import kr.ac.univ.publication.dto.PublicationDto;
import kr.ac.univ.publication.dto.PublicationSearchDto;
import kr.ac.univ.publication.service.PublicationAttachedFileService;
import kr.ac.univ.publication.service.PublicationService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@RequestMapping("/api/publications")
public class PublicationRestController {
private final PublicationService publicationService;
private final PublicationAttachedFileService publicationAttachedFileService;
public PublicationRestController(PublicationService publicationService, PublicationAttachedFileService publicationAttachedFileService) {
this.publicationService = publicationService;
this.publicationAttachedFileService = publicationAttachedFileService;
}
...
// List Scroll
@GetMapping("/list_scroll")
public ResponseEntity<?> publicationListScroll(@RequestParam(value = "lastIdx", defaultValue = "-1") Long lastIdx,
PublicationSearchDto publicationSearchDto,
Model model) {
// 방어 코드: lastIdx가 들어오지 않은 경우 충분히 큰 값을 전달하면 된다.
if (lastIdx == -1L) {
lastIdx = publicationService.findMaxPublicationIdx();
}
return new ResponseEntity<>(publicationService.findPublicationListScroll(lastIdx, publicationSearchDto), HttpStatus.OK);
}
}
View
- Publication 관련 데이터를 화면에 출력한다.
- 해당 페이지에 접근하면 조회한 리스트의 마지막 요소의 idx(pk)를 계산한다.
- 이후 스크롤을 화면 가장 하단 - 100 높이만큼 이동시키면 스크롤 이벤트가 발생하며, 요청 URI는 util 함수인 getUriParams 함수(쿼리 스트링의 파라미터를 분리)와 makeGetUri 함수(URI 생성)를 사용하여 생성한다. 생성된 URI는 ajax를 통하여 서버에 요청 후 응답받은 데이터 리스트의 마지막 요소의 idx(pk)를 계산한 다음 데이터 리스트를 화면에 출력한다.
- 데이터 조회에 성공하는 경우 loading bar는 hide되고, 조회된 데이터를 포맷에 맞게 파싱하여 출력한다.
- 무한 스크롤 이벤트 발생 자바스크립트 소스 코드는 하단 출처를 참고하였다.
출처: https://c10106.tistory.com/4173
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
<module-app-web/src/main/resources/templates/publication/list_scroll.html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- css -->
<th:block th:replace="layout/css.html"></th:block>
<title th:text="${@environment.getProperty('title')} + ' | Publication List'"></title>
</head>
<body>
<div id="wrapper">
<div id="page-content-wrapper">
<!-- header -->
<div th:replace="layout/header::header"></div>
<div class="container publication">
<form name="form" id="form" th:object="${publicationSearchDto}" action="#">
<div class="page-header breadcrumbs">
<div class="d-flex justify-content-between align-items-center" data-aos="fade-up">
<h2>Publication List</h2>
<ol>
<li>Home</li>
<li>Publication List</li>
</ol>
</div>
</div>
<div id="loading">
<img th:src="@{/images/loading.gif}" alt="Loading…"/>
</div>
<div class="page-search" data-aos="fade-up">
<div class="row justify-content-end mt-4">
<div class="custom-control custom-radio col-auto">
<input type="radio" class="custom-control-input" name="publicationSearchType" id="showAll"
th:value="SHOW_ALL"
th:checked="*{publicationSearchType?.name() == 'SHOW_ALL' || publicationSearchType?.name() == null}">
<label class="custom-control-label" th:for="showAll"> Show All </label>
</div>
<div class="custom-control custom-radio col-auto">
<input type="radio" class="custom-control-input" name="publicationSearchType" id="internationalJournal"
th:value="INTERNATIONAL_JOURNAL"
th:checked="*{publicationSearchType?.name() == 'INTERNATIONAL_JOURNAL'}">
<label class="custom-control-label" th:for="internationalJournal"> International Journal </label>
</div>
<div class="custom-control custom-radio col-auto">
<input type="radio" class="custom-control-input" name="publicationSearchType"
id="internationalConference"
th:value="INTERNATIONAL_CONFERENCE"
th:checked="*{publicationSearchType?.name() == 'INTERNATIONAL_CONFERENCE'}">
<label class="custom-control-label" th:for="internationalConference"> International Conference </label>
</div>
<div class="custom-control custom-radio col-auto">
<input type="radio" class="custom-control-input" name="publicationSearchType" id="domesticJournal"
th:value="DOMESTIC_JOURNAL"
th:checked="*{publicationSearchType?.name() == 'DOMESTIC_JOURNAL'}">
<label class="custom-control-label" th:for="domesticJournal"> Domestic Journal </label>
</div>
<div class="custom-control custom-radio col-auto">
<input type="radio" class="custom-control-input" name="publicationSearchType" id="domesticConference"
th:value="DOMESTIC_CONFERENCE"
th:checked="*{publicationSearchType?.name() == 'DOMESTIC_CONFERENCE'}">
<label class="custom-control-label" th:for="domesticConference"> Domestic Conference </label>
</div>
</div>
<div class="row justify-content-end mt-2">
<div class="pt-2 pr-3">
<select class="custom-select custom-select-sm" name="searchType" th:field="*{searchType}">
<option th:value="TITLE">Title</option>
<option th:value="AUTHORS">Authors</option>
<option th:value="PUBLISHED_IN">ID</option>
</select>
</div>
<div class="pt-2 pr-2">
<input type="search" class="custom-search-input" name="keyword" placeholder="Search" th:value="${searchDto?.keyword}">
</div>
<div class="pt-2">
<i id="search" class="fas fa-search search-icon"></i>
</div>
</div>
</div>
<!-- Common -->
<div class="page-content" data-aos="fade-up">
<div id="publications" class="mt-4">
<div th:id="publicationData0" th:each="publicationDto : ${publicationDtoList}" class="publication-wrap mt-4">
<span class="my-1">
<a th:if="!${#strings.isEmpty(publicationDto.doi)}" th:href="'https://doi.org/' + ${publicationDto.doi}" th:id="publicationTitle" th:text="${publicationDto.title}" th:class="title" target="_blank"></a>
<a th:if="${#strings.isEmpty(publicationDto.doi) and !#strings.isEmpty(publicationDto.uri)}" th:href="${publicationDto.uri}" th:id="publicationTitle" th:text="${publicationDto.title}" th:class="title"
target="_blank"></a>
<span th:if="${#strings.isEmpty(publicationDto.doi) and #strings.isEmpty(publicationDto.uri)}" th:id="publicationTitle" th:text="${publicationDto.title}" th:class="title"></span><br>
</span>
<span th:text="${publicationDto.authors}" th:id="publicationAuthors" class="authors my-1"></span><br>
<span th:if="!${#strings.isEmpty(publicationDto.publishedIn)}" th:text="${publicationDto.publishedIn}"></span>
<span th:if="!${#strings.isEmpty(publicationDto.publishedDate)}" th:text="', ' + ${#temporals.format(publicationDto.publishedDate, 'MMM', new java.util.Locale('en', 'EN'))}"></span>
<span th:if="!${#strings.isEmpty(publicationDto.publishedDate)}" th:text="', ' + ${#temporals.format(publicationDto.publishedDate, 'yyyy')}"></span>
<span th:if="!${#strings.isEmpty(publicationDto.volume)}" th:text="', Vol. ' + ${publicationDto.volume} "></span>
<span th:if="!${#strings.isEmpty(publicationDto.number)}" th:text="', No. ' + ${publicationDto.number}"></span>
<span th:if="!${#strings.isEmpty(publicationDto.pages)}" th:text="', pp. ' + ${publicationDto.pages}"></span>
<span th:if="!${#strings.isEmpty(publicationDto.isbnIssn)}" th:text="', ' + ${publicationDto.isbnIssn}"></span>
<div class="mt-2">
<span th:if="${#strings.equals(publicationDto.publishingArea, 'INTERNATIONAL') && (#strings.equals(publicationDto.publicationType, 'JOURNAL_SCIE') || #strings.equals(publicationDto.publicationType, 'JOURNAL_SCOPUS'))}"
th:text="${publicationDto.publishingArea.publishingArea} + ' ' + ${publicationDto.publicationType.publicationType} + (${#strings.isEmpty(publicationDto.impactFactor)} ? '' : '(' + ${publicationDto.impactFactor} + ')')"
class="btn-primary btn-sm"></span>
<span th:if="${#strings.equals(publicationDto.publishingArea, 'INTERNATIONAL') && !(#strings.equals(publicationDto.publicationType, 'JOURNAL_SCIE') || #strings.equals(publicationDto.publicationType, 'JOURNAL_SCOPUS'))}"
th:text="${publicationDto.publishingArea.publishingArea} + ' ' + ${publicationDto.publicationType.publicationType}"
class="btn-info btn-sm"></span>
<span th:if="${#strings.equals(publicationDto.publishingArea, 'DOMESTIC') && #strings.equals(publicationDto.publicationType, 'JOURNAL')}"
th:text="${publicationDto.publishingArea.publishingArea} + ' ' + ${publicationDto.publicationType.publicationType} "
class="btn-success btn-sm"></span>
<span th:if="${#strings.equals(publicationDto.publishingArea, 'DOMESTIC') && !(#strings.equals(publicationDto.publicationType, 'JOURNAL'))}"
th:text="${publicationDto.publishingArea.publishingArea} + ' ' + ${publicationDto.publicationType.publicationType}"
class="btn-warning btn-sm"></span>
</div>
</div>
<div th:if="${#lists.size(publicationDtoList) == 0}">
<div class="no-posts">
No posts founded.
</div>
</div>
<div id="loadingScroll" class="row justify-content-center">
<img th:src="@{/images/loading.gif}" alt="Loading…"/>
</div>
</div>
<div class="row mt-4 mb-4"></div>
</div>
</form>
</div>
</div>
</div>
<!-- footer -->
<div th:replace="layout/footer::footer"></div>
<!-- script file -->
<th:block th:replace="layout/script.html"></th:block>
<script th:inline="javascript">
var list = ([[${publicationDtoList}]]);
var lastIdx = 0;
var isLast = false;
var uri = null;
// 조회하는 데이터가 10개 보다 적은 경우 loading.gif를 hide
if (list.length < 15) {
$("#loadingScroll").hide();
isLast = true;
}
// 처음 발생하는 scroll event에서 데이터를 가져오는 기준이 된다.(마지막 배열 요소의 idx)
else {
lastIdx = list[list.length - 1].idx;
}
$(document).ready(function () {
var win = $(window);
var publicationId = 1;
// Each time the user scrolls
win.scroll(function () {
if ($(window).scrollTop() + $(window).height() > $(document).height() - 100) {
// 더이상 가져오는 데이터가 없는 경우
if (isLast) {
$('#loadingScroll').hide();
return true;
}
$('#loadingScroll').show();
// 처음과 마지막 데이터가 중복되는 경우를 제거하기 위해서 lastIdx에서 1을 뺀다.
uri = {lastIdx: lastIdx - 1};
// URI 생성
Object.assign(uri, getUriParams());
uri = makeGetUri(moduleAppApiAddress + "/api/publications/list_scroll", uri);
$.ajax({
url: uri,
type: "get",
dataType: "text",
contentType: "application/json",
async: false,
})
.done(function (msg) {
var publicationList = JSON.parse(msg);
// 가져오는 데이터가 없는 경우
if (publicationList.length == 0) {
isLast = true;
return true;
}
// 다음 발생하는 scroll event에서 데이터를 가져오는 기준이 된다.(마지막 배열 요소의 idx)
lastIdx = (JSON.parse(msg))[(JSON.parse(msg)).length - 1].idx;
var monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
$("#loadingScroll").hide();
// ajax를 통하여 받은 데이터를 html에 출력한다.
for (var i = 0; i < publicationList.length; i++) {
var str = null;
var publishedDate = publicationList[i].publishedDate.split('-');
publishedDate[1] = monthNames[publishedDate[1] - 1];
if (publicationList[i].publishingArea == 'INTERNATIONAL' && (publicationList[i].publicationType == 'JOURNAL_SCIE' || publicationList[i].publicationType == 'JOURNAL_SCOPUS')) {
str = "<span class='btn-primary btn-sm'>" + capitalize(publicationList[i].publishingArea) + ' ' + convertPublicationType(publicationList[i].publicationType) +
convertImpactFactor(publicationList[i].impactFactor) + "</span>";
} else if (publicationList[i].publishingArea == 'INTERNATIONAL' && !(publicationList[i].publicationType == 'JOURNAL_SCIE' || publicationList[i].publicationType == 'JOURNAL_SCOPUS')) {
str = "<span class='btn-info btn-sm'>" + capitalize(publicationList[i].publishingArea) + ' ' + convertPublicationType(publicationList[i].publicationType) + "</span>";
} else if (publicationList[i].publishingArea == 'DOMESTIC' && publicationList[i].publicationType == 'JOURNAL') {
str = "<span class='btn-success btn-sm'>" + capitalize(publicationList[i].publishingArea) + ' ' + convertPublicationType(publicationList[i].publicationType) + "</span>";
} else if (publicationList[i].publishingArea == 'DOMESTIC' && !(publicationList[i].publicationType == 'JOURNAL')) {
str = "<span class='btn-warning btn-sm'>" + capitalize(publicationList[i].publishingArea) + ' ' + convertPublicationType(publicationList[i].publicationType) + "</span>";
} else {
str = "";
}
var link = '<span class="my-1">';
if (!isEmpty(publicationList[i].doi)) {
link += '<a id="publicationTitle"' + publicationId + ' href="' + 'https://doi.org/' + publicationList[i].doi + '" class="title" target="_blank">' + publicationList[i].title + '</a>'
} else if (isEmpty(publicationList[i].doi) && !isEmpty(publicationList[i].uri)) {
link += '<a id="publicationTitle"' + publicationId + ' href="' + publicationList[i].uri + '" class="title" target="_blank">' + publicationList[i].title + '</a>'
} else {
link += '<span id="publicationTitle"' + publicationId + ' class="title">' + publicationList[i].title + '</span>'
}
link += '</span><br>'
var number = !isEmpty(publicationList[i].number) ? '<span>, No. ' + publicationList[i].number + '</span>' : '';
var pages = !isEmpty(publicationList[i].pages) ? '<span>, pp. ' + publicationList[i].pages + '</span>' : '';
var isbnIssn = !isEmpty(publicationList[i].isbnIssn) ? '<span>, ' + publicationList[i].isbnIssn + '</span>' : '';
$("#publications").append(
'<div id="publicationData' + publicationId + '" class="publication-wrap mt-4">'
+ link
+ '<span id="publicationAuthors"' + publicationId + ' class="authors my-1">' + publicationList[i].authors + '</span><br>'
+ '<span>' + publicationList[i].publishedIn + '</span>'
+ '<span>, ' + publishedDate[1] + '</span>'
+ '<span>, ' + publishedDate[0] + '</span>'
+ '<span>, Vol. ' + publicationList[i].volume + '</span>'
+ number
+ pages
+ isbnIssn
+ '<div class="mt-2">'
+ str
+ '</div>'
+ '</div>');
publicationId++;
}
})
.fail(function (msg) {
searchFail();
$('#loadingScroll').hide();
})
}
});
});
$("#search").click(function () {
document.form.action = "/publication/list_scroll";
document.form.method = "get";
document.form.submit();
});
</script>
</body>
</html>
Util
- Javascript: URI 파라미터 반환 및 URI 생성 기능을 담당하는 함수다.
출처: <https://m.blog.naver.com/PostView.nhn?blogId=hay6308&logNo=220958671660&proxyReferer=https:%2F%2Fwww.google.com%2F
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<module-app-web/src/main/resources/static/js/util.js>
/* URI 파라미터 반환 */
function getUriParams() {
var params = {};
window.location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(str, key, value) { params[key] = value; });
return params;
}
/* URI 생성 */
function makeGetUri(uri, params) {
Object.keys(params).forEach(function(key, index) {
uri += (index === 0 ? "?" : "&") + key + "=" + params[key];
});
return uri;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<module-app-web/src/main/resources/templates/layout/script.html>
<script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/js/jquery.serialize-object.min.js}"></script>
<script th:src="@{/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/fileUtil.js}"></script>
<script th:src="@{/js/util.js}"></script>
<script th:src="@{/summernote/summernote.min.js}"></script>
<script th:inline="javascript">
$(function() {
var csrfToken = /*[[${_csrf.token}]]*/ null;
var csrfHeader = /*[[${_csrf.headerName}]]*/ null;
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader(csrfHeader, csrfToken);
});
});
</script>
프로젝트 실행 및 결과
- 다음 이미지와 같이 무한 스크롤 논문 게시판을 확인할 수 있다.
- 검색 조건을 변경하는 따른 데이터가 출력되는 것을 확인할 수 있다.
- 스크롤을 하단으로 내려서 이벤트가 발생하면 다음 이미지와 같이 숨겨진 loading 이미지가 보이게 되고 ajax를 통하여 새로운 데이터를 받아 출력한다.
Comments powered by Disqus.