반응형

프로젝트에 사용한 라이브러리 설치 (build.gradle)

implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// DB
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Thymeleaf, Validation
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Spring Security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.7.5'

ERD

Entity 설명

User

  • id(primary key), login_id(로그인에 사용되는 아이디), password, nickname, user_role(등급), received_like_cnt(이 유저가 받은 좋아요 수), created_at(가입일)
  • User은 여러개의 Board를 작성할 수 있음 => User : Board = 1 : N
  • User은 여러개의 Comment를 작성할 수 있음 => User : Comment = 1 : N
  • User은 여러개의 좋아요를 추가할 수 있음 => User : Like = 1 : N

Board

  • id(primary key), title(제목), body(내용), category, like_cnt(이 글이 받은 좋아요 수), comment_cnt(이 글이 받은 댓글 수), created_at(작성일), last_modified_at(최근 수정일)
  • Board와 Upload Image는 1:1 관계 => Board에 Upload Image의 Id(foreign key)를 넣음 => Board가 연관관계 주인
  • Board에는 여러개의 Comment가 달릴 수 있음 => Board : Comment = 1 : N
  • Board에는 여러개의 Like가 추가될 수 있음 => Board : Like = 1 : N

Comment

  • id(primary key), body(내용), created_at(작성일), last_modified_at(최근 수정일)

Like

  • Like는 user_id, board_id를 foreign key로 갖는데, user_id는 좋아요를 누른 유저의 아이디이고, board_id는 좋아요가 달린 게시글의 id

Upload Image

  • id(primary key), original_filename(유저가 업로드한 이미지의 원본 파일명), saved_filename(서버에 저장된 파일명)
  • 원본 파일명으로 파일을 저장하면 파일명이 중복되어 에러가 발생할 수 있음 => 이를 방지하기 위해 서버에는 중복되지 않는 UUID로 파일명을 수정 후 저장

Entity 구현 코드

User

  • User가 삭제(탈퇴)되면 유저의 Board, Comment, Like 모두 삭제 => orphanrRemoval=true 설정
  • UserRole에는 @Enumerated(EnumType.STRING)을 설정 => DB에 저장될 때, "BRONZE", "SILVER"와 같이 String 타입으로 저장됨
  • User Entity에는 rankUp(), likeChange(), edit(), changeRole() 4개의 메소드가 존재
    • rankUp() : BRONZE -> SILVER, SILVER -> GOLD로 승급할 때 사용
    • likeChange() : 이 유저의 글에 좋아요가 추가되거나, 취소된 경우 유저의 receivedLikeCnt를 계산하여 수정할 때 사용
    • edit() : 유저가 수정할 수 있는 nickname, password를 수정할 때 사용
    • changeRole() : rankUp()과는 달리 관리자가 이 유저의 등급을 수정할 때 사용, BRONZE -> SILVER -> GOLD -> BLACKLIST -> BRONZE 순으로 변경
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class User {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String loginId;     // 로그인할 때 사용하는 아이디
    private String password;    // 비밀번호
    private String nickname;    // 닉네임
    private LocalDateTime createdAt;    // 가입 시간
    private Integer receivedLikeCnt; // 유저가 받은 좋아요 개수 (본인 제외)

    @Enumerated(EnumType.STRING)
    private UserRole userRole;      // 권한

    @OneToMany(mappedBy = "user", orphanRemoval = true)
    private List<Board> boards;     // 작성글

    @OneToMany(mappedBy = "user", orphanRemoval = true)
    private List<Like> likes;       // 유저가 누른 좋아요

    @OneToMany(mappedBy = "user", orphanRemoval = true)
    private List<Comment> comments; // 댓글

    public void rankUp(UserRole userRole, Authentication auth) {
        this.userRole = userRole;

        // 현재 저장되어 있는 Authentication 수정 => 재로그인 하지 않아도 권한 수정 되기 위함
        List<GrantedAuthority> updatedAuthorities = new ArrayList<>();
        updatedAuthorities.add(new SimpleGrantedAuthority(userRole.name()));
        Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);
        SecurityContextHolder.getContext().setAuthentication(newAuth);
    }

    public void likeChange(Integer receivedLikeCnt) {
        this.receivedLikeCnt = receivedLikeCnt;
        if (this.receivedLikeCnt >= 10 && this.userRole.equals(UserRole.SILVER)) {
            this.userRole = UserRole.GOLD;
        }
    }

    public void edit(String newPassword, String newNickname) {
        this.password = newPassword;
        this.nickname = newNickname;
    }

    public void changeRole() {
        if (userRole.equals(UserRole.BRONZE)) userRole = UserRole.SILVER;
        else if (userRole.equals(UserRole.SILVER)) userRole = UserRole.GOLD;
        else if (userRole.equals(UserRole.GOLD)) userRole = UserRole.BLACKLIST;
        else if (userRole.equals(UserRole.BLACKLIST)) userRole = UserRole.BRONZE;
    }
}

