[개발일지] 취미 커뮤니티 - 크루 CRUD

zwon·2023년 10월 24일
0

개발일지

목록 보기
20/23
post-thumbnail

크루 CRUD 기능 구현에 대해 정리하고자 한다.

Crew

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
public class Crew extends BaseTimeEntity {

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

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id")
  private User user; // 크루장 - 로그인한 user

  private String name; // 크루이름
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private boolean isRecruiting; // true - 모집중, false - 모집X,
  private boolean isPublished; // true - 공개O, false - 공개X
  private boolean isClosed; // true - 종료
  private String thumbnail; //이미지 경로 , 처음엔 기본 이미지 -> 크루 설정을 통해 배너 수정 가능

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String description; // 크루 목적

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String wisher; // 원하는 선원

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String plan; // 크루즈 설명

  @ManyToMany
  @JoinTable(name = "user_crew")
  private List<User> users = new ArrayList<>();


  public void update(CrewUpdateRequestDto crewUpdateRequestDto){
    this.name = crewUpdateRequestDto.getName();
    this.type = crewUpdateRequestDto.isType();
    this.cost = crewUpdateRequestDto.isCost();
    this.description = crewUpdateRequestDto.getDescription();
    this.wisher = crewUpdateRequestDto.getWisher();
    this.plan = crewUpdateRequestDto.getPlan();
  }

}
  • @ManyToMany를 권장하지는 않지만 우선 존재하는 방법이기 때문에 한번 사용은 해봐야겠다는 생각이 들어 여기서는 @ManyToMany를 사용해보았다.
  • 우선 크루 생성과 관련된 필드들만 나타냈다.

DTO

CrewSaveRequestDto

@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class CrewSaveRequestDto {
  private String name; // 크루이름
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private String description; // 크루즈 설명
  private String wisher; // 원하는 선원
  private String plan; // 크루즈 설명


  public Crew toEntity(){
    return Crew.builder()
        .name(name)
        .type(type)
        .cost(cost)
        .wisher(wisher)
        .plan(plan)
        .description(description)
        .isPublished(true) // 처음 crew를 만들 땐 true
        .isRecruiting(true) // 처음 crew를 만들 땐 true
        .isClosed(false) // 처음 crew를 만들 땐 false
        .build();
  }
}
  • 크루 썸네일 기능은 구현중에 있다..............

CrewResponseDto

@Getter
@NoArgsConstructor
public class CrewResponseDto {

  private Long id; // 크루 조회할 때 사용
  private String leader; // 크루장
  private String name; // 크루명
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private String description; // 크루즈 설명
  private String wisher; // 원하는 선원
  private String plan; // 크루즈 설명
  private boolean isRecruit; // true - 모집중, false - 모집X
  private boolean isPublished; // true - 공개O, false - 공개X
  private boolean isClosed;
  private List<UserResponseDto> users;

  public CrewResponseDto(Crew crew) {
    this.id = crew.getId();
    this.leader = crew.getUser().getNickname();
    this.name = crew.getName();
    this.type = crew.isType();
    this.cost = crew.isCost();
    this.description = crew.getDescription();
    this.wisher = crew.getWisher();
    this.plan = crew.getPlan();
    this.isRecruit = crew.isRecruiting();
    this.isPublished = crew.isPublished();
    this.isClosed = crew.isClosed();
    this.users = crew.getUsers().stream().map(UserResponseDto::new).collect(Collectors.toList());
  }
}
  • 딱히 설명이 필요해 보이는 코드는 없는거같다.

CrewUpdateRequestDto

@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class CrewUpdateRequestDto {
  private String name; // 크루이름
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private String description; // 크루즈 설명
  private String wisher; // 원하는 선원
  private String plan; // 크루즈 설명
}

CrewRepository

public interface CrewRepository extends JpaRepository<Crew, Long> {

  Page<Crew> findAll(Pageable pageable);

}
  • 크루 전체 조회시 페이징 기능을 위해 추가했다.

