Skip to content

Commit

Permalink
Merge pull request #56 from IT-Cotato/feat/my-page-#25
Browse files Browse the repository at this point in the history
Feat : 마이페이지, 출석 관련 기능 구현 #25
  • Loading branch information
chaen-ing authored Feb 5, 2025
2 parents d21c073 + 84bbb0d commit 4a1db23
Show file tree
Hide file tree
Showing 32 changed files with 652 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
package com.ripple.BE.auth.dto.kakao;

public record KakaoUserInfoResponse(
Long id, KakaoProperties properties, KakaoAccount kakao_account) {}
public record KakaoUserInfoResponse(Long id, KakaoAccount kakao_account) {}
11 changes: 11 additions & 0 deletions src/main/java/com/ripple/BE/image/controller/ImageController.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,15 @@ public ResponseEntity<ApiResponse<Object>> deleteImage(
imageService.deleteImage(imageId);
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE));
}

@Operation(summary = "유저 프로필 사진 추가", description = "프로필 등록 전 유저 프로필 사진을 추가합니다.")
@PostMapping(value = "/profile", consumes = "multipart/form-data")
public ResponseEntity<ApiResponse<Object>> createProfileImage(
final @RequestParam MultipartFile file) {

long imageId = imageService.addProfileImage(file);

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(ImageIdResponse.toImageIdResponse(imageId)));
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/ripple/BE/image/domain/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.ripple.BE.global.entity.BaseEntity;
import com.ripple.BE.news.domain.News;
import com.ripple.BE.post.domain.Post;
import com.ripple.BE.user.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -11,6 +12,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -44,6 +46,10 @@ public class Image extends BaseEntity {
@JoinColumn(name = "news_id")
private News news;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_image_id")
private User user;

public static Image toImageEntity(final S3Info s3Info) {
return Image.builder().s3Info(s3Info).build();
}
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/ripple/BE/image/service/ImageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,13 @@ public void deleteImage(long imageId) {

imageRepository.delete(image);
}

@Transactional
public long addProfileImage(MultipartFile file) {

S3Info s3Info = s3Uploader.uploadFiles(file, "profile");

Image image = imageRepository.save(Image.toImageEntity(s3Info));
return image.getId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.ripple.BE.learning.service.learningset.LearningSetService;
import com.ripple.BE.user.domain.User;
import com.ripple.BE.user.domain.type.Level;
import com.ripple.BE.user.service.AttendanceService;
import com.ripple.BE.user.service.UserProgressService;
import com.ripple.BE.user.service.UserService;
import java.util.List;
Expand All @@ -31,6 +32,7 @@ public class ConceptService {
private final LearningSetService learningSetService;
private final UserService userService;
private final UserProgressService userProgressService;
private final AttendanceService attendanceService;

private final UserLearningSetRepository userLearningSetRepository;
private final ConceptRepository conceptRepository;
Expand Down Expand Up @@ -74,6 +76,8 @@ public void completeConceptLearning(

userProgressService.updateLevel(user);
}

attendanceService.completeQuest(userId, "CONCEPT");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.ripple.BE.learning.service.learningset.LearningSetService;
import com.ripple.BE.user.domain.User;
import com.ripple.BE.user.domain.type.Level;
import com.ripple.BE.user.service.AttendanceService;
import com.ripple.BE.user.service.UserProgressService;
import com.ripple.BE.user.service.UserService;
import java.util.Collections;
Expand All @@ -46,6 +47,7 @@ public class QuizService {
private final LearningSetService learningSetService;
private final UserService userService;
private final UserProgressService userProgressService;
private final AttendanceService attendanceService;

private static final String QUESTION_TYPE = "questions";
private static final String WRONG_ANSWER_TYPE = "wrongAnswer";
Expand Down Expand Up @@ -145,6 +147,8 @@ public void finishQuiz(final long userId, final long learningSetId, final Level
}

quizRedisService.clearRedisKeys(userId); // 퀴즈 진행 관련 데이터 삭제

attendanceService.completeQuest(userId, "QUIZ");
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ public ResponseEntity<ApiResponse<Object>> getNewsList(

@Operation(summary = "뉴스 상세 조회", description = "뉴스의 상세 정보를 조회합니다.")
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Object>> getNews(final @PathVariable("id") long id) {
public ResponseEntity<ApiResponse<Object>> getNews(
final @PathVariable("id") long id,
final @AuthenticationPrincipal CustomUserDetails currentUser) {

NewsDTO newsDTO = newsService.getNews(id);
NewsDTO newsDTO = newsService.getNews(id, currentUser.getId());
return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(NewsResponse.toNewsResponse(newsDTO)));
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/com/ripple/BE/news/service/NewsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.ripple.BE.news.repository.news.NewsRepository;
import com.ripple.BE.news.repository.newscrap.NewsScrapRepository;
import com.ripple.BE.user.domain.User;
import com.ripple.BE.user.service.AttendanceService;
import com.ripple.BE.user.service.UserService;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -37,6 +38,7 @@ public class NewsService {

private final UserService userService;
private final List<NewsCrawler> crawlers;
private final AttendanceService attendanceService;

private static final int PAGE_SIZE = 10;

Expand All @@ -60,11 +62,12 @@ public NewsListDTO getNewsList(
}

@Transactional
public NewsDTO getNews(final long id) {
public NewsDTO getNews(final long id, final long userId) {
News news =
newsRepository.findByIdForUpdate(id).orElseThrow(() -> new NewsException(NEWS_NOT_FOUND));

news.increaseViews(); // 조회수 증가
attendanceService.completeQuest(userId, "ARTICLE"); // 퀘스트 완료

return NewsDTO.toNewsDTO(news);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static CommentResponse toCommentResponse(CommentDTO commentDTO) {
commentDTO.likeCount(),
commentDTO.commenter().id(),
commentDTO.commenter().nickname(),
commentDTO.commenter().profileImage().url(),
commentDTO.commenter().profileImage().getS3Info().getUrl(),
commentDTO.isDeleted(),
commentDTO.isAuthor(),
commentDTO.isLiked(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static PostResponse toPostResponse(PostDTO postDTO) {
postDTO.id(),
postDTO.title(),
postDTO.author().nickname(),
postDTO.author().profileImage().url(),
postDTO.author().profileImage().getS3Info().getUrl(),
postDTO.content(),
postDTO.type(),
postDTO.likeCount(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.ripple.BE.user.controller;

import com.ripple.BE.global.dto.response.ApiResponse;
import com.ripple.BE.user.domain.CustomUserDetails;
import com.ripple.BE.user.dto.QuestDTO;
import com.ripple.BE.user.dto.response.QuestResponse;
import com.ripple.BE.user.service.AttendanceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/attencance")
@Tag(name = "Attendance", description = "출석 API")
public class AttendanceController {

private final AttendanceService attendanceService;

@Operation(summary = "연속 출석 날짜 조회", description = "현재 사용자의 연속 출석 날짜를 조회합니다.")
@GetMapping("/current-streak")
public ResponseEntity<ApiResponse<?>> currentStreak(
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

Long currentStreak = attendanceService.getCurrentStreak(customUserDetails.getId());

return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(currentStreak));
}

@Operation(
summary = "오늘의 퀘스트 완료 여부 조회",
description = "오늘의 퀘스트 완료 여부를 조회합니다. 퍼센트로 반환됩니다. (0~100)")
@GetMapping("/today-quest")
public ResponseEntity<ApiResponse<?>> todayQuest(
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

QuestDTO todayQuest = attendanceService.getTodayQuest(customUserDetails.getId());
return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(QuestResponse.toQuestResponse(todayQuest)));
}

@Operation(summary = "요일별 출석 현황 조회", description = "요일별 출석 현황을 조회합니다. 매주 자동으로 초기화됩니다.")
@GetMapping("/weekly-attendance")
public ResponseEntity<ApiResponse<?>> weeklyAttendance(
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(attendanceService.getWeeklyAttendance(customUserDetails.getId())));
}
}
26 changes: 23 additions & 3 deletions src/main/java/com/ripple/BE/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import com.ripple.BE.user.domain.CustomUserDetails;
import com.ripple.BE.user.domain.type.Level;
import com.ripple.BE.user.dto.ProgressDTO;
import com.ripple.BE.user.dto.ProgressResponse;
import com.ripple.BE.user.dto.UpdateUserProfileRequest;
import com.ripple.BE.user.dto.UserInfoDTO;
import com.ripple.BE.user.dto.request.UpdateUserProfileRequest;
import com.ripple.BE.user.dto.response.ProgressResponse;
import com.ripple.BE.user.dto.response.UserInfoResponse;
import com.ripple.BE.user.service.MyPageService;
import com.ripple.BE.user.service.UserProgressService;
import com.ripple.BE.user.service.UserService;
Expand Down Expand Up @@ -50,7 +52,10 @@ public class UserController {
private final MyPageService myPageService;
private final UserProgressService userProgressService;

@Operation(summary = "프로필 등록", description = "로그인 후 유저의 프로필을 등록합니다.")
@Operation(
summary = "프로필 등록",
description =
"로그인 후 유저의 프로필을 등록합니다." + "프로필을 등록하기 전 이미지 등록을 완료해주세요. 이미지 등록 후 반한 된 이미지 ID를 입력해주세요.")
@PostMapping("/profile")
public ResponseEntity<ApiResponse<?>> profile(
@Valid @RequestBody UpdateUserProfileRequest request,
Expand Down Expand Up @@ -221,4 +226,19 @@ public ResponseEntity<ApiResponse<Object>> getMyScrapTermsByKeyword(
return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(TermListResponse.toTermListResponse(termListDTO)));
}

@Operation(
summary = "회원 정보 조회",
description =
"로그인한 유저의 회원 정보를 조회합니다."
+ " 프로필 사진 URL, 닉네임, 한줄소개, 생일, 업종, 직업, 연속 출석 일수, 레벨, 퀴즈 정답률을 반환합니다.")
@GetMapping("/info")
public ResponseEntity<ApiResponse<Object>> getUserInfo(
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

UserInfoDTO userInfo = userService.getUserInfo(customUserDetails.getId());

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(UserInfoResponse.toUserInfoResponse(userInfo)));
}
}
40 changes: 12 additions & 28 deletions src/main/java/com/ripple/BE/user/domain/Attendance.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "attendance")
@Table(name = "attendances")
@Getter
@Builder
@Entity
Expand All @@ -29,37 +29,21 @@ public class Attendance extends BaseEntity {
@Column(name = "attendance_id")
private Long id;

@Column(name = "monday")
private Boolean monday;

@Column(name = "tuesday")
private Boolean tuesday;

@Column(name = "wednesday")
private Boolean wednesday;

@Column(name = "thursday")
private Boolean thursday;

@Column(name = "friday")
private Boolean friday;

@Column(name = "saturday")
private Boolean saturday;

@Column(name = "sunday")
private Boolean sunday;

@Column(name = "current_streak")
private Long currentStreak;

@Column(name = "last_reset_date")
private LocalDateTime lastResetDate;
private Long currentStreak; // 현재 연속 출석일

@Column(name = "last_attended_date")
private LocalDateTime lastAttendedDate;
private LocalDate lastAttendedDate; // 마지막 출석 날짜

@OneToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;

public void updateCurrentStreak(long currentStreak) {
this.currentStreak = currentStreak;
}

public void updateLastAttendedDate(LocalDate lastAttendedDate) {
this.lastAttendedDate = lastAttendedDate;
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/ripple/BE/user/domain/AttendanceLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.ripple.BE.user.domain;

import com.ripple.BE.global.entity.BaseEntity;
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.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "attendance_logs")
@Getter
@Builder
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class AttendanceLog extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "attendance_log_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "attendance_id")
private Attendance attendance;

private LocalDate date; // 출석 날짜

private boolean isAttended; // 출석 여부

public void updateIsAttended() {
this.isAttended = true;
}
}
Loading

0 comments on commit 4a1db23

Please sign in to comment.