diff --git a/.gitignore b/.gitignore index c2065bc..0d1258f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ bin/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +src/main/resources/application.yml ### NetBeans ### /nbproject/private/ diff --git a/build.gradle b/build.gradle index 728ec9c..6815fc3 100644 --- a/build.gradle +++ b/build.gradle @@ -16,11 +16,17 @@ repositories { } dependencies { + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' runtimeOnly 'com.mysql:mysql-connector-j' testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-aop' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' } tasks.named('test') { diff --git a/src/main/java/com/example/demo/config/SecurityConfig.java b/src/main/java/com/example/demo/config/SecurityConfig.java new file mode 100644 index 0000000..f106f42 --- /dev/null +++ b/src/main/java/com/example/demo/config/SecurityConfig.java @@ -0,0 +1,54 @@ +package com.example.demo.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((auth) -> auth + .requestMatchers("/members/home","/members/register","/members/signup", "/members/view","/members/signin").permitAll() + .requestMatchers("/members/admin").hasRole("ADMIN") + .requestMatchers("/my/**").hasAnyRole("ADMIN", "USER") + .anyRequest().authenticated() + ); + + http + .formLogin((auth) -> auth.loginPage("/members/signin") + .loginProcessingUrl("/members/view") + .permitAll() + ); + + http + .csrf(AbstractHttpConfigurer::disable); + + http + .sessionManagement((auth)-> auth + .maximumSessions(1) + .maxSessionsPreventsLogin(true)); + + http + .sessionManagement((auth)-> auth + .sessionFixation().changeSessionId()); + + return http.build(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/src/main/java/com/example/demo/controller/SignController.java b/src/main/java/com/example/demo/controller/SignController.java new file mode 100644 index 0000000..4492ea7 --- /dev/null +++ b/src/main/java/com/example/demo/controller/SignController.java @@ -0,0 +1,59 @@ +package com.example.demo.controller; + +import com.example.demo.controller.dto.request.MemberCreateRequest; +import com.example.demo.controller.dto.response.MemberResponse; +import com.example.demo.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; + +@Controller +public class SignController { + + private final MemberService memberService; + + @Autowired + public SignController(MemberService memberService) { + this.memberService = memberService; + } + + @PostMapping("/members/register") + public String register(MemberCreateRequest request) { + MemberResponse response = memberService.create(request); + ResponseEntity.ok(response); + return "redirect:/members/signin"; + } + + @PostMapping("/members/view") + public String login() { + return "redirect:/members/home"; + } + + @GetMapping("/members/signup") + public String signUp() { + return "signup"; + } + + @GetMapping("/members/signin") + public String signIn() { + return "signin"; + } + + @GetMapping("/members/home") + public String home(Model model) { + String id = SecurityContextHolder.getContext().getAuthentication().getName(); + + model.addAttribute("id", id); + return "home"; + } + + @GetMapping("/members/admin") + public String admin() { + return "admin"; + } + +} diff --git a/src/main/java/com/example/demo/controller/dto/MemberDetails.java b/src/main/java/com/example/demo/controller/dto/MemberDetails.java new file mode 100644 index 0000000..9900d86 --- /dev/null +++ b/src/main/java/com/example/demo/controller/dto/MemberDetails.java @@ -0,0 +1,57 @@ +package com.example.demo.controller.dto; + +import com.example.demo.domain.Member; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public class MemberDetails implements UserDetails { + + private final Member member; + + + public MemberDetails(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + Collection collection = new ArrayList<>(); + + collection.add((GrantedAuthority) member::getRole); + + return collection; + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/example/demo/controller/dto/request/ArticleUpdateRequest.java b/src/main/java/com/example/demo/controller/dto/request/ArticleUpdateRequest.java index aced461..1177957 100644 --- a/src/main/java/com/example/demo/controller/dto/request/ArticleUpdateRequest.java +++ b/src/main/java/com/example/demo/controller/dto/request/ArticleUpdateRequest.java @@ -1,6 +1,7 @@ package com.example.demo.controller.dto.request; public record ArticleUpdateRequest( + Long authorId, Long boardId, String title, String description diff --git a/src/main/java/com/example/demo/controller/dto/response/MemberResponse.java b/src/main/java/com/example/demo/controller/dto/response/MemberResponse.java index f79aeb6..48cdb29 100644 --- a/src/main/java/com/example/demo/controller/dto/response/MemberResponse.java +++ b/src/main/java/com/example/demo/controller/dto/response/MemberResponse.java @@ -4,12 +4,12 @@ public record MemberResponse( Long id, - String name, + String username, String email ) { public static MemberResponse from(Member member) { - return new MemberResponse(member.getId(), member.getName(), member.getEmail()); + return new MemberResponse(member.getId(), member.getUsername(), member.getEmail()); } } diff --git a/src/main/java/com/example/demo/domain/Article.java b/src/main/java/com/example/demo/domain/Article.java index e0183db..cfd6227 100644 --- a/src/main/java/com/example/demo/domain/Article.java +++ b/src/main/java/com/example/demo/domain/Article.java @@ -1,17 +1,46 @@ package com.example.demo.domain; +import jakarta.persistence.*; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.time.LocalDateTime; +@Entity +@Table(name = "article") +@NoArgsConstructor +@Getter +@Setter public class Article { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(name = "author_id", nullable = false) private Long authorId; + + @Column(name = "board_id", nullable = false) private Long boardId; + + @Column(name = "title", nullable = false) private String title; + + @Column(name = "content", nullable = false) private String content; + + @Column(name = "created_date") private LocalDateTime createdAt; + + @Column(name = "modified_date") private LocalDateTime modifiedAt; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id", insertable = false, updatable = false) + private Board board; + public Article( Long id, Long authorId, @@ -47,39 +76,5 @@ public void update(Long boardId, String title, String description) { this.modifiedAt = LocalDateTime.now(); } - public void setId(Long id) { - this.id = id; - } - - public void setModifiedAt(LocalDateTime modifiedAt) { - this.modifiedAt = modifiedAt; - } - public Long getId() { - return id; - } - - public Long getAuthorId() { - return authorId; - } - - public Long getBoardId() { - return boardId; - } - - public String getTitle() { - return title; - } - - public String getContent() { - return content; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public LocalDateTime getModifiedAt() { - return modifiedAt; - } } diff --git a/src/main/java/com/example/demo/domain/Board.java b/src/main/java/com/example/demo/domain/Board.java index 992e2c6..1dc1ae6 100644 --- a/src/main/java/com/example/demo/domain/Board.java +++ b/src/main/java/com/example/demo/domain/Board.java @@ -1,10 +1,30 @@ package com.example.demo.domain; +import jakarta.persistence.*; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "board") +@NoArgsConstructor +@Getter @Setter public class Board { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(name = "name", nullable = false) private String name; + @OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true) + private List
articles = new ArrayList<>(); + public Board(Long id, String name) { this.id = id; this.name = name; @@ -14,18 +34,6 @@ public Board(String name) { this.name = name; } - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - public void update(String name) { this.name = name; } diff --git a/src/main/java/com/example/demo/domain/Member.java b/src/main/java/com/example/demo/domain/Member.java index fe80d6b..5353002 100644 --- a/src/main/java/com/example/demo/domain/Member.java +++ b/src/main/java/com/example/demo/domain/Member.java @@ -1,47 +1,51 @@ package com.example.demo.domain; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "member") +@NoArgsConstructor +@Getter +@Setter public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String name; + + @Column(name = "username", nullable = false, unique = true) + private String username; + + @Column(name = "email", nullable = false) private String email; + + @Column(name = "password", nullable = false) private String password; - public Member(Long id, String name, String email, String password) { + @Column(name = "role", nullable = false) + private String role; + + public Member(Long id, String username, String email, String password, String role) { this.id = id; - this.name = name; + this.username = username; this.email = email; this.password = password; + this.role = role; } - public Member(String name, String email, String password) { - this.name = name; + public Member(String username, String email, String password, String role) { + this.username = username; this.email = email; this.password = password; + this.role = role; } - public void update(String name, String email) { - this.name = name; + public void update(String username, String email) { + this.username = username; this.email = email; } - public void setId(Long id) { - this.id = id; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } } diff --git a/src/main/java/com/example/demo/exception/AlreadyHasEmailException.java b/src/main/java/com/example/demo/exception/AlreadyHasEmailException.java new file mode 100644 index 0000000..6b01e5c --- /dev/null +++ b/src/main/java/com/example/demo/exception/AlreadyHasEmailException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class AlreadyHasEmailException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/ArticleNotFoundException.java b/src/main/java/com/example/demo/exception/ArticleNotFoundException.java new file mode 100644 index 0000000..2303f33 --- /dev/null +++ b/src/main/java/com/example/demo/exception/ArticleNotFoundException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class ArticleNotFoundException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/BoardHasArticleException.java b/src/main/java/com/example/demo/exception/BoardHasArticleException.java new file mode 100644 index 0000000..da50657 --- /dev/null +++ b/src/main/java/com/example/demo/exception/BoardHasArticleException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class BoardHasArticleException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/BoardNotExistException.java b/src/main/java/com/example/demo/exception/BoardNotExistException.java new file mode 100644 index 0000000..c5e3119 --- /dev/null +++ b/src/main/java/com/example/demo/exception/BoardNotExistException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class BoardNotExistException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/BoardNotFoundException.java b/src/main/java/com/example/demo/exception/BoardNotFoundException.java new file mode 100644 index 0000000..07e0380 --- /dev/null +++ b/src/main/java/com/example/demo/exception/BoardNotFoundException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class BoardNotFoundException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/ErrorCode.java b/src/main/java/com/example/demo/exception/ErrorCode.java new file mode 100644 index 0000000..411ae12 --- /dev/null +++ b/src/main/java/com/example/demo/exception/ErrorCode.java @@ -0,0 +1,30 @@ +package com.example.demo.exception; + +import lombok.Getter; + +@Getter +public enum ErrorCode { + REQUEST_NULL_VALUE(400,"BAD REQUEST", "요청 메시지에 null 값이 존재합니다."), + BOARD_NOT_EXIST(400, "BAD REQUEST", "해당 게시판을 찾을 수 없습니다."), + MEMBER_NOT_EXIST(400, "BAD REQUEST", "해당 사용자를 찾을 수 없습니다."), + + ARTICLE_NOT_EXIST(404,"NOT FOUND", "해당 게시물을 찾을 수 없습니다."), + BOARD_NOT_FOUND(404, "NOT FOUND", "해당 게시판을 찾을 수 없습니다."), + MEMBER_NOT_FOUND(404, "NOT FOUND", "해당 사용자를 찾을 수 없습니다."), + + EMAIL_EXIST(409,"CONFLICT", "해당 이메일이 존재합니다."), + INVALID_MEMBER_OR_BOARD(400, "BAD REQUEST", "존재하지 않는 사용자, 게시판을 참조하고 있습니다."), + + BOARD_ARTICLE_EXIST(400, "BAD REQUEST", "게시판에 작성된 게시물이 존재합니다."), + MEMBER_ARTICLE_EXIST(400, "BAD REQUEST", "사용자가 작성한 게시물이 존재합니다."); + + private final int code; + private final String status; + private final String message; + + ErrorCode(int code, String status, String message) { + this.status = status; + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/com/example/demo/exception/ErrorResponse.java b/src/main/java/com/example/demo/exception/ErrorResponse.java new file mode 100644 index 0000000..1d8250a --- /dev/null +++ b/src/main/java/com/example/demo/exception/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.example.demo.exception; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ErrorResponse { + private final LocalDateTime timeStamp = LocalDateTime.now(); + private final int code; + private final String status; + private final String message; + + public ErrorResponse(ErrorCode errorCode) { + this.status = errorCode.getStatus(); + this.code = errorCode.getCode(); + this.message = errorCode.getMessage(); + } +} diff --git a/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..98e615c --- /dev/null +++ b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java @@ -0,0 +1,64 @@ +package com.example.demo.exception; + +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(RequestNullExistException.class) + public ResponseEntity requestNullExistException() { + ErrorResponse response = new ErrorResponse(ErrorCode.REQUEST_NULL_VALUE); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(400)); + } + + @ExceptionHandler(MemberNotExistException.class) + public ResponseEntity memberNotExistException() { + ErrorResponse response = new ErrorResponse(ErrorCode.MEMBER_NOT_EXIST); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(400)); + } + + @ExceptionHandler(BoardNotExistException.class) + public ResponseEntity boardNotExistException() { + ErrorResponse response = new ErrorResponse(ErrorCode.BOARD_NOT_EXIST); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(400)); + } + + @ExceptionHandler(ArticleNotFoundException.class) + public ResponseEntity articleNotFoundException() { + ErrorResponse response = new ErrorResponse(ErrorCode.ARTICLE_NOT_EXIST); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(404)); + } + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity memberNotFoundException() { + ErrorResponse response = new ErrorResponse(ErrorCode.MEMBER_NOT_FOUND); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(404)); + } + + @ExceptionHandler(BoardNotFoundException.class) + public ResponseEntity boardNotFoundException() { + ErrorResponse response = new ErrorResponse(ErrorCode.BOARD_NOT_FOUND); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(404)); + } + + @ExceptionHandler(AlreadyHasEmailException.class) + public ResponseEntity alreadyHasEmailException() { + ErrorResponse response = new ErrorResponse(ErrorCode.EMAIL_EXIST); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(409)); + } + + @ExceptionHandler(MemberHasArticleException.class) + public ResponseEntity memberHasArticleException() { + ErrorResponse response = new ErrorResponse(ErrorCode.MEMBER_ARTICLE_EXIST); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(400)); + } + + @ExceptionHandler(BoardHasArticleException.class) + public ResponseEntity boardHasArticleException() { + ErrorResponse response = new ErrorResponse(ErrorCode.BOARD_ARTICLE_EXIST); + return new ResponseEntity<>(response, HttpStatusCode.valueOf(400)); + } +} diff --git a/src/main/java/com/example/demo/exception/MemberHasArticleException.java b/src/main/java/com/example/demo/exception/MemberHasArticleException.java new file mode 100644 index 0000000..5e51fcd --- /dev/null +++ b/src/main/java/com/example/demo/exception/MemberHasArticleException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class MemberHasArticleException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/MemberNotExistException.java b/src/main/java/com/example/demo/exception/MemberNotExistException.java new file mode 100644 index 0000000..f7a4bdc --- /dev/null +++ b/src/main/java/com/example/demo/exception/MemberNotExistException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class MemberNotExistException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/MemberNotFoundException.java b/src/main/java/com/example/demo/exception/MemberNotFoundException.java new file mode 100644 index 0000000..daa2843 --- /dev/null +++ b/src/main/java/com/example/demo/exception/MemberNotFoundException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class MemberNotFoundException extends RuntimeException { + +} diff --git a/src/main/java/com/example/demo/exception/RequestNullExistException.java b/src/main/java/com/example/demo/exception/RequestNullExistException.java new file mode 100644 index 0000000..9566e54 --- /dev/null +++ b/src/main/java/com/example/demo/exception/RequestNullExistException.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class RequestNullExistException extends RuntimeException{ + +} diff --git a/src/main/java/com/example/demo/repository/ArticleRepository.java b/src/main/java/com/example/demo/repository/ArticleRepository.java index be3ebd4..917e52c 100644 --- a/src/main/java/com/example/demo/repository/ArticleRepository.java +++ b/src/main/java/com/example/demo/repository/ArticleRepository.java @@ -4,19 +4,10 @@ import com.example.demo.domain.Article; -public interface ArticleRepository { +import org.springframework.data.jpa.repository.JpaRepository; - List
findAll(); +public interface ArticleRepository extends JpaRepository { List
findAllByBoardId(Long boardId); - List
findAllByMemberId(Long memberId); - - Article findById(Long id); - - Article insert(Article article); - - Article update(Article article); - - void deleteById(Long id); } diff --git a/src/main/java/com/example/demo/repository/ArticleRepositoryJdbc.java b/src/main/java/com/example/demo/repository/ArticleRepositoryJdbc.java deleted file mode 100644 index c9a272e..0000000 --- a/src/main/java/com/example/demo/repository/ArticleRepositoryJdbc.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.example.demo.repository; - -import java.sql.PreparedStatement; -import java.util.List; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -import com.example.demo.domain.Article; - -@Repository -public class ArticleRepositoryJdbc implements ArticleRepository { - - private final JdbcTemplate jdbcTemplate; - - public ArticleRepositoryJdbc(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - private static final RowMapper
articleRowMapper = (rs, rowNum) -> new Article( - rs.getLong("id"), - rs.getLong("author_id"), - rs.getLong("board_id"), - rs.getString("title"), - rs.getString("content"), - rs.getTimestamp("created_date").toLocalDateTime(), - rs.getTimestamp("modified_date").toLocalDateTime() - ); - - @Override - public List
findAll() { - return jdbcTemplate.query(""" - SELECT id, board_id, author_id, title, content, created_date, modified_date - FROM article - """, articleRowMapper); - } - - @Override - public List
findAllByBoardId(Long boardId) { - return jdbcTemplate.query(""" - SELECT id, board_id, author_id, title, content, created_date, modified_date - FROM article - WHERE board_id = ? - """, articleRowMapper, boardId); - } - - @Override - public List
findAllByMemberId(Long memberId) { - return jdbcTemplate.query(""" - SELECT id, board_id, author_id, title, content, created_date, modified_date - FROM article - WHERE author_id = ? - """, articleRowMapper, memberId); - } - - @Override - public Article findById(Long id) { - return jdbcTemplate.queryForObject(""" - SELECT id, board_id, author_id, title, content, created_date, modified_date - FROM article - WHERE id = ? - """, articleRowMapper, id); - } - - @Override - public Article insert(Article article) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement(""" - INSERT INTO article (board_id, author_id, title, content) - VALUES (?, ?, ?, ?) - """, - new String[]{"id"}); - ps.setLong(1, article.getBoardId()); - ps.setLong(2, article.getAuthorId()); - ps.setString(3, article.getTitle()); - ps.setString(4, article.getContent()); - return ps; - }, keyHolder); - return findById(keyHolder.getKey().longValue()); - } - - @Override - public Article update(Article article) { - jdbcTemplate.update(""" - UPDATE article - SET board_id = ?, title = ?, content = ? - WHERE id = ? - """, - article.getBoardId(), - article.getTitle(), - article.getContent(), - article.getId() - ); - return findById(article.getId()); - } - - @Override - public void deleteById(Long id) { - jdbcTemplate.update(""" - DELETE FROM article - WHERE id = ? - """, id); - } -} diff --git a/src/main/java/com/example/demo/repository/ArticleRepositoryMemory.java b/src/main/java/com/example/demo/repository/ArticleRepositoryMemory.java deleted file mode 100644 index 13ba78b..0000000 --- a/src/main/java/com/example/demo/repository/ArticleRepositoryMemory.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.demo.repository; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; - -import com.example.demo.domain.Article; - -public class ArticleRepositoryMemory implements ArticleRepository { - - private static final Map articles = new HashMap<>(); - private static final AtomicLong autoincrement = new AtomicLong(1); - - @Override - public List
findAll() { - return articles.entrySet().stream() - .map(it -> { - Article article = it.getValue(); - article.setId(it.getKey()); - return article; - }).toList(); - } - - @Override - public List
findAllByBoardId(Long boardId) { - return articles.entrySet().stream() - .filter(it -> it.getValue().getBoardId().equals(boardId)) - .map(it -> { - Article article = it.getValue(); - article.setId(it.getKey()); - return article; - }).toList(); - } - - @Override - public List
findAllByMemberId(Long memberId) { - return articles.entrySet().stream() - .filter(it -> it.getValue().getAuthorId().equals(memberId)) - .map(it -> { - Article article = it.getValue(); - article.setId(it.getKey()); - return article; - }).toList(); - } - - @Override - public Article findById(Long id) { - return articles.getOrDefault(id, null); - } - - @Override - public Article insert(Article article) { - long id = autoincrement.getAndIncrement(); - articles.put(id, article); - article.setId(id); - return article; - } - - @Override - public Article update(Article article) { - articles.put(article.getId(), article); - return article; - } - - @Override - public void deleteById(Long id) { - articles.remove(id); - } -} diff --git a/src/main/java/com/example/demo/repository/BoardRepository.java b/src/main/java/com/example/demo/repository/BoardRepository.java index cc2dfd0..3a5da9b 100644 --- a/src/main/java/com/example/demo/repository/BoardRepository.java +++ b/src/main/java/com/example/demo/repository/BoardRepository.java @@ -1,18 +1,9 @@ package com.example.demo.repository; -import java.util.List; - import com.example.demo.domain.Board; -public interface BoardRepository { - - List findAll(); - - Board findById(Long id); - - Board insert(Board board); +import org.springframework.data.jpa.repository.JpaRepository; - void deleteById(Long id); +public interface BoardRepository extends JpaRepository { - Board update(Board board); } diff --git a/src/main/java/com/example/demo/repository/BoardRepositoryJdbc.java b/src/main/java/com/example/demo/repository/BoardRepositoryJdbc.java deleted file mode 100644 index c4fd6f6..0000000 --- a/src/main/java/com/example/demo/repository/BoardRepositoryJdbc.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.example.demo.repository; - -import java.sql.PreparedStatement; -import java.util.List; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -import com.example.demo.domain.Board; - -@Repository -public class BoardRepositoryJdbc implements BoardRepository { - - private final JdbcTemplate jdbcTemplate; - - public BoardRepositoryJdbc(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - private static final RowMapper boardRowMapper = (rs, rowNum) -> new Board( - rs.getLong("id"), - rs.getString("name") - ); - - @Override - public List findAll() { - return jdbcTemplate.query(""" - SELECT id, name - FROM board - """, boardRowMapper); - } - - @Override - public Board findById(Long id) { - return jdbcTemplate.queryForObject(""" - SELECT id, name - FROM board - WHERE id = ? - """, boardRowMapper, id); - } - - @Override - public Board insert(Board board) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement(""" - INSERT INTO board (name) VALUES (?) - """, new String[]{"id"}); - ps.setString(1, board.getName()); - return ps; - }, keyHolder); - return findById(keyHolder.getKey().longValue()); - } - - @Override - public void deleteById(Long id) { - jdbcTemplate.update(""" - DELETE FROM board WHERE id = ? - """, id); - } - - @Override - public Board update(Board board) { - return jdbcTemplate.queryForObject(""" - UPDATE board SET name = ? WHERE id = ? - """, boardRowMapper, board.getName(), board.getId() - ); - } -} diff --git a/src/main/java/com/example/demo/repository/BoardRepositoryMemory.java b/src/main/java/com/example/demo/repository/BoardRepositoryMemory.java deleted file mode 100644 index 8cf5ecf..0000000 --- a/src/main/java/com/example/demo/repository/BoardRepositoryMemory.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.demo.repository; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; - -import com.example.demo.domain.Board; - -public class BoardRepositoryMemory implements BoardRepository { - - private static final Map boards = new HashMap<>(); - private static final AtomicLong autoincrement = new AtomicLong(1); - - static { - // 1번 게시판을 미리 만들어둔다. - boards.put(autoincrement.getAndIncrement(), new Board("자유게시판")); - } - - @Override - public List findAll() { - return boards.entrySet().stream() - .map(it -> { - Board board = it.getValue(); - board.setId(it.getKey()); - return board; - }).toList(); - } - - @Override - public Board findById(Long id) { - return boards.getOrDefault(id, null); - } - - @Override - public Board insert(Board board) { - boards.put(autoincrement.getAndIncrement(), board); - return board; - } - - @Override - public void deleteById(Long id) { - boards.remove(id); - } - - @Override - public Board update(Board board) { - return boards.put(board.getId(), board); - } -} diff --git a/src/main/java/com/example/demo/repository/MemberRepository.java b/src/main/java/com/example/demo/repository/MemberRepository.java index 8e2ad14..50ed367 100644 --- a/src/main/java/com/example/demo/repository/MemberRepository.java +++ b/src/main/java/com/example/demo/repository/MemberRepository.java @@ -1,18 +1,14 @@ package com.example.demo.repository; -import java.util.List; - import com.example.demo.domain.Member; -public interface MemberRepository { - - List findAll(); - - Member findById(Long id); +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; - Member insert(Member member); +import java.util.Optional; - Member update(Member member); +public interface MemberRepository extends JpaRepository { + boolean existsByEmail(String email); - void deleteById(Long id); + Member findMemberByUsername(String username); } diff --git a/src/main/java/com/example/demo/repository/MemberRepositoryJdbc.java b/src/main/java/com/example/demo/repository/MemberRepositoryJdbc.java deleted file mode 100644 index 30d2262..0000000 --- a/src/main/java/com/example/demo/repository/MemberRepositoryJdbc.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.example.demo.repository; - -import java.sql.PreparedStatement; -import java.util.List; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -import com.example.demo.domain.Member; - -@Repository -public class MemberRepositoryJdbc implements MemberRepository { - - private final JdbcTemplate jdbcTemplate; - - public MemberRepositoryJdbc(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - private static final RowMapper memberRowMapper = (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("password") - ); - - @Override - public List findAll() { - return jdbcTemplate.query(""" - SELECT id, name, email, password - FROM member - """, memberRowMapper); - } - - @Override - public Member findById(Long id) { - return jdbcTemplate.queryForObject(""" - SELECT id, name, email, password - FROM member - WHERE id = ? - """, memberRowMapper, id); - } - - @Override - public Member insert(Member member) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(con -> { - PreparedStatement ps = con.prepareStatement(""" - INSERT INTO member (name, email, password) VALUES (?, ?, ?) - """, new String[]{"id"}); - ps.setString(1, member.getName()); - ps.setString(2, member.getEmail()); - ps.setString(3, member.getPassword()); - return ps; - }, keyHolder); - return findById(keyHolder.getKey().longValue()); - } - - @Override - public Member update(Member member) { - jdbcTemplate.update(""" - UPDATE member - SET name = ?, email = ? - WHERE id = ? - """, member.getName(), member.getEmail(), member.getId()); - return findById(member.getId()); - } - - @Override - public void deleteById(Long id) { - jdbcTemplate.update(""" - DELETE FROM member - WHERE id = ? - """, id); - } -} diff --git a/src/main/java/com/example/demo/repository/MemberRepositoryMemory.java b/src/main/java/com/example/demo/repository/MemberRepositoryMemory.java deleted file mode 100644 index b4cf722..0000000 --- a/src/main/java/com/example/demo/repository/MemberRepositoryMemory.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.demo.repository; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; - -import com.example.demo.domain.Member; - -public class MemberRepositoryMemory implements MemberRepository { - - private static final Map members = new HashMap<>(); - private static final AtomicLong autoincrement = new AtomicLong(1); - - static { - // 1번 유저를 미리 만들어둔다. - members.put(autoincrement.getAndIncrement(), new Member("최준호", "temp@gmail.com", "password")); - } - - @Override - public List findAll() { - return members.entrySet().stream() - .map(it -> { - Member member = it.getValue(); - member.setId(it.getKey()); - return member; - }).toList(); - } - - @Override - public Member findById(Long id) { - return members.getOrDefault(id, null); - } - - @Override - public Member insert(Member member) { - long id = autoincrement.getAndIncrement(); - members.put(id, member); - member.setId(id); - return member; - } - - @Override - public Member update(Member member) { - return members.put(member.getId(), member); - } - - @Override - public void deleteById(Long id) { - members.remove(id); - } -} diff --git a/src/main/java/com/example/demo/service/ArticleService.java b/src/main/java/com/example/demo/service/ArticleService.java index 7f8610b..5a291bd 100644 --- a/src/main/java/com/example/demo/service/ArticleService.java +++ b/src/main/java/com/example/demo/service/ArticleService.java @@ -2,15 +2,18 @@ import java.util.List; +import com.example.demo.exception.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.demo.controller.dto.request.ArticleCreateRequest; import com.example.demo.controller.dto.response.ArticleResponse; import com.example.demo.controller.dto.request.ArticleUpdateRequest; + import com.example.demo.domain.Article; import com.example.demo.domain.Board; import com.example.demo.domain.Member; + import com.example.demo.repository.ArticleRepository; import com.example.demo.repository.BoardRepository; import com.example.demo.repository.MemberRepository; @@ -34,9 +37,12 @@ public ArticleService( } public ArticleResponse getById(Long id) { - Article article = articleRepository.findById(id); - Member member = memberRepository.findById(article.getAuthorId()); - Board board = boardRepository.findById(article.getBoardId()); + Article article = articleRepository.findById(id).orElseThrow(ArticleNotFoundException::new); + + Member member = memberRepository.findById(article.getAuthorId()).orElseThrow(MemberNotExistException::new); + + Board board = boardRepository.findById(article.getBoardId()).orElseThrow(BoardNotExistException::new); + return ArticleResponse.of(article, member, board); } @@ -44,8 +50,8 @@ public List getByBoardId(Long boardId) { List
articles = articleRepository.findAllByBoardId(boardId); return articles.stream() .map(article -> { - Member member = memberRepository.findById(article.getAuthorId()); - Board board = boardRepository.findById(article.getBoardId()); + Member member = memberRepository.findById(article.getAuthorId()).orElseThrow(MemberNotExistException::new); + Board board = boardRepository.findById(article.getBoardId()).orElseThrow(BoardNotExistException::new); return ArticleResponse.of(article, member, board); }) .toList(); @@ -59,19 +65,29 @@ public ArticleResponse create(ArticleCreateRequest request) { request.title(), request.description() ); - Article saved = articleRepository.insert(article); - Member member = memberRepository.findById(saved.getAuthorId()); - Board board = boardRepository.findById(saved.getBoardId()); + + if (request.boardId() == null || request.authorId() == null || request.title() == null || request.description() == null) { + throw new RequestNullExistException(); + } + + Member member = memberRepository.findById(request.authorId()).orElseThrow(MemberNotExistException::new); + + Board board = boardRepository.findById(request.boardId()).orElseThrow(BoardNotExistException::new); + + Article saved = articleRepository.save(article); + return ArticleResponse.of(saved, member, board); } @Transactional public ArticleResponse update(Long id, ArticleUpdateRequest request) { - Article article = articleRepository.findById(id); + Article article = articleRepository.findById(id).orElseThrow(ArticleNotFoundException::new); + + Member member = memberRepository.findById(request.authorId()).orElseThrow(MemberNotExistException::new); + + Board board = boardRepository.findById(request.boardId()).orElseThrow(BoardNotExistException::new); + article.update(request.boardId(), request.title(), request.description()); - Article updated = articleRepository.update(article); - Member member = memberRepository.findById(updated.getAuthorId()); - Board board = boardRepository.findById(article.getBoardId()); return ArticleResponse.of(article, member, board); } diff --git a/src/main/java/com/example/demo/service/BoardService.java b/src/main/java/com/example/demo/service/BoardService.java index ffff891..a17290f 100644 --- a/src/main/java/com/example/demo/service/BoardService.java +++ b/src/main/java/com/example/demo/service/BoardService.java @@ -1,6 +1,16 @@ package com.example.demo.service; import java.util.List; +import java.util.Objects; + +import com.example.demo.domain.Article; + +import com.example.demo.exception.BoardHasArticleException; +import com.example.demo.exception.BoardNotExistException; +import com.example.demo.exception.BoardNotFoundException; +import com.example.demo.exception.RequestNullExistException; + +import com.example.demo.repository.ArticleRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -8,6 +18,7 @@ import com.example.demo.controller.dto.request.BoardCreateRequest; import com.example.demo.controller.dto.request.BoardUpdateRequest; import com.example.demo.controller.dto.response.BoardResponse; + import com.example.demo.domain.Board; import com.example.demo.repository.BoardRepository; @@ -15,9 +26,11 @@ @Transactional(readOnly = true) public class BoardService { + private final ArticleRepository articleRepository; private final BoardRepository boardRepository; - public BoardService(BoardRepository boardRepository) { + public BoardService(ArticleRepository articleRepository, BoardRepository boardRepository) { + this.articleRepository = articleRepository; this.boardRepository = boardRepository; } @@ -28,27 +41,40 @@ public List getBoards() { } public BoardResponse getBoardById(Long id) { - Board board = boardRepository.findById(id); + Board board = boardRepository.findById(id).orElseThrow(BoardNotFoundException::new); return BoardResponse.from(board); } @Transactional public BoardResponse createBoard(BoardCreateRequest request) { Board board = new Board(request.name()); - Board saved = boardRepository.insert(board); + + if(board.getName() == null) { + throw new RequestNullExistException(); + } + + Board saved = boardRepository.save(board); return BoardResponse.from(saved); } @Transactional public void deleteBoard(Long id) { + List
allArticles = articleRepository.findAll(); + boolean boardHasArticles = allArticles.stream() + .anyMatch(article -> Objects.equals(article.getAuthorId(), id)); + + if (boardHasArticles) { + throw new BoardHasArticleException(); + } boardRepository.deleteById(id); } @Transactional public BoardResponse update(Long id, BoardUpdateRequest request) { - Board board = boardRepository.findById(id); + Board board = boardRepository.findById(id).orElseThrow(BoardNotExistException::new); + board.update(request.name()); - Board updated = boardRepository.update(board); + Board updated = boardRepository.save(board); return BoardResponse.from(updated); } } diff --git a/src/main/java/com/example/demo/service/MemberDetailsService.java b/src/main/java/com/example/demo/service/MemberDetailsService.java new file mode 100644 index 0000000..6e8d149 --- /dev/null +++ b/src/main/java/com/example/demo/service/MemberDetailsService.java @@ -0,0 +1,36 @@ +package com.example.demo.service; + +import com.example.demo.controller.dto.MemberDetails; +import com.example.demo.domain.Member; +import com.example.demo.repository.MemberRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.PathVariable; + +@Service +public class MemberDetailsService implements UserDetailsService { + + + private final MemberRepository memberRepository; + + + @Autowired + public MemberDetailsService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + System.out.println("@@@@" + username); + Member member = memberRepository.findMemberByUsername(username); + System.out.println("@@@@" + member); + if(member != null) { + return new MemberDetails(member); + } + + return null; + } +} diff --git a/src/main/java/com/example/demo/service/MemberService.java b/src/main/java/com/example/demo/service/MemberService.java index 04c1bc8..3c656d9 100644 --- a/src/main/java/com/example/demo/service/MemberService.java +++ b/src/main/java/com/example/demo/service/MemberService.java @@ -1,13 +1,20 @@ package com.example.demo.service; import java.util.List; +import java.util.Objects; +import com.example.demo.domain.Article; +import com.example.demo.exception.*; +import com.example.demo.repository.ArticleRepository; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.demo.controller.dto.request.MemberCreateRequest; import com.example.demo.controller.dto.request.MemberUpdateRequest; import com.example.demo.controller.dto.response.MemberResponse; + import com.example.demo.domain.Member; import com.example.demo.repository.MemberRepository; @@ -15,14 +22,18 @@ @Transactional(readOnly = true) public class MemberService { + private final ArticleRepository articleRepository; private final MemberRepository memberRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; - public MemberService(MemberRepository memberRepository) { + public MemberService(ArticleRepository articleRepository, MemberRepository memberRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { + this.articleRepository = articleRepository; this.memberRepository = memberRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; } public MemberResponse getById(Long id) { - Member member = memberRepository.findById(id); + Member member = memberRepository.findById(id).orElseThrow(MemberNotFoundException::new); return MemberResponse.from(member); } @@ -35,22 +46,47 @@ public List getAll() { @Transactional public MemberResponse create(MemberCreateRequest request) { - Member member = memberRepository.insert( - new Member(request.name(), request.email(), request.password()) + if (request.name() == null || request.email() == null || request.password() == null) { + throw new RequestNullExistException(); + } + + boolean emailExists = memberRepository.existsByEmail(request.email()); + + if (emailExists) { + throw new AlreadyHasEmailException(); + } + + Member member = memberRepository.save( + new Member(request.name(), request.email(), bCryptPasswordEncoder.encode(request.password()), "ROLE_ADMIN") ); + return MemberResponse.from(member); } @Transactional public void delete(Long id) { + List
allArticles = articleRepository.findAll(); + boolean memberHasArticles = allArticles.stream() + .anyMatch(article -> Objects.equals(article.getAuthorId(), id)); + + if (memberHasArticles) { + throw new MemberHasArticleException(); + } memberRepository.deleteById(id); } @Transactional public MemberResponse update(Long id, MemberUpdateRequest request) { - Member member = memberRepository.findById(id); + Member member = memberRepository.findById(id).orElseThrow(MemberNotExistException::new); + + boolean emailExists = memberRepository.existsByEmail(request.email()); + + if (emailExists) { + throw new AlreadyHasEmailException(); + } + member.update(request.name(), request.email()); - memberRepository.update(member); + memberRepository.save(member); return MemberResponse.from(member); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ff69a9e..4868267 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,15 @@ spring: datasource: - url: jdbc:mysql://localhost:3306/bcsd # 본인의 환경에 맞게 수정한다. + url: jdbc:mysql://localhost:3306/bcsd?serverTimezone=Asia/Seoul # 본인의 환경에 맞게 수정한다. username: root # 본인의 환경에 맞게 수정한다. - password: qwer1234 # 본인의 환경에 맞게 수정한다. + password: jabell0310 # 본인의 환경에 맞게 수정한다. driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + + show-sql: true + + session: + timeout: 1800s diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html new file mode 100644 index 0000000..2299033 --- /dev/null +++ b/src/main/resources/templates/admin.html @@ -0,0 +1,18 @@ + + + + + 메인화면 + + + + + +

메인 화면

+

로그인 상태

+ + \ No newline at end of file diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..f96e8af --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,18 @@ + + + + + 메인화면 + + + + + +

메인 화면

+

로그인 상태

+ + \ No newline at end of file diff --git a/src/main/resources/templates/signin.html b/src/main/resources/templates/signin.html new file mode 100644 index 0000000..f60e619 --- /dev/null +++ b/src/main/resources/templates/signin.html @@ -0,0 +1,43 @@ + + + + + 로그인 + + + + + +
+
+
+
+

LOGIN

+

서비스를 사용하려면 로그인을 해주세요!

+ +
+
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 0000000..aa48dcf --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,47 @@ + + + + + 회원 가입 + + + + + +
+
+
+
+

SIGN UP

+

서비스 사용을 위한 회원 가입

+ +
+
+ + +
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+
+
+
+ + \ No newline at end of file