CrewService

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class CrewService {
  private final CrewRepository crewRepository;

  // 크루 저장
  public Long save(CrewSaveRequestDto crewSaveRequestDto, User user){
    Crew crew = crewSaveRequestDto.toEntity();
    crew.setUser(user);
    Crew savedCrew = crewRepository.save(crew);
    return savedCrew.getId();
  }
  // 크루 전체 조회
  public Page<CrewResponseDto> findAll(Pageable pageable) {
    Page<CrewResponseDto> crews = crewRepository.findAll(pageable).map(CrewResponseDto::new);
    return crews;
  }
  // 크루 상세 조회
  public CrewResponseDto findById(Long id){
    Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
    return new CrewResponseDto(crew);
  }

  // 크루 삭제 (크루장만 삭제 가능)
  public void delete(Long id){
    crewRepository.deleteById(id);
  }

  // 크루 참가
  public void addUser(Long id, User user){
    Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
    if (crew.isJoinable(user)){
      crew.addUser(user);
    }
  }

  public void leaveCrew(Long id, User user){
    Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
    if (crew.isMember(user)) {
      crew.removeUser(user);
    }
  }

  @Transactional
  public void update(Long id, CrewUpdateRequestDto crewUpdateRequestDto){
    Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
    crew.update(crewUpdateRequestDto); 
  }
}
  • 이렇게 엔티티로 Dto를 넘겨도 되는지는 아직 의문이지만 우선은 Update 정보를 담은 dto를 넘겨줌으로써 크루 수정 코드를 작성했다.

CrewViewController

@Controller
@RequiredArgsConstructor
@Transactional
@Slf4j
public class CrewViewController {
  private final CrewService crewService;

  // 크루 생성 폼
  @GetMapping("/crews/form")
  public String crewForm(Model model) {
    model.addAttribute("crewSaveRequestDto", new CrewSaveRequestDto());
    return "crew/form";
  }

  // 크루 생성
  @PostMapping("/crews/form")
  public String crewSave(@ModelAttribute CrewSaveRequestDto crewSaveRequestDto,
                         HttpSession session) {
    User loginUser = (User) session.getAttribute("loginUser");
    Long id = crewService.save(crewSaveRequestDto, loginUser);
    return "redirect:/crews/" + id;
  }

  // 크루 조회 - 상세 조회
  @GetMapping("/crews/{id}") // 서비스 기본화면
  public String crew(@PathVariable Long id, Model model, HttpSession session) {
    User user = (User) session.getAttribute("loginUser");
    CrewResponseDto crewResponseDto = crewService.findById(id);

    UserResponseDto loginUser = new UserResponseDto(user);

    model.addAttribute("loginUser", loginUser);
    model.addAttribute("crewResponseDto", crewResponseDto);

    return "crew/intro";
  }

  // 크루 전체 조회 - 현재 모집 중인 크루즈
  @GetMapping(/crews) // 서비스 기본화면
  public String crews(Model model,
                      @PageableDefault(size = 5, sort = "createdDate",
                          direction = Sort.Direction.DESC) Pageable pageable,
                      @SessionAttribute(name = "loginUser", required = false) User loginUser) {
    Page<CrewResponseDto> crews = crewService.findAll(pageable);
    model.addAttribute("loginUser", loginUser);
    model.addAttribute("crews", crews); // 현재 모집 중인 크루즈를 나타낼 때 사용
    //model.addAttribute("top10Crew", crews); // top10크루 정보를 담고 있음 -> 쿼리를 생성해서 가져와야할듯
    return "main";

  }
  @DeleteMapping("/crews/{id}/delete")
  public String deleteCrew(@PathVariable Long id){
    crewService.delete(id);
    return "redirect:/";
  }
  
}
  • 아직 Top10 크루는 구현하지 않았다. 어떤 기준으로 Top10을 선정할지 고민중이다.
  • 아마 Top10 크루를 가져올 때 쿼리를 직접 작성해서 가져올거같다.

Top10 크루 가져오기 추가 예정!

  • 이 부분은 아직 기능이 완성되지 않았다.
  • 그래서 Top10 관련된 코드들은 무시하면 된다.

Thymeleaf

크루 생성 폼

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <link rel="stylesheet" th:href="@{../css/crew/crewForm.css}">
</head>
<body>
<div class="container">
  <p>크루 생성 후 수정할 수 있습니다.</p>
  <form th:action th:object="${crewSaveRequestDto}" method="post">
    <label for="crewName">크루즈명</label>
    <input type="text" id="crewName" placeholder="크루명을 입력해주세요." th:field="*{name}">

    <label for="online">온라인 유무</label>
    <input type="checkbox" id="online" th:field="*{type}">

    <label for="cost">비용 유무</label>
    <input type="checkbox" id="cost" th:field="*{cost}">

    <label for="crewGoal">크루 소개</label>
    <textarea type="text" id="crewGoal" placeholder="크루 소개를 입력해주세요." th:field="*{description}"></textarea>

    <label for="wisher">이런 선원을 원해요.</label>
    <textarea type="text" id="wisher" placeholder="원하는 선원을 입력해주세요." th:field="*{wisher}"></textarea>

    <label for="plan">항해 계획</label>
    <textarea type="text" id="plan" placeholder="항해 계획을 입력해주세요." th:field="*{plan}"></textarea>
    <button type="submit">등록</button>
  </form>
</div>

</body>
</html>

크루 상세 화면

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>크루 상세 화면</title>
  <link rel="stylesheet" th:href="@{/css/crew/intro.css}">
</head>

<body>
  <header th:replace="~{fragment/fragment :: crewDetailheader(${crewResponseDto.name})}"></header>
  <nav th:replace="~{fragment/fragment :: crewDetailnav(${loginUser.name}, ${crewResponseDto.leader}
  ,${crewResponseDto.users.contains(loginUser)}, ${crewResponseDto.recruit} )}"></nav>

  <div id="container">
    <h2 th:text="|안녕하세요. ${crewResponseDto.name}입니다.|"></h2>
    <h4>👍저희 크루는 이런 크루입니다.</h4>
    <p th:text="${crewResponseDto.description}">
    <h4>🧐저희 크루는 이러한 사람을 원해요.</h4>
    <p th:text="${crewResponseDto.wisher}"></p>
    <h4>📆저희 크루의 활동 계획입니다.</h4>
    <p th:text="${crewResponseDto.plan}">
    <h2>👥크루원</h2>
    <p th:text="|크루장 : ${crewResponseDto.leader}|"></p>
    <div class="member" th:each="member : ${crewResponseDto.getUsers()}">
      <p th:text="|크루원 : ${member.nickname}|"></p>
    </div>
  </div>

<div th:replace="~{fragment/fragment :: footerFragment}"></div>
</main>
</body>
</html>

fragment.html

  • header나 버튼 등의 공통 조각들을 가지고 있다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<header th:fragment="boardHeader">
  <div class="left-section">
    <ul><a href="/boards">자유게시판</a></ul>
    <ul><a href="/crews">크루 화면</a></ul>
  </div>

  <div class="right-section">
    <button type="button" th:onclick="|location.href='@{/board/form}'|">글 등록</button> <!-- 폼 태그로 감싸던가 아니면 자바스크립트-->
    <form th:action="@{/logout}" th:method="post">
      <button type="submit">로그아웃</button>
    </form>
    <button type="button" th:onclick="|location.href='@{/my-page}'|">마이페이지</button>
  </div>
</header>

<header th:fragment="crewDetailheader(name)">
  <div class="left-section">
    <ul><a href="/boards">자유게시판</a></ul>
    <ul><a href="/crews">크루 화면</a></ul>
  </div>
  <div class="middle-section">
    <h1 th:text="${name}"></h1>
  </div>
  <div class="right-section">
    <form th:action="@{/logout}" th:method="post">
      <button type="submit">로그아웃</button>
    </form>
    <button type="button" th:onclick="|location.href='@{/my-page}'|">마이페이지</button>
  </div>
</header>

<nav class="info" th:fragment="crewDetailnav(loginUserNicname, crewLeaderName,isMember, isRecruit)">
  <div class="intro-btn">
    <button type="button" th:onclick="|location.href='@{/crews/{id}(id=${crewResponseDto.id})}'|">
      소개
    </button>
    <button type="button" th:onclick="|location.href='@{/crews/{id}/log(id=${crewResponseDto.id})}'|">
      활동 일지
    </button>
    <button type="button" th:onclick="|location.href='@{/crews/{id}/meeting(id=${crewResponseDto.id})}'|">
      모임
    </button>
    <button type="button" th:if="${loginUserNicname == crewLeaderName}"
            th:onclick="|location.href='@{/crews/{crewId}/setting(crewId=${crewResponseDto.id})}'|">크루 설정
    </button>
  </div>
  <div class="crew-btn">  <!--관심있어요 -> 맴버가 아니고, 공개된 크루이고, 회원 모집중일때만 가능하게 하기.-->
    <button type="button" th:if="${!isMember}"
            th:onclick="|location.href='@{/crews/{crewId}/like(crewId=${crewResponseDto.id})}'|">관심있어요.
    </button>
    <button type="button" th:if="${isMember}"
            th:onclick="|location.href='@{/crews/{crewId}/leave(crewId=${crewResponseDto.id})}'|">크루 탈퇴
    </button>
    <button type="button" th:if="${!isMember and loginUserNicname != crewLeaderName and isRecruit}"
            th:onclick="|location.href='@{/crews/{crewId}/join(crewId=${crewResponseDto.id})}'|">크루 가입
    </button>
  </div>
