Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

hellonayeon/bbs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

13 Commits

Repository files navigation

Bulletin Board System

κ²Œμ‹œνŒ μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ ν”„λ‘œμ νŠΈ μž…λ‹ˆλ‹€.

2022εΉ΄08月04ζ—₯ ~ 2022εΉ΄08月07ζ—₯ λ™μ•ˆ Spring Boot 와 React λ₯Ό μ‚¬μš©ν•΄ κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

이 ν”„λ‘œμ νŠΈλ₯Ό 톡해 이루고자 ν•œ λͺ©ν‘œλŠ” Springμ—μ„œ μ œκ³΅ν•˜λŠ” ν”„λ ˆμž„μ›Œν¬λ₯Ό 직접 μ‚¬μš©ν•΄ 보기 μ˜€μŠ΅λ‹ˆλ‹€. ν”„λ‘œμ νŠΈ κ΅¬ν˜„ κ³Όμ • λ™μ•ˆ νšŒμ› 인증/인가, μ˜ˆμ™Έμ²˜λ¦¬λ₯Ό κ³ λ―Όν•˜λ©° μ½”λ“œλ₯Ό μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

πŸ“š λͺ©μ°¨

πŸŽƒ ν”„λ‘œμ νŠΈ ꡬ쑰

πŸ“Œ Backend

backend-project-structure

πŸ₯• Frontend

frontend-project-structure

πŸ•Ή μ‚¬μš© 기술

πŸ“Œ Backend

기술 버전
Spring Boot 2.7.2
Spring Security 2.7.2
Bean Validation 2.7.2
JSON Web Token 0.9.1
MyBatis 2.1.3
MySQL Connector J 8.0.28
Swagger 3.0.0

πŸ₯• Frontend

기술 버전
NodeJS 16.16.0
React 18.2.0
axios 0.27.2
react-axios 2.0.6
react-dom 18.2.0
react-js-pagination 3.0.3
react-router 6.3.0
react-router-dom 6.3.0
react-scripts 5.0.1

🎒 κ΅¬ν˜„ κΈ°λŠ₯

  • κ²Œμ‹œνŒ κΈ°λŠ₯
    • λͺ¨λ“  κ²Œμ‹œκΈ€ 및 νŠΉμ • κ²Œμ‹œκΈ€ 쑰회
    • κ²Œμ‹œκΈ€ 검색 (제λͺ©, λ‚΄μš©, μž‘μ„±μž)
    • κ²Œμ‹œκΈ€ μž‘μ„± [νšŒμ›]
    • κ²Œμ‹œκΈ€ μˆ˜μ • [νšŒμ›, κ²Œμ‹œκΈ€ μž‘μ„±μž]
    • κ²Œμ‹œκΈ€ μ‚­μ œ [νšŒμ›, κ²Œμ‹œκΈ€ μž‘μ„±μž]
    • κ²Œμ‹œκΈ€ λ‹΅κΈ€ μž‘μ„± [νšŒμ›]
  • λŒ“κΈ€ κΈ°λŠ₯
    • λŒ“κΈ€ 쑰회
    • λŒ“κΈ€ μž‘μ„± [νšŒμ›]
    • λŒ“κΈ€ μˆ˜μ • [νšŒμ›, λŒ“κΈ€ μž‘μ„±μž]
    • λŒ“κΈ€ μ‚­μ œ [νšŒμ›, λŒ“κΈ€ μž‘μ„±μž]
  • νšŒμ› κΈ°λŠ₯
    • νšŒμ›κ°€μž…
    • 둜그인/λ‘œκ·Έμ•„μ›ƒ

🍭 κΈ°λŠ₯ μ‹€ν–‰ν™”λ©΄

κ²Œμ‹œνŒ κΈ°λŠ₯

λͺ¨λ“  κ²Œμ‹œκΈ€ 및 νŠΉμ • κ²Œμ‹œκΈ€ 쑰회

  • λͺ¨λ“  κ²Œμ‹œκΈ€μ„ μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. νŽ˜μ΄μ§• κΈ°λŠ₯을 톡해 ν•œ νŽ˜μ΄μ§€μ—μ„œ μ΅œλŒ€ 10개의 κ²Œμ‹œκΈ€μ΄ μ‘°νšŒλ©λ‹ˆλ‹€.

bbslist1

