게시글 댓글 개발 과정을 소개한다.
- github: https://github.com/scribnote5/lab
github commit: https://github.com/scribnote5/lab/commit/927d3ed72af532b438c7131482bfb09fcf3698d0
- 최신 프로젝트 코드와 형상이 다를 수 있습니다. 게시글 코드는 참고만 하시되, 최신 코드는 github에서 확인 부탁드립니다.
댓글 기능 및 구체적인 권한 접근 설계
- 사용자의 권한에 따라 댓글을 등록 할 수 있다.
- general 권한을 가진 사용자가 root 권한을 가진 사용자가 작성한 게시글의 form 페이지의 URI 주소를 유추할 수 있고 접근할 수 있다. 따라서 사용자 권한에 따라 게시판의 create, update, delete 페이지에 접근을 차단하는 기능을 구현하였다.
권한 및 계정 정보
- 총 4개의 권한이 존재하며, 권한 별로 게시글 및 페이지 접근이 제한된다.
권한 별 페이지 및 게시글 접근 여부는 다음과 같다.
- 프로젝트에서 분류한 권한 종류는 다음과 같다.
- root: 모든 권한에 대한 접근 허용, 특정 경로(admin 페이지) 접근 가능
- manager: general 권한에 대한 접근 허용, 본인이 create한 게시글에 update 및 delete 가능
- general: 본인이 create한 게시글에 update 및 delete 가능
- non_user: 비회원으로서, 게시글 read만 가능
- anonymous: 게시글 read만 가능
Table 설계
- 프로젝트에서 사용할 댓글 table을 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<notice_board_comment>
CREATE TABLE notice_board_comment
(
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,
notice_board_idx long null,
content longtext null
);
ALTER TABLE notice_board_comment AUTO_INCREMENT=1;
DROP TABLE notice_board_comment;
Domain 및 DTO
- NoticeBoard Comment에서 사용하는 Domain다.
- NoticeBoard의 idx를 참조할 수 있는 noticeBoardIdx 멤버 필드가 존재한다.
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
<module-domain-core/src/main/java/kr/ac/univ/noticeBoard/domain/NoticeBoardComment.java>
package kr.ac.univ.noticeBoard.domain;
import kr.ac.univ.common.domain.CommonAudit;
import kr.ac.univ.common.domain.enums.ActiveStatus;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Getter
@NoArgsConstructor
@Entity
@Table
@ToString
public class NoticeBoardComment extends CommonAudit {
@Column
private Long noticeBoardIdx;
@Column
private String content;
@Builder
public NoticeBoardComment(Long idx, String createdBy, String lastModifiedBy, ActiveStatus activeStatus, Long noticeBoardIdx, String content) {
setIdx(idx);
setCreatedBy(createdBy);
setLastModifiedBy(lastModifiedBy);
setActiveStatus(activeStatus);
this.noticeBoardIdx = noticeBoardIdx;
this.content = content;
}
public void update(NoticeBoardComment noticeBoard) {
setActiveStatus(noticeBoard.getActiveStatus());
this.content = noticeBoard.getContent();
}
}
- NoticeBoard Comment에서 사용하는 DTO다.
- 새로 등록한 댓글인지 확인하는 isNewIcon 멤버 필드와 사용자 권한에 따라 접근할 수 있는 isAccess 멤버 필드가 존재한다.
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
<module-domain-core/src/main/java/kr/ac/univ/noticeBoard/dto/NoticeBoardCommentDto.java>
package kr.ac.univ.noticeBoard.dto;
import kr.ac.univ.common.dto.CommonDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@NoArgsConstructor
@ToString
public class NoticeBoardCommentDto extends CommonDto {
/* CommonDto: JPA Audit */
/* 기본 정보 */
private Long noticeBoardIdx;
private String content;
/* newIcon */
private boolean isNewIcon;
/* 접근 여부 */
private boolean isAccess;
}
- NoticeBoard Comment Entity<->DTO mapping 소스 코드를 생성하는 Mapper 클래스다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<module-domain-core/src/main/java/kr/ac/univ/noticeBoard/dto/mapper/NoticeBoardCommentMapper.java>
package kr.ac.univ.noticeBoard.dto.mapper;
import kr.ac.univ.common.dto.mapper.EntityMapper;
import kr.ac.univ.noticeBoard.domain.NoticeBoardComment;
import kr.ac.univ.noticeBoard.dto.NoticeBoardCommentDto;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface NoticeBoardCommentMapper extends EntityMapper<NoticeBoardCommentDto, NoticeBoardComment> {
NoticeBoardCommentMapper INSTANCE = Mappers.getMapper(NoticeBoardCommentMapper.class);
}
Repository
- JPA를 사용하여 다음과 같은 쿼리를 작성하였다.
- findAllByNoticeBoardIdxOrderByCreatedDateDesc: 같은 게시글에 등록된 댓글을 생성일 순서로 내림차순 정렬한다.
- deleteAllByNoticeBoardIdx: 게시글에 등록된 댓글을 모두 삭제한다.
- 다음 출처를 참고하면 JPA로 대량의 데이터 삭제할 때 성능 문제가 발생한다. 해당 문제를 해결하기 위해서 JPA 대신 QueryDsl로 쿼리를 작성하여 게시글과 연관된 모든 댓글 삭제 기능을 구현하였다.
출처: https://jojoldu.tistory.com/235
https://derekpark.tistory.com/84
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<module-domain-core/src/main/java/kr/ac/univ/noticeBoard/repository/NoticeBoardCommentRepository.java>
package kr.ac.univ.noticeBoard.repository;
import java.util.List;
import kr.ac.univ.noticeBoard.domain.NoticeBoardComment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface NoticeBoardCommentRepository extends JpaRepository<NoticeBoardComment, Long> {
List<NoticeBoardComment> findAllByNoticeBoardIdxOrderByCreatedDateDesc(Long noticeBoardIdx);
void deleteAllByNoticeBoardIdx(Long noticeBoardIdx);
}
- QueryDsl를 사용하여 다음과 같은 쿼리를 작성하였다.
- deleteAllByNoticeBoardIdx: 게시글에 등록된 댓글을 모두 삭제한다.
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
<module-domain-core/src/main/java/kr/ac/univ/noticeBoard/repository/NoticeBoardCommentRepositoryImpl.java>
package kr.ac.univ.noticeBoard.repository;
import javax.transaction.Transactional;
import kr.ac.univ.noticeBoard.domain.NoticeBoardComment;
import kr.ac.univ.noticeBoard.domain.QNoticeBoardComment;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;
import com.querydsl.jpa.impl.JPAQueryFactory;
@Repository
@Transactional
public class NoticeBoardCommentRepositoryImpl extends QuerydslRepositorySupport {
private final JPAQueryFactory queryFactory;
public NoticeBoardCommentRepositoryImpl(JPAQueryFactory queryFactory) {
super(NoticeBoardComment.class);
this.queryFactory = queryFactory;
}
public Long deleteAllByNoticeBoardIdx(Long noticeBoardIdx) {
QNoticeBoardComment noticeBoardComment = QNoticeBoardComment.noticeBoardComment;
/*
* DELETE FROM AttachedFile
* WHERE noticeBoardIdx = 'noticeBoardIdx'
*/
return queryFactory
.delete(noticeBoardComment)
.where(noticeBoardComment.noticeBoardIdx.eq(noticeBoardIdx))
.execute();
}
}
Service
- NoticeBoard Comment의 비즈니스 로직이다.
- findAllByNoticeBoardIdxOrderByCreatedDateDesc: 게시글에 등록된 댓글을 모두 조회하며, 새로 등록된 댓글 여부와 접근 권한을 설정한다.
- NoticeBoard Comment service 처럼 NoticeBoard service 또한 접근 불가능한 사용자가 게시글에 접근 권한을 설정한다.(해당 내용은 생략되었다.)
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
<module-domain-core/src/main/java/kr/ac/univ/noticeBoard/service/NoticeBoardCommentService.java>
package kr.ac.univ.noticeBoard.service;
import kr.ac.univ.maintenance.dto.mapper.MaintenanceMapper;
import kr.ac.univ.noticeBoard.domain.NoticeBoardComment;
import kr.ac.univ.noticeBoard.dto.NoticeBoardCommentDto;
import kr.ac.univ.noticeBoard.dto.mapper.NoticeBoardCommentMapper;
import kr.ac.univ.noticeBoard.repository.NoticeBoardCommentRepository;
import kr.ac.univ.noticeBoard.repository.NoticeBoardCommentRepositoryImpl;
import kr.ac.univ.user.domain.User;
import kr.ac.univ.user.repository.UserRepository;
import kr.ac.univ.util.AccessCheck;
import kr.ac.univ.util.EmptyUtil;
import kr.ac.univ.util.NewIconCheck;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
@Service
public class NoticeBoardCommentService {
private final NoticeBoardCommentRepository noticeBoardCommentRepository;
private final NoticeBoardCommentRepositoryImpl noticeBoardCommentRepositoryImpl;
private final UserRepository userRepository;
public NoticeBoardCommentService(NoticeBoardCommentRepository noticeBoardCommentRepository, NoticeBoardCommentRepositoryImpl noticeBoardCommentRepositoryImpl, UserRepository userRepository) {
this.noticeBoardCommentRepository = noticeBoardCommentRepository;
this.noticeBoardCommentRepositoryImpl = noticeBoardCommentRepositoryImpl;
this.userRepository = userRepository;
}
public List<NoticeBoardCommentDto> findAllByNoticeBoardIdxOrderByCreatedDateDesc(Long noticeBoardIdx) {
List<NoticeBoardCommentDto> noticeBoardCommentDtoList = null;
noticeBoardCommentDtoList = NoticeBoardCommentMapper.INSTANCE.toDto(noticeBoardCommentRepository.findAllByNoticeBoardIdxOrderByCreatedDateDesc(noticeBoardIdx));
for (NoticeBoardCommentDto noticeBoardCommentDto : noticeBoardCommentDtoList) {
// NewIcon 판별
noticeBoardCommentDto.setNewIcon(NewIconCheck.isNew(noticeBoardCommentDto.getCreatedDate()));
// 권한 설정
// Update: isAccessInGeneral 메소드에 따라 접근 가능 및 불가
// 탈퇴 회원은 권한을 general로 설정 후 권한을 검사함
User user = userRepository.findByUsername(noticeBoardCommentDto.getCreatedBy());
noticeBoardCommentDto.setAccess(AccessCheck.isAccessInGeneral(noticeBoardCommentDto.getCreatedBy(), EmptyUtil.isEmpty(user) ? "general" : user.getAuthorityType().getAuthorityType()));
}
return noticeBoardCommentDtoList;
}
public Long insertNoticeBoardComment(NoticeBoardCommentDto noticeBoardCommentDto) {
return noticeBoardCommentRepository.save(NoticeBoardCommentMapper.INSTANCE.toEntity(noticeBoardCommentDto)).getIdx();
}
@Transactional
public Long updateNoticeBoardComment(Long idx, NoticeBoardCommentDto noticeBoardCommentDto) {
NoticeBoardComment persistNoticeBoardComment = noticeBoardCommentRepository.getOne(idx);
NoticeBoardComment noticeBoardComment = NoticeBoardCommentMapper.INSTANCE.toEntity(noticeBoardCommentDto);
persistNoticeBoardComment.update(noticeBoardComment);
return noticeBoardCommentRepository.save(persistNoticeBoardComment).getIdx();
}
public void deleteNoticeBoardCommentByIdx(Long idx) {
noticeBoardCommentRepository.deleteById(idx);
}
public void deleteAllByNoticeBoardIdx(Long idx) {
noticeBoardCommentRepositoryImpl.deleteAllByNoticeBoardIdx(idx);
}
}
Controller
- NoticeBoard Comment 관련 클라이언트의 요청을 view로 매핑한다.
- NoticeBoard service에서 설정한 접근 권한을 바탕으로 접근 불가능한 사용자가 게시글에 접근할 수 없도록 로직을 추가하였다.
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
<module-app-web/src/main/java/kr/ac/univ/controller/NoticeBoardController.java>
package kr.ac.univ.controller;
import kr.ac.univ.common.dto.SearchDto;
import kr.ac.univ.noticeBoard.dto.NoticeBoardCommentDto;
import kr.ac.univ.noticeBoard.dto.NoticeBoardDto;
import kr.ac.univ.noticeBoard.service.NoticeBoardAttachedFileService;
import kr.ac.univ.noticeBoard.service.NoticeBoardCommentService;
import kr.ac.univ.noticeBoard.service.NoticeBoardService;
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;
import java.util.List;
@Controller
@RequestMapping("/notice-board")
public class NoticeBoardController {
private final NoticeBoardService noticeBoardService;
private final NoticeBoardAttachedFileService noticeBoardAttachedFileService;
private final NoticeBoardCommentService noticeBoardCommentService;
public NoticeBoardController(NoticeBoardService noticeBoardService, NoticeBoardAttachedFileService noticeBoardAttachedFileService, NoticeBoardCommentService noticeBoardCommentService) {
this.noticeBoardService = noticeBoardService;
this.noticeBoardAttachedFileService = noticeBoardAttachedFileService;
this.noticeBoardCommentService = noticeBoardCommentService;
}
// List
@GetMapping("/list")
public String noticeBoardList(@PageableDefault Pageable pageable, SearchDto searchDto, Model model) {
model.addAttribute("noticeBoardDtoList", noticeBoardService.findNoticeBoardList(pageable, searchDto));
return "/noticeBoard/list";
}
// Form Update
@GetMapping("/form{idx}")
public String noticeBoardForm(@RequestParam(value = "idx", defaultValue = "0") Long idx, Model model) {
NoticeBoardDto noticeBoardDto = noticeBoardService.findNoticeBoardByIdx(idx);
String returnPage = null;
// 권한 확인
if (noticeBoardDto.isAccess()) {
noticeBoardDto = noticeBoardAttachedFileService.findAttachedFileByNoticeBoardIdx(idx, noticeBoardDto);
model.addAttribute("noticeBoardDto", noticeBoardDto);
returnPage = "/noticeBoard/form";
} else {
returnPage = "/user/permission-denied";
}
return returnPage;
}
// Read
@GetMapping({"", "/"})
public String noticeBoardRead(@RequestParam(value = "idx", defaultValue = "0") Long idx, Model model) {
NoticeBoardDto noticeBoardDto = null;
List<NoticeBoardCommentDto> noticeBoardCommentDtoList = null;
noticeBoardDto = noticeBoardService.findNoticeBoardByIdx(idx);
noticeBoardDto = noticeBoardAttachedFileService.findAttachedFileByNoticeBoardIdx(idx, noticeBoardDto);
noticeBoardCommentDtoList = noticeBoardCommentService.findAllByNoticeBoardIdxOrderByCreatedDateDesc(idx);
model.addAttribute("noticeBoardDto", noticeBoardDto);
model.addAttribute("noticeBoardCommentDtoList", noticeBoardCommentDtoList);
return "/noticeBoard/read";
}
}
RestController
- NoticeBoard Comment 관련 클라이언트의 요청을 json 타입으로 응답한다.
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
<module-app-api/src/main/java/kr/ac/univ/controller/NoticeBoardCommentRestController.java>
package kr.ac.univ.controller;
import kr.ac.univ.noticeBoard.domain.NoticeBoardComment;
import kr.ac.univ.noticeBoard.dto.NoticeBoardCommentDto;
import kr.ac.univ.noticeBoard.service.NoticeBoardCommentService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/notice-boards-comments")
public class NoticeBoardCommentRestController {
private final NoticeBoardCommentService noticeBoardCommentService;
public NoticeBoardCommentRestController(NoticeBoardCommentService noticeBoardCommentService) {
this.noticeBoardCommentService = noticeBoardCommentService;
}
@PostMapping
public ResponseEntity<?> postNoticeBoard(@RequestBody NoticeBoardComment noticeBoardComment) {
Long idx = noticeBoardCommentService.insertNoticeBoardComment(noticeBoardComment);
return new ResponseEntity<>(idx, HttpStatus.CREATED);
}
@PutMapping("/{idx}")
public ResponseEntity<?> putNoticeBoard(@PathVariable("idx") Long idx, @RequestBody NoticeBoardCommentDto noticeBoardCommentDto) {
noticeBoardCommentService.updateNoticeBoardComment(idx, noticeBoardCommentDto);
return new ResponseEntity<>("{}", HttpStatus.OK);
}
@DeleteMapping("/{idx}")
public ResponseEntity<?> deleteNoticeBoard(@PathVariable("idx") Long idx) {
noticeBoardCommentService.deleteNoticeBoardCommentByIdx(idx);
return new ResponseEntity<>("{}", HttpStatus.OK);
}
}
View
- NoticeBoard Comment 관련 화면을 출력한다.
- 사용자의 권한에 따라 댓글을 등록할 수 있고, 수정 및 삭제 할 수 있다.
- 댓글의 ‘Update’ 버튼을 클릭하는 경우 span 태그가 보이지 않고 form 태그가 보여 댓글을 수정할 수 있고, ‘Cancel’ 버튼을 클릭하는 경우 span 태그가 보이고 form 태그가 보이지 않게 된다.
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
<module-app-web/src/main/resources/templates/noticeBoard/read.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')} + ' | Notice Board Detail'"></title>
</head>
<body>
<div id="page-content-wrapper">
<!-- header -->
<div th:replace="layout/header::header"></div>
<div class="container">
<form name="form" id="form" th:object="${noticeBoardDto}" action="#">
<div class="page-header breadcrumbs">
<div class="d-flex justify-content-between align-items-center" data-aos="fade-up">
<h2>Notice Board Detail</h2>
<ol>
<li>Home</li>
<li>Notice Board Detail</li>
</ol>
</div>
</div>
<div id="loading">
<img th:src="@{/images/loading.gif}" alt="Loading…"/>
</div>
<div class="page-content" data-aos="fade-up">
<table class="table mobile-table-read mt-4">
<colgroup>
<col width="17.5%"/>
<col width="82.5%"/>
</colgroup>
<!-- Desktop -->
<tr class="d-none d-sm-none d-md-none d-lg-table-row">
<td class="text-right border-0" colspan="10">
<strong class="additional-information">Created By: </strong><span th:text="*{createdBy}" class="additional-information"></span>
<strong class="additional-information">Created Date: </strong><span th:text="*{#temporals.format(createdDate,'yyyy.MM.dd. HH:mm')}" class="additional-information"></span>
<strong class="additional-information">Views: </strong> <span th:text="*{views}" class="additional-information"/></span>
</td>
</tr>
<tr class="d-none d-sm-none d-md-none d-lg-table-row">
<th>Title</th>
<td th:text="*{title}"></td>
</tr>
<!-- Mobile -->
<tr class="d-print-none d-sm-table-row d-md-table-row d-lg-none d-xl-none d-table-row">
<td colspan="2">
<h4 th:text="*{title}" class="mobile-title"></h4>
<div class="text-right">
<span th:text="*{createdBy}" class="mobile-additional-information"></span>
<span th:text="*{#temporals.format(createdDate,'yyyy.MM.dd. HH:mm')}" class="mobile-additional-information"></span>
<span th:text="'Views: ' + *{views}" class="mobile-additional-information"/>
</div>
</td>
</tr>
<!-- Common -->
<tr>
<td colspan="2">
<div class="content ck-content" th:utext="*{content}"></div>
</td>
</tr>
<tr>
<th class="d-none d-sm-none d-md-none d-lg-table-cell">Uploaded Attached File</th>
<td colspan="2">
<strong class="d-print-none d-sm-inline d-md-inline d-lg-none d-xl-none d-inline mobile-default">Uploaded Attached File</strong>
<div id="attachedFileList" th:each="attachedFile : *{attachedFileList}">
<span th:attr="onclick=|location.href=encodeURI('${@environment.getProperty('module-app-api.address')}/api/attachedFiles/download/${attachedFile.savedFileName}')|"
th:text="${attachedFile.fileName} + ', ' + 'File size: ' + ${attachedFile.fileSize}"></span>
</div>
</td>
</tr>
<tr>
<td colspan="10"></td>
</tr>
</table>
<!-- Comment -->
<table class="table mt-3">
<colgroup>
<col width="17.5%"/>
<col width="82.5%"/>
</colgroup>
<tr>
<th colspan="2" class="sub-item-title">Comments</th>
</tr>
<tr th:if="${!#strings.equals(#authentication.principal, 'anonymousUser')}">
<td colspan="2">
<textarea class="form-control comment" name="content" id="comment"></textarea>
<button type="button" class="btn btn-sm btn-primary mt-3" id="insertComment">Register</button>
</td>
</tr>
</table>
<div th:if="!${#lists.isEmpty(noticeBoardCommentDtoList)}" class="mt-3">
<table class="table table-hover">
<tr th:each="noticeBoardCommentDto : ${noticeBoardCommentDtoList}">
<td>
<span th:text="${noticeBoardCommentDto.createdBy}"></span>
<span th:text="${#temporals.format(noticeBoardCommentDto.createdDate,'yyyy.MM.dd. HH:mm')}" class="comment-additional-information"></span>
<img th:class="new-icon" th:if="${noticeBoardCommentDto.newIcon}" th:attr="src=@{|/images/new-icon.png|}"/>
<div class="mt-2">
<span th:id="commentContent + ${noticeBoardCommentDtoStat.index}"
th:utext="${noticeBoardCommentDto.content}"
class="comment">
</span>
<textarea th:id="updateCommentContent + ${noticeBoardCommentDtoStat.index}"
th:text="${noticeBoardCommentDto.content}"
class="form-control comment"
style="display: none;"
name="updateContent">
</textarea>
</div>
<div th:if="${noticeBoardCommentDto.access}" class="mt-3">
<button type="button" class="btn btn-sm btn-outline-info mr-2"
th:id="displayComment + ${noticeBoardCommentDtoStat.index}"
th:onclick="displayComment([[${noticeBoardCommentDtoStat.index}]])">
Update
</button>
<button type="button" class="btn btn-sm btn-outline-dangIIggdfger"
th:id="deleteComment + ${noticeBoardCommentDtoStat.index}"
th:onclick="deleteComment([[${noticeBoardCommentDto.idx}]], [[${noticeBoardCommentDtoStat.index}]])">
Delete
</button>
<button type="button" style="display:none;" class="btn btn-sm btn-outline-primary mr-2"
th:id="updateComment + ${noticeBoardCommentDtoStat.index}"
th:onclick="updateComment([[${noticeBoardCommentDto.idx}]], [[${noticeBoardCommentDto.createdBy}]], [[${noticeBoardCommentDto.activeStatus}]],[[${noticeBoardCommentDtoStat.index}]])">
Register
</button>
<button type="button" style="display:none;" class="btn btn-sm btn-outline-danger" th:id="cancelComment + ${noticeBoardCommentDtoStat.index}"
th:onclick="cancelComment([[${noticeBoardCommentDtoStat.index}]])">
Cancel
</button>
</div>
</td>
</tr>
</table>
</div>
<div class="row justify-content-between mt-4 mb-4">
<div class="col-auto">
<a href="/notice-board/list" class="btn btn-sm btn-secondary">Move to List</a>
</div>
<div th:if="*{access}" class="col-auto ml-2">
<a th:href="'/notice-board/form?idx='+*{idx}" class="btn btn-sm btn-info mx-1">Update</a>
<button type="button" class="btn btn-sm btn-danger mx-1" id="delete">Delete</button>
</div>
</div>
<!-- input type="hidden" -->
<input type="hidden" name="idx" th:value="*{idx}"/>
<input type="hidden" name="createdBy" th:if="${!#strings.equals(#authentication.principal, 'anonymousUser')}" th:value="${#authentication.principal.username}"/>
<input type="hidden" name="lastModifiedBy" th:if="${!#strings.equals(#authentication.principal, 'anonymousUser')}" th:value="${#authentication.principal.username}"/>
<input type="hidden" name="activeStatus" th:if="${!#strings.equals(#authentication.principal, 'anonymousUser')}" th:value="ACTIVE"/>
</div>
</form>
</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">
sweetalertFire("notice-board");
$("#delete").click(function () {
Confirm.fire({
icon: "warning",
title: "Do you want to delete?",
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: moduleAppApiAddress + "/api/notice-boards/" + document.getElementsByName("idx")[0].value,
type: "delete",
dataType: "text",
contentType: "application/json",
async: false,
})
.done(function (msg) {
localStorage.setItem("result", "/notice-board/delete-success");
location.href = "/notice-board/list";
})
.fail(function (msg) {
parseErrorMsg(msg);
})
} else {
return false;
}
})
});
$("#insertComment").click(function () {
var jsonData =
{
createdBy: document.getElementsByName("createdBy")[0].value,
lastModifiedBy: document.getElementsByName("lastModifiedBy")[0].value,
activeStatus: document.getElementsByName("activeStatus")[0].value,
noticeBoardIdx: document.getElementsByName("idx")[0].value,
content: document.getElementsByName("content")[0].value.replace(/\n/g, "<br>")
}
// duplicate submit check
if (duplicateSubmitCheck()) return false;
$.ajax({
url: moduleAppApiAddress + "/api/notice-boards-comments/",
type: "post",
data: JSON.stringify(jsonData),
dataType: "text",
contentType: "application/json",
async: false,
})
.done(function (msg) {
localStorage.setItem("result", "/notice-board/register-success");
location.href = "/notice-board?idx=" + document.getElementsByName("idx")[0].value;
})
.fail(function (msg) {
parseErrorMsg(msg);
duplicateSubmitFlag = false;
})
});
function updateComment(idx, createdBy, activeStatus, commentId) {
var jsonData =
{
idx: idx,
createdBy: createdBy,
lastModifiedBy: document.getElementsByName("lastModifiedBy")[0].value,
activeStatus: activeStatus,
noticeBoardIdx: document.getElementsByName("idx")[0].value,
content: document.getElementsByName("updateContent")[commentId].value.replace(/\n/g, "<br>")
}
// duplicate submit check
if (duplicateSubmitCheck()) return false;
$.ajax({
url: moduleAppApiAddress + "/api/notice-boards-comments/" + idx,
type: "put",
data: JSON.stringify(jsonData),
dataType: "text",
contentType: "application/json",
async: false,
})
.done(function (msg) {
localStorage.setItem("result", "/notice-board/update-success");
location.href = "/notice-board?idx=" + document.getElementsByName("idx")[0].value;
})
.fail(function (msg) {
parseErrorMsg(msg);
duplicateSubmitFlag = false;
})
}
function deleteComment(idx, commentId) {
Confirm.fire({
icon: "warning",
title: "Do you want to delete?",
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: moduleAppApiAddress + "/api/notice-boards-comments/" + idx,
type: "delete",
dataType: "text",
contentType: "application/json",
async: false,
})
.done(function (msg) {
localStorage.setItem("result", "/notice-board/delete-success");
location.href = "/notice-board?idx=" + document.getElementsByName("idx")[0].value;
})
.fail(function (msg) {
parseErrorMsg(msg);
})
} else {
return false;
}
})
}
function displayComment(index) {
document.getElementsByName("updateContent")[index].style.display = "inline";
document.getElementById("commentContent" + index).style.display = "none";
document.getElementById("displayComment" + index).style.display = "none";
document.getElementById("deleteComment" + index).style.display = "none";
document.getElementById("updateComment" + index).style.display = "inline";
document.getElementById("cancelComment" + index).style.display = "inline";
}
function cancelComment(index) {
document.getElementsByName("updateContent")[index].style.display = "none";
document.getElementById("commentContent" + index).style.display = "inline";
document.getElementById("displayComment" + index).style.display = "inline";
document.getElementById("deleteComment" + index).style.display = "inline";
document.getElementById("updateComment" + index).style.display = "none";
document.getElementById("cancelComment" + index).style.display = "none";
}
</script>
</body>
</html>
Util
- 생성자, 권한, 로그인한 사용자의 데이터를 활용하여 사용자 접근 권한을 판별한다.
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
<module-system-common/src/main/java/kr/ac/univ/util/AccessCheck.java>
package kr.ac.univ.util;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
public class AccessCheck {
/**
* [일반적인 상황에서 사용자 권한에 따른 접근 가능 여부]
* <p>
* root: 모든 권한에 대한 접근 허용, admin 페이지 접근 가능
* manager: 작성자 권한이 root인 경우 접근 불가, 작성자 권한이 manager인 경우 로그인한 사용자의 username과 작성자가 같은 경우 접근 허용, 작성자 권한이 general인 경우 접근 허용, admin 페이지 접근 가능
* general: 작성자 권한이 root, manager인 경우 접근 불가, 작성자 권한이 general인 경우 로그인한 사용자의 username과 작성자가 같은 경우 접근 허용
* non_user: 로그인 불가
* anonymous: 로그인 불가
*
* @param createdBy
* @return
*/
public static Boolean isAccessInGeneral(String createdBy, String authorityType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean result = false;
// 비인증 사용자, 인증이 안된 경우, authentication 객체가 null인 경우
// -> 접근 불가
if (!"anonymousUser".equals(authentication.getPrincipal()) || !authentication.isAuthenticated() || EmptyUtil.isEmpty(authentication)) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String authenticationUsername = userDetails.getUsername();
for (GrantedAuthority grantedAuthority : userDetails.getAuthorities()) {
switch (grantedAuthority.getAuthority()) {
// 로그인한 사용자의 권한: root
// -> 접근 가능
case "root":
result = true;
break;
case "manager":
// createdBy: root
// -> 접근 불가
if ("root".equals(createdBy)) {
result = false;
}
// username authority: MANAGER
// 로그인한 사용자의 username과 username: 다름
// -> 접근 불가
else if ("manager".equals(authorityType) && !authenticationUsername.equals(createdBy)) {
result = false;
}
// 나머지 조건
// -> 접근 가능
else {
result = true;
}
break;
default:
// 로그인한 사용자의 username과 createdBy: 같음
// -> 접근 가능
if (authenticationUsername.equals(createdBy)) {
result = true;
}
// 로그인한 사용자의 username과 createdBy: 다름
// -> 접근 불가
else {
result = false;
}
break;
}
}
}
return result;
}
/**
* [module-app-admin user에서 사용자 권한에 따른 접근 가능 여부]
* <p>
* 비인증 사용자인 경우 접근 불가
* root: 모든 권한에 대한 접근 허용
* manager: 작성자가 root인 경우 접근 허용, 로그인한 사용자의 username과 작성자가 같은 경우 접근 허용
*
* @param createdBy
* @return
*/
public static Boolean isAccessInModuleAdminUser(String createdBy, String username, String authorityType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean result = false;
// 비인증 사용자, 인증이 안된 경우, authentication 객체가 null인 경우
// -> 접근 불가
if (!"anonymousUser".equals(authentication.getPrincipal()) || !authentication.isAuthenticated() || EmptyUtil.isEmpty(authentication)) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String authenticationUsername = userDetails.getUsername();
for (GrantedAuthority grantedAuthority : userDetails.getAuthorities()) {
switch (grantedAuthority.getAuthority()) {
// 로그인한 사용자의 권한: root
// -> 접근 가능
case "root":
result = true;
break;
case "manager":
// 로그인한 사용자의 권한: manager
// 로그인한 사용자의 username과 createdBy: 같음
// -> 접근 가능
if (authenticationUsername.equals(createdBy)) {
result = true;
}
// 로그인한 사용자의 권한: manager
// username의 권한 general || non_user
// -> 접근 가능
else if (("general".equals(authorityType) || "non_user".equals(authorityType))) {
result = true;
}
// 로그인한 사용자의 권한: manager
// 로그인한 사용자의 username과 username: 같음
// -> 접근 가능
else if (authenticationUsername.equals(username)) {
result = true;
}
// 이외
// -> 접근 불가
else {
result = false;
}
break;
default:
result = false;
break;
}
}
}
return result;
}
/**
* [module-app-web user에서 사용자 권한에 따른 접근 가능 여부]
* <p>
* 비인증 사용자인 경우 접근 불가
* 작성자가 root인 경우 접근 허용
* 작성자 권한이 MANAGER인 경우 접근 허용
* 작성자와 사용자 아이디가 같은 경우 접근 허용
*
* @param createdBy
* @return
*/
public static Boolean isAccessInModuleWebUser(String createdBy, String username, String authorityType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean result = false;
// 비인증 사용자, 인증이 안된 경우, authentication 객체가 null인 경우
// -> 접근 불가
if ("anonymousUser".equals(authentication.getPrincipal()) || !authentication.isAuthenticated() || EmptyUtil.isEmpty(authentication)) {
result = false;
} else {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String authenticationUsername = userDetails.getUsername();
// createdBy: root
// username authority: MANAGER
// 로그인한 사용자의 username과 username: 같음
// -> 접근 가능
if ("root".equals(createdBy) || "MANAGER".equals(authorityType) && username.equals(authenticationUsername)) {
result = true;
}
// 로그인한 사용자의 username과 createdBy: 같음
// -> 접근 가능
else if (authenticationUsername.equals(createdBy)) {
result = true;
} else {
result = false;
}
}
return result;
}
}
프로젝트 실행 및 결과
- 다음 이미지와 같이 게시글에 댓글이 등록된 것을 확인할 수 있다.
- ID: manager2, 권한: manager인 사용자는 앞서 설명한 권한 설정에 따라 접근할 수 있는 댓글이 다르다.
- ID: manager2 사용자는 ID: manager 사용자와 ID: root 사용자의 댓글에 접근할 수 없다.
- 만약 비인증 사용자가 ID: root 사용자가 작성한 게시글의 URI(http://localhost:8080/notice-board/form?idx=200 )로 접근하는 경우, 다음 이미지와 같이 접근을 차단한다.
- 이외에도 ID: root가 아닌 사용자가 ID: root 사용자가 작성한 게시글에 접근하는 경우, 접근을 차단한다.
Comments powered by Disqus.