크루 CRUD 기능 구현에 대해 정리하고자 한다.
@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();
}
}
@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();
}
}
@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());
}
}
@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; // 크루즈 설명
}
public interface CrewRepository extends JpaRepository<Crew, Long> {
Page<Crew> findAll(Pageable pageable);
}
@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);
}
}
@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:/";
}
}
<!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>
<!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>
<!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>
<!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>
아직 해당 웹 서비스 컨셉을 고민중이어서 단어 컨셉(?)이 일치하지 않을 수 있습니다...
디자인은 마지막에...
header부분은 sticky로 설정해서 그렇구 추후에 수정할 생각입니다.. 디자인 ... ㅠㅠ
수정 폼에 수정할 내용으로 입력하면 다음과 같이 수정된다
크루 삭제 버튼을 누르면 크루가 삭제된다. 참고로 크루장만 크루 삭제가 가능하다.
포스팅 중에 경로를 crew -> crews로 변경했고 포스팅 내용에도 수정을 했지만 수정이 덜 된 부분이 있을 수 있어 혹시 404에러가 발생하신다면 crews로 변경하시면 됩니다.
아직 부족한 코드이고 공부하면서 작성하고있습니다. 조언 너무 감사합니다.