bbslist2

  • κ²Œμ‹œκΈ€μ˜ 제λͺ©μ„ ν΄λ¦­ν•˜λ©΄, κ²Œμ‹œκΈ€μ˜ 상세 λ‚΄μš©μ„ μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbsdetail1

bbsdetail2

κ²Œμ‹œκΈ€ 검색

  • κ²Œμ‹œκΈ€μ˜ 제λͺ©κ³Ό λ‚΄μš© λ˜λŠ” μž‘μ„±μžλ‘œ κ²Œμ‹œκΈ€μ„ 검색할 수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-search

κ²Œμ‹œκΈ€ μž‘μ„±

  • λ‘œκ·ΈμΈν•œ μ‚¬μš©μžλŠ” κ²Œμ‹œκΈ€μ„ μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-write

bbs-write-success

bbs-write-result

  • λ‘œκ·ΈμΈν•˜μ§€ μ•Šμ•˜μ„ 경우 κΈ€ μž‘μ„±μ΄ μ œν•œλ©λ‹ˆλ‹€.

bbs-write-auth

κ²Œμ‹œκΈ€ μˆ˜μ •

  • κ²Œμ‹œκΈ€ μž‘μ„±μžλŠ” κ²Œμ‹œκΈ€μ„ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-update

bbs-update2

bbs-update-success

  • μžμ‹ μ΄ μž‘μ„±ν•œ κ²Œμ‹œκΈ€μ—λ§Œ μˆ˜μ •, μ‚­μ œ λ²„νŠΌμ΄ ν™œμ„±ν™”λ©λ‹ˆλ‹€. bbs-update-delete-btn-deactive

bbs-update-delete-btn-active

κ²Œμ‹œκΈ€ μ‚­μ œ

  • κ²Œμ‹œκΈ€ μž‘μ„±μžλŠ” κ²Œμ‹œκΈ€μ„ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-delete

bbs-delete-result

κ²Œμ‹œκΈ€ λ‹΅κΈ€ μž‘μ„±

  • ν•˜λ‚˜μ˜ κ²Œμ‹œκΈ€μ— λŒ€ν•œ 닡글을 μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ²Œμ‹œκΈ€ μž‘μ„± κ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ λ‘œκ·ΈμΈν•œ μ‚¬μš©μžλ§Œ 닡글을 μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-answer1

bbs-answer2

bbs-answer3

bbs-answer4

λŒ“κΈ€ κΈ°λŠ₯

λŒ“κΈ€ 쑰회

  • κ²Œμ‹œκΈ€ 상세 μ—μ„œ κ΄€λ ¨λœ λŒ“κΈ€μ„ μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. νŽ˜μ΄μ§• κΈ°λŠ₯을 톡해 ν•œ νŽ˜μ΄μ§€μ—μ„œ μ΅œλŒ€ 5개의 λŒ“κΈ€μ΄ μ‘°νšŒλ©λ‹ˆλ‹€.

comment1

comment2

λŒ“κΈ€ μž‘μ„±

  • λ‘œκ·ΈμΈν•œ μ‚¬μš©μžλŠ” λŒ“κΈ€μ„ μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

comment-write1

comment-write2

λŒ“κΈ€ μˆ˜μ •

  • μžμ‹ μ΄ μž‘μ„±ν•œ λŒ“κΈ€μ„ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

comment-update1

comment-update2

comment-update3

λŒ“κΈ€ μ‚­μ œ

  • μžμ‹ μ΄ μž‘μ„±ν•œ λŒ“κΈ€μ„ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€.

comment-delete

comment-delete2

νšŒμ› κΈ°λŠ₯

νšŒμ›κ°€μž…

  • νšŒμ›κ°€μž… μ‹œ 아이디 쀑볡을 μ²΄ν¬ν•©λ‹ˆλ‹€.

signup-idcheck

  • νšŒμ›κ°€μž…μ„ 톡해 μ„œλΉ„μŠ€μ— μ‚¬μš©μž 정보λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.

signup-success

둜그인/λ‘œκ·Έμ•„μ›ƒ

  • 둜그인

login-form

login-success

  • λ‘œκ·ΈμΈμ„ μ™„λ£Œν•˜λ©΄ λΈŒλΌμš°μ €μ˜ Local Storage 에 μ‚¬μš©μž id 와 JWT 토큰 정보λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.

login-after-devtool

  • λ‘œκ·Έμ•„μ›ƒ

