diff --git a/src/main/java/dongguk/osori/domain/goal/controller/GoalCommentController.java b/src/main/java/dongguk/osori/domain/goal/controller/GoalCommentController.java index d9f6fbd..4e8797e 100644 --- a/src/main/java/dongguk/osori/domain/goal/controller/GoalCommentController.java +++ b/src/main/java/dongguk/osori/domain/goal/controller/GoalCommentController.java @@ -1,6 +1,7 @@ package dongguk.osori.domain.goal.controller; import dongguk.osori.domain.goal.dto.GoalCommentDto; +import dongguk.osori.domain.goal.dto.GoalCommentRequestDto; import dongguk.osori.domain.goal.dto.GoalCommentResponseDto; import dongguk.osori.domain.goal.service.GoalCommentService; import jakarta.servlet.http.HttpSession; @@ -26,17 +27,18 @@ public ResponseEntity addComment( @Parameter(description = "댓글을 추가할 목표의 ID", required = true) @PathVariable("goalId") Long goalId, @Parameter(description = "댓글의 내용 (이모지와 내용 포함)", required = true) - @RequestBody GoalCommentDto commentDto, + @RequestBody GoalCommentRequestDto goalRequestDto, @Parameter(hidden = true) HttpSession session) { Long userId = (Long) session.getAttribute("userId"); if (userId == null) { return ResponseEntity.status(401).build(); } - GoalCommentResponseDto response = goalCommentService.addComment(goalId, userId, commentDto); + GoalCommentResponseDto response = goalCommentService.addComment(goalId, userId, goalRequestDto); // goalRequestDto로 변경 return ResponseEntity.ok(response); } + // 댓글 삭제 @Operation(summary = "댓글 삭제", description = "특정 목표의 댓글을 삭제합니다.") @DeleteMapping("/{commentId}") diff --git a/src/main/java/dongguk/osori/domain/goal/dto/GoalCommentRequestDto.java b/src/main/java/dongguk/osori/domain/goal/dto/GoalCommentRequestDto.java new file mode 100644 index 0000000..7394e5c --- /dev/null +++ b/src/main/java/dongguk/osori/domain/goal/dto/GoalCommentRequestDto.java @@ -0,0 +1,11 @@ +package dongguk.osori.domain.goal.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GoalCommentRequestDto { + private String content; + private int emoji; +} diff --git a/src/main/java/dongguk/osori/domain/goal/service/GoalCommentService.java b/src/main/java/dongguk/osori/domain/goal/service/GoalCommentService.java index c578af3..68ed3fe 100644 --- a/src/main/java/dongguk/osori/domain/goal/service/GoalCommentService.java +++ b/src/main/java/dongguk/osori/domain/goal/service/GoalCommentService.java @@ -1,12 +1,14 @@ package dongguk.osori.domain.goal.service; -import dongguk.osori.domain.goal.dto.GoalCommentDto; import dongguk.osori.domain.goal.dto.GoalCommentResponseDto; import dongguk.osori.domain.goal.dto.GoalDetailResponseDto; +import dongguk.osori.domain.goal.dto.GoalCommentRequestDto; import dongguk.osori.domain.goal.entity.Goal; import dongguk.osori.domain.goal.entity.GoalComment; import dongguk.osori.domain.goal.repository.GoalCommentRepository; import dongguk.osori.domain.goal.repository.GoalRepository; +import dongguk.osori.domain.quest.entity.MissionType; +import dongguk.osori.domain.quest.service.QuestService; import dongguk.osori.domain.user.entity.User; import dongguk.osori.domain.user.repository.UserRepository; import jakarta.transaction.Transactional; @@ -26,24 +28,28 @@ public class GoalCommentService { private final GoalCommentRepository goalCommentRepository; private final GoalRepository goalRepository; private final UserRepository userRepository; + private final QuestService questService; // 댓글 추가 @Transactional - public GoalCommentResponseDto addComment(Long goalId, Long userId, GoalCommentDto commentDto) { + public GoalCommentResponseDto addComment(Long goalId, Long userId, GoalCommentRequestDto goalRequestDto) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); Goal goal = goalRepository.findById(goalId) .orElseThrow(() -> new RuntimeException("Goal not found")); GoalComment comment = GoalComment.builder() - .content(commentDto.getContent()) - .emoji(commentDto.getEmoji()) + .content(goalRequestDto.getContent()) + .emoji(goalRequestDto.getEmoji()) .goal(goal) .user(user) .build(); goalCommentRepository.save(comment); + questService.updateMissionStatus(userId, MissionType.COMMENTED_ON_FRIEND_GOAL); + + return new GoalCommentResponseDto( comment.getCommentId(), user.getNickname(), @@ -53,6 +59,7 @@ public GoalCommentResponseDto addComment(Long goalId, Long userId, GoalCommentDt ); } + // 댓글 조회 @Transactional public Optional getGoalDetailsWithComments(Long goalId) { diff --git a/src/main/java/dongguk/osori/domain/goal/service/GoalService.java b/src/main/java/dongguk/osori/domain/goal/service/GoalService.java index ff60f60..dcc5e7c 100644 --- a/src/main/java/dongguk/osori/domain/goal/service/GoalService.java +++ b/src/main/java/dongguk/osori/domain/goal/service/GoalService.java @@ -4,6 +4,8 @@ import dongguk.osori.domain.goal.repository.GoalCommentRepository; import dongguk.osori.domain.goal.repository.GoalRepository; import dongguk.osori.domain.goal.entity.Goal; +import dongguk.osori.domain.quest.entity.MissionType; +import dongguk.osori.domain.quest.service.QuestService; import dongguk.osori.domain.user.entity.User; import dongguk.osori.domain.user.repository.UserRepository; import jakarta.transaction.Transactional; @@ -25,6 +27,7 @@ public class GoalService { private final GoalRepository goalRepository; private final UserRepository userRepository; private final GoalCommentRepository goalCommentRepository; + private final QuestService questService; // 로그인된 사용자 가져오기 private User getLoggedInUser(Long userId) { @@ -45,18 +48,18 @@ public List getUserGoals(Long userId) { // 로그인된 사용자의 목표 생성 @Transactional public GoalResponseDto createGoal(GoalDto goalDto, Long userId) { - log.debug("Creating goal for user ID: {}", userId); // 디버깅용 User user = getLoggedInUser(userId); - log.debug("User found: {}", user); // 디버깅용 - // Goal Builder 사용하여 새로운 목표 생성 Goal goal = Goal.builder() .content(goalDto.getContent()) .user(user) .build(); - log.debug("New goal created: {}", goal); // 디버깅용 goalRepository.save(goal); + + // 퀘스트 업데이트 + questService.updateMissionStatus(userId, MissionType.GOAL_WRITTEN); + return new GoalResponseDto(goal.getGoalId(), goal.getContent(), goal.getCreatedAt(), goal.isCompleted()); } diff --git a/src/main/java/dongguk/osori/domain/portfolio/service/PortfolioService.java b/src/main/java/dongguk/osori/domain/portfolio/service/PortfolioService.java index 2546bd1..f6320fa 100644 --- a/src/main/java/dongguk/osori/domain/portfolio/service/PortfolioService.java +++ b/src/main/java/dongguk/osori/domain/portfolio/service/PortfolioService.java @@ -3,6 +3,8 @@ import dongguk.osori.domain.portfolio.dto.*; import dongguk.osori.domain.portfolio.entity.*; import dongguk.osori.domain.portfolio.repository.PortfolioRepository; +import dongguk.osori.domain.quest.entity.MissionType; +import dongguk.osori.domain.quest.service.QuestService; import dongguk.osori.domain.user.entity.User; import dongguk.osori.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -20,6 +22,7 @@ public class PortfolioService { private final PortfolioRepository portfolioRepository; private final UserRepository userRepository; + private final QuestService questService; @Transactional public PortfolioDetailDto createPortfolio(Long userId, PortfolioRequestDto requestDto) { @@ -53,6 +56,9 @@ public PortfolioDetailDto createPortfolio(Long userId, PortfolioRequestDto reque portfolioRepository.save(portfolio); + // 퀘스트 업데이트 + questService.updateMissionStatus(userId, MissionType.PORTFOLIO_WRITTEN); + return mapToDetailDto(portfolio); } diff --git a/src/main/java/dongguk/osori/domain/quest/controller/QuestController.java b/src/main/java/dongguk/osori/domain/quest/controller/QuestController.java new file mode 100644 index 0000000..bb5186b --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/controller/QuestController.java @@ -0,0 +1,38 @@ +package dongguk.osori.domain.quest.controller; + +import dongguk.osori.domain.goal.dto.GoalResponseDto; +import dongguk.osori.domain.quest.dto.QuestAndStampStatusDto; +import dongguk.osori.domain.quest.dto.QuestStatusDto; +import dongguk.osori.domain.quest.service.QuestService; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; + +@RestController +@RequestMapping("/api/quests") +@RequiredArgsConstructor +public class QuestController { + + private final QuestService questService; + + @GetMapping("/status") + public ResponseEntity getQuestStatus(HttpSession session) { + Long userId = (Long) session.getAttribute("userId"); + if (userId == null) { + return ResponseEntity.status(401).build(); // Unauthorized 응답 + } + + QuestStatusDto questStatus = questService.getQuestStatus(userId, LocalDate.now()); + int stampCount = questService.getCurrentStampCount(userId); // 현재 스탬프 개수 가져오기 + boolean fullyCompleted = stampCount == 7; // 스탬프가 모두 채워졌는지 확인 + + QuestAndStampStatusDto response = new QuestAndStampStatusDto(questStatus, stampCount, fullyCompleted); + return ResponseEntity.ok(response); + } +} + diff --git a/src/main/java/dongguk/osori/domain/quest/dto/MissionRequestDto.java b/src/main/java/dongguk/osori/domain/quest/dto/MissionRequestDto.java new file mode 100644 index 0000000..61dacec --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/dto/MissionRequestDto.java @@ -0,0 +1,12 @@ +package dongguk.osori.domain.quest.dto; + +import dongguk.osori.domain.quest.entity.MissionType; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class MissionRequestDto { + private LocalDate date; + private MissionType missionType; +} diff --git a/src/main/java/dongguk/osori/domain/quest/dto/QuestAndStampStatusDto.java b/src/main/java/dongguk/osori/domain/quest/dto/QuestAndStampStatusDto.java new file mode 100644 index 0000000..40c6fdd --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/dto/QuestAndStampStatusDto.java @@ -0,0 +1,12 @@ +package dongguk.osori.domain.quest.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class QuestAndStampStatusDto { + private QuestStatusDto questStatus; + private int stampCount; + private boolean fullyCompleted; +} diff --git a/src/main/java/dongguk/osori/domain/quest/dto/QuestStatusDto.java b/src/main/java/dongguk/osori/domain/quest/dto/QuestStatusDto.java new file mode 100644 index 0000000..3b66f92 --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/dto/QuestStatusDto.java @@ -0,0 +1,14 @@ +package dongguk.osori.domain.quest.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class QuestStatusDto { + private boolean attended; + private boolean goalWritten; + private boolean commentedOnFriendGoal; + private boolean portfolioWritten; +} + diff --git a/src/main/java/dongguk/osori/domain/quest/entity/MissionType.java b/src/main/java/dongguk/osori/domain/quest/entity/MissionType.java new file mode 100644 index 0000000..96b5d6b --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/entity/MissionType.java @@ -0,0 +1,9 @@ +package dongguk.osori.domain.quest.entity; + +public enum MissionType { + GOAL_WRITTEN, + COMMENTED_ON_FRIEND_GOAL, + PORTFOLIO_WRITTEN, + ATTENDANCE_CHECKED +} + diff --git a/src/main/java/dongguk/osori/domain/quest/entity/Quest.java b/src/main/java/dongguk/osori/domain/quest/entity/Quest.java new file mode 100644 index 0000000..257d53f --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/entity/Quest.java @@ -0,0 +1,63 @@ +package dongguk.osori.domain.quest.entity; + +import dongguk.osori.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Entity +@Getter +@NoArgsConstructor +public class Quest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDate date; // 퀘스트가 속한 날짜 + + @Column(nullable = false) + private boolean attended; // 출석 여부 + + @Column(nullable = false) + private boolean goalWritten; // 목표 작성 여부 + + @Column(nullable = false) + private boolean commentedOnFriendGoal; // 친구 목표에 댓글 단 여부 + + @Column(nullable = false) + private boolean portfolioWritten; // 포트폴리오 작성 여부 + + public Quest(User user, LocalDate date) { + this.user = user; + this.date = date; + } + + public boolean isAllMissionsCompleted() { + return attended && goalWritten && commentedOnFriendGoal && portfolioWritten; + } + + public void markAttended() { + this.attended = true; + } + + public void markGoalWritten() { + this.goalWritten = true; + } + + public void markCommentedOnFriendGoal() { + this.commentedOnFriendGoal = true; + } + + public void markPortfolioWritten() { + this.portfolioWritten = true; + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} + diff --git a/src/main/java/dongguk/osori/domain/quest/entity/Stamp.java b/src/main/java/dongguk/osori/domain/quest/entity/Stamp.java new file mode 100644 index 0000000..7340edd --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/entity/Stamp.java @@ -0,0 +1,46 @@ +package dongguk.osori.domain.quest.entity; + +import dongguk.osori.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Stamp { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private int weekNumber; // 몇 주차 스탬프인지 + + @Column(nullable = false) + private int year; // 해당 연도 + + @Column(nullable = false) + private int count; // 스탬프 개수 (최대 7) + + public Stamp(User user, int weekNumber, int year) { + this.user = user; + this.weekNumber = weekNumber; + this.year = year; + } + + public void incrementStamp() { + if (count < 7) { + count++; + } + } + + public boolean isFullyCompleted() { + return count == 7; + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} + diff --git a/src/main/java/dongguk/osori/domain/quest/repository/QuestRepository.java b/src/main/java/dongguk/osori/domain/quest/repository/QuestRepository.java new file mode 100644 index 0000000..36c136e --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/repository/QuestRepository.java @@ -0,0 +1,15 @@ +package dongguk.osori.domain.quest.repository; + +import dongguk.osori.domain.quest.entity.Quest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.Optional; + +@Repository +public interface QuestRepository extends JpaRepository { + Optional findByUser_UserIdAndDate(Long userId, LocalDate date); + +} + diff --git a/src/main/java/dongguk/osori/domain/quest/repository/StampRepository.java b/src/main/java/dongguk/osori/domain/quest/repository/StampRepository.java new file mode 100644 index 0000000..785c210 --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/repository/StampRepository.java @@ -0,0 +1,13 @@ +package dongguk.osori.domain.quest.repository; + +import dongguk.osori.domain.quest.entity.Stamp; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface StampRepository extends JpaRepository { + Optional findByUser_UserIdAndWeekNumberAndYear(Long userId, int weekNumber, int year); +} + diff --git a/src/main/java/dongguk/osori/domain/quest/service/QuestService.java b/src/main/java/dongguk/osori/domain/quest/service/QuestService.java new file mode 100644 index 0000000..1a56cd0 --- /dev/null +++ b/src/main/java/dongguk/osori/domain/quest/service/QuestService.java @@ -0,0 +1,127 @@ +package dongguk.osori.domain.quest.service; + +import dongguk.osori.domain.quest.dto.QuestStatusDto; +import dongguk.osori.domain.quest.entity.MissionType; +import dongguk.osori.domain.quest.dto.MissionRequestDto; +import dongguk.osori.domain.quest.entity.Quest; +import dongguk.osori.domain.quest.entity.Stamp; +import dongguk.osori.domain.quest.repository.QuestRepository; +import dongguk.osori.domain.quest.repository.StampRepository; +import dongguk.osori.domain.user.entity.User; +import dongguk.osori.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.temporal.WeekFields; + +@Service +@RequiredArgsConstructor +public class QuestService { + + private final QuestRepository questRepository; + private final StampRepository stampRepository; + private final UserRepository userRepository; + + @Transactional + public void completeMission(Long userId, LocalDate date) { + Quest quest = getOrCreateQuest(userId, date); + + // 모든 미션이 완료되었는지 확인 + if (!quest.isAllMissionsCompleted()) { + throw new IllegalStateException("모든 미션이 완료되지 않았습니다."); + } + + // 스탬프 증가 + incrementStamp(userId, date); + } + + private void incrementStamp(Long userId, LocalDate date) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다.")); + + int weekNumber = date.get(WeekFields.ISO.weekOfWeekBasedYear()); + int year = date.getYear(); + + Stamp stamp = stampRepository.findByUser_UserIdAndWeekNumberAndYear(userId, weekNumber, year) + .orElseGet(() -> new Stamp(user, weekNumber, year)); + + stamp.incrementStamp(); + stampRepository.save(stamp); + } + + private Quest getOrCreateQuest(Long userId, LocalDate date) { + return questRepository.findByUser_UserIdAndDate(userId, date) + .orElseGet(() -> { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다.")); + return questRepository.save(new Quest(user, date)); + }); + } + + @Transactional + public void updateMissionStatus(Long userId, MissionType missionType) { + Quest quest = getOrCreateQuest(userId, LocalDate.now()); + + switch (missionType) { + case ATTENDANCE_CHECKED: + if (!quest.isAttended()) { + quest.markAttended(); + } + break; + case GOAL_WRITTEN: + if (!quest.isGoalWritten()) { + quest.markGoalWritten(); + } + break; + case COMMENTED_ON_FRIEND_GOAL: + if (!quest.isCommentedOnFriendGoal()) { + quest.markCommentedOnFriendGoal(); + } + break; + case PORTFOLIO_WRITTEN: + if (!quest.isPortfolioWritten()) { + quest.markPortfolioWritten(); + } + break; + default: + throw new IllegalArgumentException("잘못된 미션 타입입니다."); + } + + questRepository.save(quest); + + // 모든 미션이 완료되었다면 스탬프 추가 + if (quest.isAllMissionsCompleted()) { + completeMission(userId, LocalDate.now()); + } + } + + @Transactional(readOnly = true) + public QuestStatusDto getQuestStatus(Long userId, LocalDate date) { + Quest quest = questRepository.findByUser_UserIdAndDate(userId, date) + .orElse(new Quest(userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found")), date)); + + return new QuestStatusDto( + quest.isAttended(), + quest.isGoalWritten(), + quest.isCommentedOnFriendGoal(), + quest.isPortfolioWritten() + ); + } + + @Transactional(readOnly = true) + public int getCurrentStampCount(Long userId) { + LocalDate today = LocalDate.now(); + int weekNumber = today.get(WeekFields.ISO.weekOfWeekBasedYear()); + int year = today.getYear(); + + return stampRepository.findByUser_UserIdAndWeekNumberAndYear(userId, weekNumber, year) + .map(Stamp::getCount) + .orElse(0); + } + +} + + diff --git a/src/main/java/dongguk/osori/domain/user/service/UserService.java b/src/main/java/dongguk/osori/domain/user/service/UserService.java index 1df4475..a91533d 100644 --- a/src/main/java/dongguk/osori/domain/user/service/UserService.java +++ b/src/main/java/dongguk/osori/domain/user/service/UserService.java @@ -1,5 +1,8 @@ package dongguk.osori.domain.user.service; +import dongguk.osori.domain.quest.dto.MissionRequestDto; +import dongguk.osori.domain.quest.entity.MissionType; +import dongguk.osori.domain.quest.service.QuestService; import dongguk.osori.domain.user.repository.UserRepository; import dongguk.osori.domain.user.dto.*; import dongguk.osori.domain.user.entity.User; @@ -9,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.util.List; import java.util.stream.Collectors; @@ -19,6 +23,7 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final QuestService questService; // 회원가입 로직 @Transactional @@ -41,7 +46,7 @@ public void signup(SignupUserDto signupUserDto) { // 로그인 인증 로직 - @Transactional(readOnly = true) + @Transactional public Long authenticate(String email, String password) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("해당 이메일을 가진 유저가 없습니다.")); @@ -51,6 +56,9 @@ public Long authenticate(String email, String password) { throw new IllegalArgumentException("잘못된 비밀번호입니다."); } + // 퀘스트 업데이트 + questService.updateMissionStatus(user.getUserId(), MissionType.ATTENDANCE_CHECKED); + // 인증 성공 시 유저 ID 반환 return user.getUserId(); }