UserRole

public enum UserRole {
    BRONZE, SILVER, GOLD, BLACKLIST, ADMIN;
}

Board

  • 좋아요 수, 댓글 수를 저장한 이유는 리스트에서 정렬할 때 더 빠른 조회를 위해 따로 저장
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class Board extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;   // 제목
    private String body;    // 본문

    @Enumerated(EnumType.STRING)
    private BoardCategory category; // 카테고리

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;      // 작성자

    @OneToMany(mappedBy = "board", orphanRemoval = true)
    private List<Like> likes;       // 좋아요
    private Integer likeCnt;        // 좋아요 수

    @OneToMany(mappedBy = "board", orphanRemoval = true)
    private List<Comment> comments; // 댓글
    private Integer commentCnt;     // 댓글 수

    @OneToOne(fetch = FetchType.LAZY)
    private UploadImage uploadImage;

    public void update(BoardDto dto) {
        this.title = dto.getTitle();
        this.body = dto.getBody();
    }

    public void likeChange(Integer likeCnt) {
        this.likeCnt = likeCnt;
    }

    public void commentChange(Integer commentCnt) {
        this.commentCnt = commentCnt;
    }

    public void setUploadImage(UploadImage uploadImage) {
        this.uploadImage = uploadImage;
    }

}

BoardCategory

  • of 메소드를 통해 String 타입의 category를 BoardCategory 타입으로 변환하는 기능 추가
public enum BoardCategory {
    FREE, GREETING, GOLD;

    public static BoardCategory of(String category) {
        if (category.equalsIgnoreCase("free")) return BoardCategory.FREE;
        else if (category.equalsIgnoreCase("greeting")) return BoardCategory.GREETING;
        else if (category.equalsIgnoreCase("gold")) return BoardCategory.GOLD;
        else return null;
    }
}

Comment

  • 댓글은 수정이 가능하기 때문에 update() 메소드 추가
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class Comment extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String body;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;      // 작성자

    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;    // 댓글이 달린 게시판

    public void update(String newBody) {
        this.body = newBody;
    }
}

Like

  • MySQL에서 Table 명을 like로 사용할 수 없는데, @Table(name = ""like"")를 통해 like로 설정
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Table(name = "\"like\"")
public class Like {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;      // 좋아요를 누른 유저

    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;    // 좋아요가 추가된 게시글

}

UploadImage

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class UploadImage {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String originalFilename;    // 원본 파일명
    private String savedFilename;        // 서버에 저장된 파일명

    @OneToOne(mappedBy = "uploadImage", fetch = FetchType.LAZY)
    private Board board;

}

BaseEntity

  • 하나의 객체가 DB에 저장될 때, 자동으로 createdAt, lastModifiedAt을 저장하기 위해 사용되는 Entity
  • 아래의 JpaAuditingConfig를 등록하고, Board와 같이 상속시켜 사용
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime lastModifiedAt;
}

JpaAuditingConfig

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
반응형

↓ 클릭시 이동

복사했습니다!