logout

  • λ‘œκ·Έμ•„μ›ƒμ„ μ™„λ£Œν•˜λ©΄ λΈŒλΌμš°μ €μ˜ Local Storage 의 λ‚΄μš©λ„ μ‚­μ œν•©λ‹ˆλ‹€.

logout-after-devtool

πŸ€™πŸ» API λͺ…μ„Έμ„œ

HTTP λ©”μ„œλ“œλ₯Ό 톡해 ν–‰μœ„λ₯Ό λͺ…μ‹œν•  수 μžˆλ„λ‘ RESTful λ°©μ‹μœΌλ‘œ μ„€κ³„ν–ˆμŠ΅λ‹ˆλ‹€.

api-definition

πŸ•Έ ERD 섀계

erd

πŸ‘Ύ νŠΈλŸ¬λΈ”μŠˆνŒ…

νšŒμ› 인증 및 인가 κΈ°λŠ₯ κ΅¬ν˜„ (Spring Security + JWT)

νšŒμ› 및 λΉ„νšŒμ›μ— 따라 κ°€μš©ν•œ κΈ°λŠ₯에 μ œμ•½μ„ 두기 μœ„ν•΄ Spring Security + JWT 토큰 λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ν–ˆλ‹€.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/bbs", "/comment").authenticated()
.antMatchers(HttpMethod.PATCH, "/bbs", "/comment").authenticated()
.antMatchers(HttpMethod.DELETE, "/bbs", "/comment").authenticated()
.anyRequest().permitAll();
http.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String userId = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX,"");
try {
userId = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
System.out.println("JwtAuthenticationFilter: token error (fail get user id) !");
e.printStackTrace();
} catch (ExpiredJwtException e) {
System.out.println("JwtAuthenticationFilter: expired token !");
e.printStackTrace();
} catch(SignatureException e){
System.out.println("JwtAuthenticationFilter: invalid member !");
e.printStackTrace();
}
} else {
System.out.println("JwtAuthenticationFilter: request that do not require authorization.");
}
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + userId + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}

κ°„λ‹¨ν•œ BBS μ‹œμŠ€ν…œμ„ λ§Œλ“œλŠ” 것을 λͺ©ν‘œλ‘œ ν–ˆμœΌλ‚˜, Spring Security λ₯Ό μ‚¬μš©ν•˜λ‹ˆ 인증 λ‘œμ§μ„ κ΅¬ν˜„ν•˜κΈ° λ³΅μž‘ν–ˆλ‹€. Spring Security 의 λ™μž‘ 원리λ₯Ό μ •ν™•ν•˜κ²Œ λͺ¨λ₯΄λŠ” μƒνƒœμ—μ„œ κ΅¬ν˜„ν•˜λ € ν•˜λ‹ˆ 어렀움이 μžˆμ—ˆλ‹€. (배보닀 배꼽이 더 컀진 λŠλ‚Œ 🫀)

Spring μ—μ„œ μ œκ³΅ν•˜λŠ” Interceptor κΈ°λŠ₯κ³Ό ArgumentResolver, Annotation κΈ°λŠ₯을 μ‚¬μš©ν•˜κ³  μ‚¬μš©μž μ•„μ΄λ””λ§Œμ„ λ°›μ•„ 인가 체크λ₯Ό ν–ˆλ‹€λ©΄ κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ κ°€λŠ₯ν–ˆμ„ 것 κ°™λ‹€.

μš”μ²­μ„ 보낸 μ‚¬μš©μžλ₯Ό νŒλ³„ν•˜κΈ° μœ„ν•΄ @AuthenticationPrincipal 을 μ‚¬μš©ν•˜μ—¬, 둜그인 μ‹œ μΈμ¦ν•œ ν›„ μ €μž₯ν•œ μ‚¬μš©μž 정보인 UserDetails 의 username(Id) λ₯Ό 가져와 κΈ€ μž‘μ„±μžμ™€ λΉ„κ΅ν–ˆλ‹€.