</nav>

<footer th:fragment="footerFragment">
  <p> COPYRIGHT@ JIWON27</p>
</footer>
</body>
</html>

update form

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>크루 상세 화면</title>
  <link rel="stylesheet" th:href="@{/css/crew/setting.css}">
</head>
<body>
<header th:replace="~{fragment/fragment :: crewDetailheader(${crewResponseDto.name})}"></header>
<nav th:replace="~{fragment/fragment :: crewDetailnav(${loginUser.name}, ${crewResponseDto.leader},
${crewResponseDto.users.contains(loginUser)}, ${crewResponseDto.recruit} )}"></nav>

<div id="container">
  <div class="setting">
    <div class="crew-setting">
      <!--크루 공개-->
      <form th:action="@{/crews/{id}/published(id=${crewResponseDto.id})}" method="post">
        <button type="submit"  th:text="${crewResponseDto.published} ? '크루 비공개' : '크루원 공개'">크루 비공개</button>
      </form>
      <!--크루 인원 모집 -->
      <form th:action="@{/crews/{id}/recruit(id=${crewResponseDto.id})}" method="post">
        <button type="submit" th:text="${crewResponseDto.recruit} ? '크루원 모집 중단' : '크루원 모집하기'"></button>
      </form>
        <!--크루 종료, 활동일지 등 이런걸 추억으로 남기고 싶을 땐 종료하고 마이페이지 관심 크루, 종료 크루, 참여 크루 보여주자....-->
      <form th:action="@{/crews/{id}/close(id=${crewResponseDto.id})}" method="post">
        <button type="submit" th:text="${crewResponseDto.closed} ? '크루 활성화' : '크루 활동 종료'"></button>
      </form>
        <!--크루 삭제-->
      <form th:action="@{/crews/{id}/delete(id=${crewResponseDto.id})}" method="delete">
        <button type="submit">크루 삭제</button>
      </form>
    </div> <!--crew-setting-->

    <form th:action th:object="${crewUpdateRequestDto}" method="post">
      <div class="text">
        <label for="crewName">크루즈명</label>
        <input type="text" id="crewName" placeholder="크루명을 수정합니다." th:field="*{name}">
      </div>
      <div class="checkbox">
        <label for="online">온라인 유무(체크O - 온라인, 체크X - 오프라인)</label>
        <input type="checkbox" id="online" th:field="*{type}">
      </div>

      <div class="checkbox">
        <label for="cost">비용 유무(체크O - 유료, 체크X - 무료)</label>
        <input type="checkbox" id="cost" th:field="*{cost}">
      </div>

      <div class="text">
        <label for="crewGoal">크루 소개</label>
        <textarea type="text" id="crewGoal" placeholder="크루 소개를 수정합니다." th:field="*{description}"></textarea>
      </div>

      <div class="text">
        <label for="wisher">이런 선원을 원해요.</label>
        <textarea type="text" id="wisher" placeholder="원하는 선원을 수정합니다." th:field="*{wisher}"></textarea>
      </div>

      <div class="text">
        <label for="plan">크루 계획</label>
        <textarea type="text" id="plan" placeholder="크루 계획을 수정합니다." th:field="*{plan}"></textarea>
      </div>
    <button type="submit">수정</button>
    </form>
  </div><!--setting-->
</div> <!--container-->

<div th:replace="~{fragment/fragment :: footerFragment}"></div>
</main>
</body>
</html>
  • 여기엔 다음 포스팅에서 정리할 크루 공개/비공개, 크루 모집 등의 설정하는 부분ㄷ 있지만 우선 크루 삭제하는 부분인 크루 삭제 버튼과 수정 폼을 집중적으로 보면된다.

main 화면

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>main</title>
  <link rel="stylesheet" th:href="@{css/main.css}">
</head>
<body>
<!-- 로그인 성공 후 메인 화면-->
<header>
  <div class="left-section">
    <ul><a href="/boards">자유게시판</a></ul>
    <ul><a href="#this">크루 화면</a></ul>
  </div>

  <div class="right-section">
    <button type="submit" th:onclick="|location.href='@{/crew/form}'|" th:if="${loginUser != null}">크루 생성</button>
    <button type="submit" th:onclick="|location.href='@{/logout}'|" th:if="${loginUser != null}">로그아웃</button>
    <button type="submit" th:onclick="|location.href='@{/my-page}'|" th:if="${loginUser != null}">마이페이지</button>
    <button type="submit" th:onclick="|location.href='@{/login}'|" th:if="${loginUser == null}">로그인</button>
    <button type="submit" th:onclick="|location.href='@{/sign-up}'|" th:if="${loginUser == null}">회원가입</button>
  </div>
</header>
<h3>Top 10 크루</h3>
<div class="top10_crew">
  <div class="crew"  th:each="crew : ${crews}" th:if="${crew.isPublished && !crew.closed}">
    <img src="" alt="" width="150px" height="100px"><br>
    <a th:href="@{/crews/{crewId}(crewId=${crew.id})}" th:text="${crew.name}"></a><br>
    <span th:text="${crew.leader}"></span> <br>
    <span th:text="${crew.type} ? '온라인 활동' : '오프라인 활동'"></span><br> <!--crew.type=true면 온라인-->
    <span th:text="${crew.cost} ? '유료' : '무료'"></span><br> <!--crew.cost=true면 유료-->
  </div>

</div>
<h3>현재 모집 중인 크루즈</h3>
  <div class="crew-container">
    <div class="crew"  th:each="crew : ${crews}" th:if="${crew.isPublished && !crew.closed}">
      <img src="" alt="" width="150px" height="100px"><br>
      <a th:href="@{/crews/{crewId}(crewId=${crew.id})}" th:text="${crew.name}"></a><br>
      <span th:text="${crew.leader}"></span> <br>
      <span th:text="${crew.type} ? '온라인 활동' : '오프라인 활동'"></span><br> <!--crew.type=true면 온라인-->
      <span th:text="${crew.cost} ? '유료' : '무료'"></span><br> <!--crew.cost=true면 유료-->
    </div>
  </div>

  <div class="page-btn">
    <a th:if="${!crews.isFirst()}" th:href="@{/crews(page=${crews.getNumber() - 1})}">이전 페이지</a>
    <a th:if="${!crews.isLast()}" th:href="@{/crews(page=${crews.getNumber() + 1})}"}>다음 페이지</a>
  </div>
</body>
</html>
  • 크루 설정에 관한 부분도 있지만 우선 우선 크루 CRUD와 관련해서 집중적으로 보면 된다.
  • img는 크루 썸네일 관련한 부분이라 우선은 img로 해두었다. 빨리 해야지,....
  • CRUD에 관련해서는 딱히 설명한 부분은 없는거같다.

아직 해당 웹 서비스 컨셉을 고민중이어서 단어 컨셉(?)이 일치하지 않을 수 있습니다...
디자인은 마지막에...

화면

main 화면

크루 form 화면

크루 상세 화면

  • 크루 생성 후 상세 화면으로 redirect

크루 수정 및 삭제

  • header부분은 sticky로 설정해서 그렇구 추후에 수정할 생각입니다.. 디자인 ... ㅠㅠ

  • 수정 폼에 수정할 내용으로 입력하면 다음과 같이 수정된다

  • 크루 삭제 버튼을 누르면 크루가 삭제된다. 참고로 크루장만 크루 삭제가 가능하다.

포스팅 중에 경로를 crew -> crews로 변경했고 포스팅 내용에도 수정을 했지만 수정이 덜 된 부분이 있을 수 있어 혹시 404에러가 발생하신다면 crews로 변경하시면 됩니다.


아직 부족한 코드이고 공부하면서 작성하고있습니다. 조언 너무 감사합니다.

profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글