diff --git a/sql/init.sql b/sql/init.sql index 4042d95..98a85fc 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -47,7 +47,7 @@ CREATE TABLE interest_job CREATE TABLE resume ( resume_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - title VARCHAR(50) NOT NULL, + title VARCHAR(50) NOT NULL, category VARCHAR(10) NOT NULL, content VARCHAR(50) NOT NULL, start_date DATE, @@ -68,10 +68,10 @@ CREATE TABLE goal CREATE TABLE member_goal ( - member_id BIGINT NOT NULL, - goal_id BIGINT NOT NULL, - is_complete BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (member_id, goal_id), + member_goal_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + goal_id BIGINT NOT NULL, + is_complete BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT member_goal_member FOREIGN KEY (member_id) REFERENCES member (member_id), CONSTRAINT member_goal_goal FOREIGN KEY (goal_id) REFERENCES goal (goal_id) ) ENGINE = InnoDB @@ -85,9 +85,8 @@ CREATE TABLE quest created_at DATETIME NOT NULL, deadline DATE NOT NULL, sequence INT NOT NULL, - member_id BIGINT NOT NULL, - goal_id BIGINT NOT NULL, - CONSTRAINT fk_quest_member_goal FOREIGN KEY (member_id, goal_id) REFERENCES member_goal (member_id, goal_id) + member_goal_id BIGINT NOT NULL, + CONSTRAINT fk_quest_member_goal FOREIGN KEY (member_goal_id) REFERENCES member_goal (member_goal_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; diff --git a/src/main/java/com/groom/orbit/common/exception/ErrorCode.java b/src/main/java/com/groom/orbit/common/exception/ErrorCode.java index f0cabbb..e0d5869 100644 --- a/src/main/java/com/groom/orbit/common/exception/ErrorCode.java +++ b/src/main/java/com/groom/orbit/common/exception/ErrorCode.java @@ -35,6 +35,7 @@ public enum ErrorCode { SEARCH_SHORT_LENGTH_ERROR(40007, HttpStatus.BAD_REQUEST, "검색어는 2글자 이상이어야 합니다."), INVALID_ACCESS_URL(40008, HttpStatus.BAD_REQUEST, "잘못된 사용자 접근입니다."), INVALID_FILE(40009, HttpStatus.BAD_REQUEST, "잘못된 파일입니다."), + ALREADY_EXISTS_GOAL(40010, HttpStatus.BAD_REQUEST, "이미 목표가 존재합니다."), // Gone Error GONE_SHARED_URL(41001, HttpStatus.GONE, "해당 공유 URL이 만료되었습니다."), diff --git a/src/main/java/com/groom/orbit/goal/app/MemberGoalService.java b/src/main/java/com/groom/orbit/goal/app/MemberGoalService.java index 9671c27..ab986a8 100644 --- a/src/main/java/com/groom/orbit/goal/app/MemberGoalService.java +++ b/src/main/java/com/groom/orbit/goal/app/MemberGoalService.java @@ -1,6 +1,7 @@ package com.groom.orbit.goal.app; import java.util.List; +import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -8,7 +9,8 @@ import com.groom.orbit.common.dto.CommonSuccessDto; import com.groom.orbit.common.exception.CommonException; import com.groom.orbit.common.exception.ErrorCode; -import com.groom.orbit.goal.app.dto.request.CreateMemberGoalRequestDto; +import com.groom.orbit.goal.app.command.GoalCommandService; +import com.groom.orbit.goal.app.dto.request.MemberGoalRequestDto; import com.groom.orbit.goal.app.dto.response.GetCompletedGoalResponseDto; import com.groom.orbit.goal.app.dto.response.GetOnGoingGoalResponseDto; import com.groom.orbit.goal.app.query.GoalQueryService; @@ -31,6 +33,7 @@ public class MemberGoalService { private final QuestQueryService questQueryService; private final MemberQueryService memberQueryService; private final GoalQueryService goalQueryService; + private final GoalCommandService goalCommandService; @Transactional(readOnly = true) public MemberGoal findMemberGoal(Long memberId, Long goalId) { @@ -46,12 +49,12 @@ public List findOnGoingGoals(Long memberId) { return memberGoals.stream() .map( memberGoal -> { - Long goalId = memberGoal.getGoalId(); + Long goalId = memberGoal.getGoal().getGoalId(); long totalQuestCount = questQueryService.getTotalQuestCount(goalId); long finishQuestCount = questQueryService.getFinishQuestCount(goalId); return new GetOnGoingGoalResponseDto( - memberGoal.getGoalId(), memberGoal.getTitle(), totalQuestCount, finishQuestCount); + goalId, memberGoal.getTitle(), totalQuestCount, finishQuestCount); }) .toList(); } @@ -78,14 +81,36 @@ public CommonSuccessDto deleteGoal(Long memberId, Long goalId) { return new CommonSuccessDto(true); } - public CommonSuccessDto createGoal(Long memberId, Long goalId, CreateMemberGoalRequestDto dto) { + public CommonSuccessDto createGoal(Long memberId, MemberGoalRequestDto dto) { Member member = memberQueryService.findMember(memberId); - Goal goal = goalQueryService.findGoal(goalId); - + Goal goal = getGoal(dto.title(), dto.category()); MemberGoal memberGoal = MemberGoal.create(member, goal); goal.increaseCount(); + memberGoalRepository.save(memberGoal); return new CommonSuccessDto(true); } + + public CommonSuccessDto updateGoal(Long memberId, Long goalId, MemberGoalRequestDto dto) { + MemberGoal memberGoal = findMemberGoal(memberId, goalId); + Goal goal = getGoal(dto.title(), dto.category()); + + validateMemberGoal(memberId, goal.getGoalId()); + memberGoal.updateGoal(goal); + + // memberGoalRepository.updateGoalId(goal.getGoalId()); + return new CommonSuccessDto(true); + } + + private void validateMemberGoal(Long memberId, Long goalId) { + if (memberGoalRepository.findByMemberIdAndGoalId(memberId, goalId).isPresent()) { + throw new CommonException(ErrorCode.ALREADY_EXISTS_GOAL); + } + } + + private Goal getGoal(String title, String category) { + Optional findGoal = goalQueryService.findGoalByTitleAndCategory(title, category); + return findGoal.orElseGet(() -> goalCommandService.createGoal(title, category)); + } } diff --git a/src/main/java/com/groom/orbit/goal/app/command/GoalCommandService.java b/src/main/java/com/groom/orbit/goal/app/command/GoalCommandService.java new file mode 100644 index 0000000..6bd9e78 --- /dev/null +++ b/src/main/java/com/groom/orbit/goal/app/command/GoalCommandService.java @@ -0,0 +1,25 @@ +package com.groom.orbit.goal.app.command; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.groom.orbit.goal.dao.GoalRepository; +import com.groom.orbit.goal.dao.entity.Goal; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional +@RequiredArgsConstructor +public class GoalCommandService { + + private final GoalRepository goalRepository; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Goal createGoal(String title, String category) { + Goal goal = Goal.create(title, category); + + return goalRepository.save(goal); + } +} diff --git a/src/main/java/com/groom/orbit/goal/app/dto/request/CreateMemberGoalRequestDto.java b/src/main/java/com/groom/orbit/goal/app/dto/request/CreateMemberGoalRequestDto.java deleted file mode 100644 index 6bc4024..0000000 --- a/src/main/java/com/groom/orbit/goal/app/dto/request/CreateMemberGoalRequestDto.java +++ /dev/null @@ -1,3 +0,0 @@ -package com.groom.orbit.goal.app.dto.request; - -public record CreateMemberGoalRequestDto(String title, String category) {} diff --git a/src/main/java/com/groom/orbit/goal/app/dto/request/MemberGoalRequestDto.java b/src/main/java/com/groom/orbit/goal/app/dto/request/MemberGoalRequestDto.java new file mode 100644 index 0000000..d1055cd --- /dev/null +++ b/src/main/java/com/groom/orbit/goal/app/dto/request/MemberGoalRequestDto.java @@ -0,0 +1,3 @@ +package com.groom.orbit.goal.app.dto.request; + +public record MemberGoalRequestDto(String title, String category) {} diff --git a/src/main/java/com/groom/orbit/goal/app/query/GoalQueryService.java b/src/main/java/com/groom/orbit/goal/app/query/GoalQueryService.java index 3fa4170..f3ecbba 100644 --- a/src/main/java/com/groom/orbit/goal/app/query/GoalQueryService.java +++ b/src/main/java/com/groom/orbit/goal/app/query/GoalQueryService.java @@ -1,5 +1,7 @@ package com.groom.orbit.goal.app.query; +import java.util.Optional; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +27,11 @@ public Goal findGoal(Long goalId) { .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_GOAL)); } + public Optional findGoalByTitleAndCategory(String title, String category) { + + return goalRepository.findByTitleAndCategory(title, GoalCategory.from(category)); + } + public GetGoalCategoryResponseDto getGoalCategory() { return new GetGoalCategoryResponseDto(GoalCategory.getAll()); } diff --git a/src/main/java/com/groom/orbit/goal/controller/command/MemberGoalCommandController.java b/src/main/java/com/groom/orbit/goal/controller/command/MemberGoalCommandController.java index 01ff361..21aac07 100644 --- a/src/main/java/com/groom/orbit/goal/controller/command/MemberGoalCommandController.java +++ b/src/main/java/com/groom/orbit/goal/controller/command/MemberGoalCommandController.java @@ -1,6 +1,7 @@ package com.groom.orbit.goal.controller.command; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -11,7 +12,7 @@ import com.groom.orbit.common.dto.CommonSuccessDto; import com.groom.orbit.common.dto.ResponseDto; import com.groom.orbit.goal.app.MemberGoalService; -import com.groom.orbit.goal.app.dto.request.CreateMemberGoalRequestDto; +import com.groom.orbit.goal.app.dto.request.MemberGoalRequestDto; import lombok.RequiredArgsConstructor; @@ -28,11 +29,17 @@ public ResponseDto deleteMemberGoal( return ResponseDto.ok(memberGoalService.deleteGoal(memberId, goalId)); } - @PutMapping("/{goal_id}") + @PutMapping public ResponseDto createMemberGoal( + @AuthMember Long memberId, @RequestBody MemberGoalRequestDto dto) { + return ResponseDto.created(memberGoalService.createGoal(memberId, dto)); + } + + @PatchMapping("/{goal_id}") + public ResponseDto updateMemberGoal( @AuthMember Long memberId, @PathVariable("goal_id") Long goalId, - @RequestBody CreateMemberGoalRequestDto dto) { - return ResponseDto.created(memberGoalService.createGoal(memberId, goalId, dto)); + @RequestBody MemberGoalRequestDto dto) { + return ResponseDto.ok(memberGoalService.updateGoal(memberId, goalId, dto)); } } diff --git a/src/main/java/com/groom/orbit/goal/dao/GoalRepository.java b/src/main/java/com/groom/orbit/goal/dao/GoalRepository.java index 4ee6508..60ca49a 100644 --- a/src/main/java/com/groom/orbit/goal/dao/GoalRepository.java +++ b/src/main/java/com/groom/orbit/goal/dao/GoalRepository.java @@ -1,7 +1,13 @@ package com.groom.orbit.goal.dao; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import com.groom.orbit.goal.dao.entity.Goal; +import com.groom.orbit.goal.dao.entity.GoalCategory; + +public interface GoalRepository extends JpaRepository { -public interface GoalRepository extends JpaRepository {} + Optional findByTitleAndCategory(String title, GoalCategory category); +} diff --git a/src/main/java/com/groom/orbit/goal/dao/MemberGoalRepository.java b/src/main/java/com/groom/orbit/goal/dao/MemberGoalRepository.java index 2bd7c9b..80c0dbf 100644 --- a/src/main/java/com/groom/orbit/goal/dao/MemberGoalRepository.java +++ b/src/main/java/com/groom/orbit/goal/dao/MemberGoalRepository.java @@ -8,11 +8,14 @@ import org.springframework.data.repository.query.Param; import com.groom.orbit.goal.dao.entity.MemberGoal; -import com.groom.orbit.goal.dao.entity.MemberGoalId; -public interface MemberGoalRepository extends JpaRepository { +public interface MemberGoalRepository extends JpaRepository { - @Query("select mg from MemberGoal mg" + " where mg.memberId=:member_id and mg.goalId=:goal_id") + @Query( + "select mg from MemberGoal mg" + + " join fetch mg.member m" + + " join fetch mg.goal g" + + " where m.id=:member_id and g.goalId=:goal_id") Optional findById(@Param("member_id") Long memberId, @Param("goal_id") Long goalId); @Query( @@ -20,7 +23,20 @@ public interface MemberGoalRepository extends JpaRepository findByIsComplete( @Param("member_id") Long memberId, @Param("is_complete") Boolean isComplete); + + @Query( + "select mg from MemberGoal mg" + + " join fetch mg.goal g" + + " join fetch mg.member m" + + " where mg.member.id=:member_id and mg.goal.goalId=:goal_id") + Optional findByMemberIdAndGoalId( + @Param("member_id") Long memberId, @Param("goal_id") Long goalId); + + // @Modifying + // @Query("UPDATE MemberGoal mg SET mg.goal.goalId = :goalId WHERE mg.memberGoalId = + // :memberGoalId") + // void updateGoalId(Long goalId); } diff --git a/src/main/java/com/groom/orbit/goal/dao/QuestRepository.java b/src/main/java/com/groom/orbit/goal/dao/QuestRepository.java index ecc9f75..5680891 100644 --- a/src/main/java/com/groom/orbit/goal/dao/QuestRepository.java +++ b/src/main/java/com/groom/orbit/goal/dao/QuestRepository.java @@ -13,25 +13,36 @@ public interface QuestRepository extends JpaRepository { @Query( "select q from Quest q" + " join fetch q.memberGoal mg" - + " where mg.memberId=:member_id and mg.goalId=:goal_id") + + " join fetch mg.goal g" + + " join fetch mg.member m" + + " where m.id=:member_id and g.goalId=:goal_id") List findByMemberIdAndGoalId( @Param("member_id") Long memberId, @Param("goal_id") Long goalId); - @Query("select count(*) from Quest q" + " join q.memberGoal mg" + " where mg.goalId=:goal_id") + @Query( + "select count(*) from Quest q" + + " join q.memberGoal mg" + + " join mg.goal g" + + " where g.goalId=:goal_id") int getCountByGoalId(@Param("goal_id") Long goalId); List findByQuestIdIn(List ids); - @Query("select count(*) from Quest q" + " join q.memberGoal mg" + " where mg.goalId=:goal_id") + @Query( + "select count(*) from Quest q" + + " join q.memberGoal mg" + + " join mg.goal g" + + " where g.goalId=:goal_id") long countByMemberGoal_GoalId(@Param("goal_id") Long goalId); @Query( "select count(*) from Quest q" + " join q.memberGoal mg" - + " where q.isComplete=true and mg.goalId=:goal_id") + + " join mg.goal g" + + " where q.isComplete=true and g.goalId=:goal_id") long countCompletedByMemberGoal_GoalId(@Param("goal_id") Long goalId); @Query( - "SELECT q FROM Quest q join fetch q.memberGoal mg WHERE MONTH(q.deadline) = :month AND mg.memberId = :memberId ORDER BY q.deadline ASC") + "SELECT q FROM Quest q join fetch q.memberGoal mg join fetch mg.member m WHERE MONTH(q.deadline) = :month AND m.id = :memberId ORDER BY q.deadline ASC") List findAllByMonthAndMemberId(Long memberId, Integer month); } diff --git a/src/main/java/com/groom/orbit/goal/dao/entity/Goal.java b/src/main/java/com/groom/orbit/goal/dao/entity/Goal.java index 31f2219..a38681c 100644 --- a/src/main/java/com/groom/orbit/goal/dao/entity/Goal.java +++ b/src/main/java/com/groom/orbit/goal/dao/entity/Goal.java @@ -35,7 +35,7 @@ public class Goal { @ColumnDefault("0") @Column(nullable = false) - private Integer count; + private Integer count = 0; public static Goal create(String title, String category) { Goal goal = new Goal(); diff --git a/src/main/java/com/groom/orbit/goal/dao/entity/MemberGoal.java b/src/main/java/com/groom/orbit/goal/dao/entity/MemberGoal.java index ca8703c..f0209c6 100644 --- a/src/main/java/com/groom/orbit/goal/dao/entity/MemberGoal.java +++ b/src/main/java/com/groom/orbit/goal/dao/entity/MemberGoal.java @@ -7,14 +7,16 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.IdClass; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicUpdate; import com.groom.orbit.member.dao.jpa.entity.Member; @@ -22,29 +24,26 @@ @Entity @Getter +@DynamicUpdate @Table(name = "member_goal") -@IdClass(MemberGoalId.class) public class MemberGoal { @Id - @Column(name = "member_id") // 필드 명시 - private Long memberId; // 변경 - - @Id - @Column(name = "goal_id") // 필드 명시 - private Long goalId; // 변경 + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_goal_id") + private Long memberGoalId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", insertable = false, updatable = false) private Member member; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "goal_id", insertable = false, updatable = false) + @JoinColumn(name = "goal_id") private Goal goal; @ColumnDefault("false") @Column(nullable = false) - private Boolean isComplete; + private Boolean isComplete = false; @OneToMany(mappedBy = "memberGoal", cascade = CascadeType.ALL, orphanRemoval = true) private List quests = new ArrayList<>(); @@ -60,4 +59,8 @@ public static MemberGoal create(Member member, Goal goal) { public String getTitle() { return this.goal.getTitle(); } + + public void updateGoal(Goal goal) { + this.goal = goal; + } } diff --git a/src/main/java/com/groom/orbit/goal/dao/entity/MemberGoalId.java b/src/main/java/com/groom/orbit/goal/dao/entity/MemberGoalId.java deleted file mode 100644 index a186ce7..0000000 --- a/src/main/java/com/groom/orbit/goal/dao/entity/MemberGoalId.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.groom.orbit.goal.dao.entity; - -import java.io.Serializable; - -import lombok.EqualsAndHashCode; -import lombok.Getter; - -@Getter -@EqualsAndHashCode -public class MemberGoalId implements Serializable { - private Long memberId; - private Long goalId; - - public static MemberGoalId create(Long memberId, Long goalId) { - MemberGoalId memberGoalId = new MemberGoalId(); - memberGoalId.memberId = memberId; - memberGoalId.goalId = goalId; - - return memberGoalId; - } -} diff --git a/src/main/java/com/groom/orbit/goal/dao/entity/Quest.java b/src/main/java/com/groom/orbit/goal/dao/entity/Quest.java index 8e2e605..364885b 100644 --- a/src/main/java/com/groom/orbit/goal/dao/entity/Quest.java +++ b/src/main/java/com/groom/orbit/goal/dao/entity/Quest.java @@ -9,7 +9,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinColumns; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @@ -18,6 +17,7 @@ import com.groom.orbit.common.dao.entity.BaseTimeEntity; import com.groom.orbit.common.exception.CommonException; import com.groom.orbit.common.exception.ErrorCode; +import com.groom.orbit.member.dao.jpa.entity.Member; import lombok.Getter; @@ -44,10 +44,7 @@ public class Quest extends BaseTimeEntity { private Integer sequence; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "member_id", referencedColumnName = "member_id"), - @JoinColumn(name = "goal_id", referencedColumnName = "goal_id") - }) + @JoinColumn(name = "member_goal_id") private MemberGoal memberGoal; public static Quest create( @@ -69,7 +66,8 @@ public void decreaseSequence() { } public void validateMember(Long memberId) { - this.memberGoal.getMemberId().equals(memberId); + Member member = this.memberGoal.getMember(); + member.validateId(memberId); } public int compareWithId(Long questId) { diff --git a/src/main/java/com/groom/orbit/member/dao/jpa/entity/Member.java b/src/main/java/com/groom/orbit/member/dao/jpa/entity/Member.java index 8480fe0..cf757d1 100644 --- a/src/main/java/com/groom/orbit/member/dao/jpa/entity/Member.java +++ b/src/main/java/com/groom/orbit/member/dao/jpa/entity/Member.java @@ -14,6 +14,8 @@ import org.hibernate.annotations.ColumnDefault; +import com.groom.orbit.common.exception.CommonException; +import com.groom.orbit.common.exception.ErrorCode; import com.groom.orbit.job.dao.jpa.entity.InterestJob; import com.groom.orbit.job.dao.jpa.entity.Job; import com.groom.orbit.member.app.dto.request.UpdateMemberRequestDto; @@ -77,4 +79,10 @@ public void updateMember(UpdateMemberRequestDto requestDto, String newProfileUrl this.isNotification = requestDto.isNotification(); this.isProfile = requestDto.isProfile(); } + + public void validateId(Long memberId) { + if (!this.id.equals(memberId)) { + throw new CommonException(ErrorCode.ACCESS_DENIED); + } + } }