/* [PATCH] /comment/{seq} λŒ“κΈ€ μˆ˜μ • */
@PatchMapping("/{seq}")
public ResponseEntity<UpdateCommentResponse> updateComment(@AuthenticationPrincipal UserDetails userDetails,
@PathVariable Integer seq,
@RequestBody UpdateCommentRequest req) {
return ResponseEntity.ok(service.updateComment(userDetails.getUsername(), seq, req));
}
/* λŒ“κΈ€ μˆ˜μ • */
@Transactional
public UpdateCommentResponse updateComment(String id, Integer seq, UpdateCommentRequest req) {
Comment comment = dao.getCommentBySeq(seq);
if (!comment.getId().equals(id)) {
System.out.println("μž‘μ„±μžλ§Œ λŒ“κΈ€μ„ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.");
return null;
}
Integer updatedRecordCount = dao.updateComment(new UpdateCommentParam(seq, req.getContent()));
if (updatedRecordCount != 1) {
System.out.println("λŒ“κΈ€ μˆ˜μ •μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
return null;
}
return new UpdateCommentResponse(updatedRecordCount);
}



μ‚¬μš©μž 둜그인 μš”μ²­ 데이터 검증과 μ‚¬μš©μž 인증 μ˜ˆμ™Έ 처리

μ‚¬μš©μžκ°€ νšŒμ›κ°€μž…κ³Ό λ‘œκ·ΈμΈμ„ μœ„ν•΄ μž…λ ₯ν•œ 데이터에 λŒ€ν•΄ Bean Validation 을 μ‚¬μš©ν•΄ 검증 κΈ°λŠ₯을 κ΅¬ν˜„ν–ˆλ‹€.

ν…œν”Œλ¦Ώ μ—”μ§„μœΌλ‘œ μ„œλ²„μ—μ„œ λ·°λ₯Ό κ·Έλ¦¬λŠ” 방식이 μ•„λ‹Œ, λ°μ΄ν„°λ§Œ μ „μ†‘ν•˜λŠ” API μ„œλ²„μ—μ„œ μž…λ ₯κ°’ 검증에 λŒ€ν•œ 였λ₯˜ λ©”μ‹œμ§€λ₯Ό μ–΄λ–»κ²Œ 전달해야 ν• μ§€ κ³ λ―Όν–ˆμ—ˆλ‹€.

Bean Validation μ—μ„œ λ˜μ§„ μ˜ˆμ™Έλ₯Ό λ°›μ•„μ„œ μ²˜λ¦¬ν•œ λ‹€μŒ 였λ₯˜ λ©”μ‹œμ§€λ₯Ό μ‘λ‹΅μœΌλ‘œ 내렀주도둝 κ΅¬ν˜„ν–ˆλ‹€.

  • νšŒμ›κ°€μž… μš”μ²­ 폼 데이터
public class JoinRequest {
@NotBlank
private String id;
@NotBlank
private String name;
@NotBlank
private String pwd;
@NotBlank
private String checkPwd;
@NotBlank
private String email;
  • νšŒμ› κ°€μž… μš”μ²­ 처리 컨트둀러
/* μš”μ²­ DTO 검증 μ˜ˆμ™Έμ²˜λ¦¬ ν•Έλ“€λŸ¬ */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
System.out.println("UserController handleMethodArgumentNotValidException " + new Date());
BindingResult bs = e.getBindingResult();
StringBuilder sb = new StringBuilder();
bs.getFieldErrors().forEach(err -> {
sb.append(String.format("[%s]: %s.\nμž…λ ₯된 κ°’: %s",
err.getField(), err.getDefaultMessage(), err.getRejectedValue()));
});
// Map 으둜 보낸닀면 ν”„λ‘ νŠΈμ—μ„œ μ‚¬μš©ν•˜κΈ° 더 νŽΈλ¦¬ν•  λ“― !
return new ResponseEntity<>(sb.toString(), HttpStatus.BAD_REQUEST);
}

λ˜ν•œ μ‚¬μš©μž κ΄€λ ¨ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ μ»€μŠ€ν…€ μ˜ˆμ™Έλ₯Ό λ§Œλ“€μ–΄μ„œ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚€κ³  ν•Έλ“€λŸ¬μ—μ„œ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ κ΅¬ν˜„ν–ˆλ‹€.

private void isExistUserId(String id) {
Integer result = dao.isExistUserId(id);
if (result == 1) {
throw new MemberException("이미 μ‚¬μš©μ€‘μΈ μ•„μ΄λ””μž…λ‹ˆλ‹€.", HttpStatus.BAD_REQUEST);
}
}
private void checkPwd(String pwd, String checkPwd) {
if (!pwd.equals(checkPwd)) {
throw new MemberException("λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", HttpStatus.BAD_REQUEST);
}
}
/* μ‚¬μš©μž κ΄€λ ¨ μš”μ²­ μ˜ˆμ™Έμ²˜λ¦¬ ν•Έλ“€λŸ¬ */
@ExceptionHandler(MemberException.class)
public ResponseEntity<?> handleUserException(MemberException e) {
System.out.println("UserController handlerUserException " + new Date());
return new ResponseEntity<>(e.getMessage(), e.getStatus());
}

@AdviceController λ₯Ό 더 많이 μ‚¬μš©ν•œλ‹€κ³  ν–ˆλŠ”λ°, 이 ν”„λ‘œμ νŠΈμ—μ„œλŠ” νšŒμ› κ΄€λ ¨λœ μ˜ˆμ™Έ μ²˜λ¦¬λ§Œμ„ μž‘μ„±ν•˜κΈ° μœ„ν•΄ μ „μ—­ μ˜ˆμ™Έμ²˜λ¦¬λ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ•˜λ‹€. λ˜ν•œ ν‰μ†Œμ—λ„ μ»¨νŠΈλ‘€λŸ¬λ³„λ‘œ μ²˜λ¦¬ν•˜λŠ” μ˜ˆμ™Έλ₯Ό μ„ΈλΆ„ν™” ν•˜λŠ”κ²Œ μ’‹μ§€ μ•Šμ„κΉŒ? λΌλŠ” 생각을 κ°€μ§€κ³  μžˆμ—ˆλ‹€.

ν•˜μ§€λ§Œ κ²°κ΅­ μ–΄λ–€ μ˜ˆμ™Έμ²˜λ¦¬λ“  μ„œλ²„μ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•œ λ‹€μŒ μš”μ²­μ„ 보낸 ν”„λ‘ νŠΈ μͺ½μœΌλ‘œ μƒνƒœ μ½”λ“œλ“  였λ₯˜ λ©”μ‹œμ§€λ“  λ‚΄λ €μ€˜μ•Ό ν•œλ‹€. λ”°λΌμ„œ μ»€μŠ€ν…€ μ˜ˆμ™Έμ™€ μ˜ˆμ™Έ λ©”μ‹œμ§€λ₯Ό λ²”μš©μ„± 있게 μž‘μ„±ν•˜λ©΄ 였히렀 μ „μ—­μ—μ„œ μ˜ˆμ™Έμ²˜λ¦¬ ν•˜λŠ”κ²Œ 일관성 μžˆμ„ κ²ƒμ΄λΌλŠ” κΉ¨λ‹¬μŒμ„ μ–»μ—ˆλ‹€.



DTO 클래슀 뢄리

μš”μ²­κ³Ό μ‘λ‹΅μœΌλ‘œ μ£Όκ³ λ°›λŠ” 데이터λ₯Ό ν•œ λˆˆμ— ν™•μΈν•˜κΈ° μœ„ν•΄ μš”μ²­ 데이터와 응닡 데이터λ₯Ό 각각의 DTO둜 λΆ„λ¦¬ν–ˆλ‹€.

μš”μ²­μœΌλ‘œ 받은 데이터λ₯Ό λ°”νƒ•μœΌλ‘œ SQL 쿼리λ₯Ό μˆ˜ν–‰ν•  λ•Œ ν•„μš”ν•œ λ°μ΄ν„°λ§Œμ„ λ„˜κ²¨μ£ΌκΈ° μœ„ν•΄ Serviceμ—μ„œ Dao둜 λ„˜κΈ°λŠ” νŒŒλΌλ―Έν„°λ„ DTO둜 λΆ„λ¦¬ν–ˆλ‹€.

project-structure

μ΄λ ‡κ²Œ κ΅¬ν˜„ν–ˆμ„ λ•Œμ˜ μž₯점은 컨트둀러 λ©”μ„œλ“œμ˜ νŒŒλΌλ―Έν„°λ‘œ λ§Žμ€ 인자λ₯Ό λ„˜κ²¨μ£Όμ§€ μ•Šμ•„λ„ λœλ‹€λŠ” 점, μ£Όκ³ λ°›λŠ” 데이터λ₯Ό ν™•μΈν•˜κ³  μˆ˜μ •ν•΄μ•Όν•˜λŠ” 경우 DTO 클래슀 λ§Œμ„ μˆ˜μ •ν•΄μ•Ό ν•˜λŠ” μ μ΄μ—ˆλ‹€.

ν•˜μ§€λ§Œ 단점은 κΈ°λŠ₯이 좔가될 λ•Œλ§ˆλ‹€ 클래슀 파일이 λŠ˜μ–΄λ‚˜ 관리가 νž˜λ“€μ–΄μ‘Œκ³ , 데이터λ₯Ό ν•œ λˆˆμ— 확인할 수 μžˆμ§€λ§Œ ν™•μΈν•˜κΈ° μœ„ν•΄μ„œλŠ” 직접 클래슀 νŒŒμΌμ„ 열어봐야 ν•œλ‹€λŠ” 점 μ΄μ—ˆλ‹€.

μš”μ²­ νŒŒλΌλ―Έν„°κ°€ λ§Žμ§€ μ•Šμ€ 경우 (2-3개) ꡳ이 클래슀 파일둜 λ”°λ‘œ μž‘μ„±ν•˜μ§€ μ•Šκ³  컨트둀러 λ©”μ„œλ“œμ— μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ λ§€ν•‘ν•˜λŠ” 것이 μ½”λ“œμ˜ 가독성과 μœ μ§€λ³΄μˆ˜ μΈ‘λ©΄μ—μ„œ 쒋은 방법이 될 것이라 μƒκ°λœλ‹€.



쑰회수 쀑볡 μΉ΄μš΄νŒ… 예방

κ²Œμ‹œκΈ€ 쑰회수 쀑볡 μΉ΄μš΄νŒ… μ˜ˆλ°©μ„ μœ„ν•΄ read_history ν…Œμ΄λΈ”μ„ 두어 κ΅¬ν˜„ν–ˆλ‹€.

Cookie 의 경우 ν•˜λ‚˜μ˜ 도메인 λ‹Ή μ‚¬μš©ν•  수 μžˆλŠ” κ°œμˆ˜κ°€ μ œν•œλ˜κΈ° λ•Œλ¬Έμ—, μ—¬λŸ¬ κ²Œμ‹œκΈ€μ„ μ‘°νšŒν•˜λ©΄ μ œν•œ 개수λ₯Ό λ„˜μ–΄λ²„λ¦΄ 것이라 μ˜ˆμƒν–ˆλ‹€. κ·Έλž˜μ„œ μ‚¬μš©μžκ°€ 이미 읽은 κ²Œμ‹œκΈ€μΈμ§€ 확인할 수 μžˆλ„λ‘ μ„œλ²„μ—μ„œ μ‚¬μš©μž 아이디 κ²Œμ‹œκΈ€ 쑰회 μ‹œκ°„ 을 두어 μ²΄ν¬ν–ˆλ‹€.

/* νŠΉμ • κΈ€ */
/* 쑰회수 μˆ˜μ • */
public BbsResponse getBbs(Integer seq, String readerId) {
// 둜그인 ν•œ μ‚¬μš©μžμ˜ 쑰회수만 μΉ΄μš΄νŒ…
if (!readerId.isEmpty()) {
CreateReadCountParam param = new CreateReadCountParam(seq, readerId);
Integer result = dao.createBbsReadCountHistory(param); // 쑰회수 νžˆμŠ€ν† λ¦¬ 처리 (insert: 1, update: 2)
if (result == 1) {
Integer updatedRecordCount = dao.increaseBbsReadCount(seq); // 쑰회수 증가
}
}
return new BbsResponse(dao.getBbs(seq));
}

μŠ€μΌ€μ₯΄λŸ¬λ₯Ό κ΅¬ν˜„ν•΄ 24μ‹œκ°„ λ‹¨μœ„λ‘œ 쑰회수λ₯Ό μ΄ˆκΈ°ν™” μ‹œμΌœμ„œ λ‹€μŒλ‚ μ΄ 되면 μ‘°νšŒμˆ˜κ°€ λ‹€μ‹œ μΉ΄μš΄νŒ… 될 수 μžˆκ²Œλ” κ΅¬ν˜„ν•˜λ©΄ 쒋을 것 κ°™λ‹€. μ•„λ‹ˆλ©΄ μ‘°νšŒμˆ˜λΌλŠ” 데이터λ₯Ό κ΄€λ¦¬ν•˜μ§€ μ•Šκ³  읽은 κΈ€, 읽지 μ•Šμ€ κΈ€ 둜 관리될 수 μžˆλ„λ‘ κ΅¬ν˜„ν•˜λŠ” 것도 ν•˜λ‚˜μ˜ 쒋은 방법이라 μƒκ°ν•œλ‹€.

About

πŸ‘» κ²Œμ‹œνŒ (Spring Boot + React)

Topics

Resources

License

Stars

Watchers

Forks

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /