Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
package grep.neogul_coder.domain.buddy.controller;

import grep.neogul_coder.domain.buddy.controller.dto.response.BuddyEnergyResponse;
import grep.neogul_coder.domain.buddy.enums.BuddyEnergyReason;
import grep.neogul_coder.domain.buddy.service.BuddyEnergyService;
import grep.neogul_coder.global.response.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/buddy-energy")
public class BuddyEnergyController implements BuddyEnergySpecification {

private final BuddyEnergyService buddyEnergyService;

@GetMapping("/{userId}")
public ApiResponse<BuddyEnergyResponse> get(@PathVariable("userId") Long userId) {
return ApiResponse.success(new BuddyEnergyResponse());
return ApiResponse.success(buddyEnergyService.getBuddyEnergy(userId));
}



}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package grep.neogul_coder.domain.buddy.controller.dto.response;

import grep.neogul_coder.domain.buddy.entity.BuddyEnergy;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -14,5 +15,18 @@ public class BuddyEnergyResponse {
@Schema(description = "현재 버디에너지 수치", example = "61")
private int level;

@Builder
protected BuddyEnergyResponse(Long userId, int level) {
this.userId = userId;
this.level = level;
}

public static BuddyEnergyResponse from(BuddyEnergy energy) {
return BuddyEnergyResponse.builder()
.userId(energy.getUserId())
.level(energy.getLevel())
.build();
}


}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package grep.neogul_coder.domain.buddy.entity;

import grep.neogul_coder.domain.buddy.enums.BuddyEnergyReason;
import grep.neogul_coder.domain.review.ReviewType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand All @@ -20,4 +22,30 @@ public class BuddyEnergy {

private int level;

protected BuddyEnergy() {

}


private BuddyEnergy(Long userId, int level) {
this.userId = userId;
this.level = level;
}

public static BuddyEnergy createDefault(Long userId) {
return new BuddyEnergy(userId, BuddyEnergyReason.SIGN_UP.getPoint());
}

public void updateLevel(int newLevel) {
this.level = newLevel;
}

// 리뷰 타입 기반 에너지 변경 //
public void updateEnergy(ReviewType reviewType) {
if (reviewType == ReviewType.GOOD || reviewType == ReviewType.EXCELLENT) {
this.level += 1;
} else if (reviewType == ReviewType.BAD) {
this.level -= 1;
}
}
}
18 changes: 18 additions & 0 deletions src/main/java/grep/neogul_coder/domain/buddy/entity/BuddyLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Builder;

@Entity
public class BuddyLog {
Expand All @@ -27,4 +28,21 @@ public class BuddyLog {
@JoinColumn(name = "buddy_energy_id")
private BuddyEnergy buddyEnergy;

protected BuddyLog() {

}

@Builder
private BuddyLog(BuddyEnergy buddyEnergy, BuddyEnergyReason reason) {
this.buddyEnergy = buddyEnergy;
this.reason = reason;
}

public static BuddyLog of(BuddyEnergy energy, BuddyEnergyReason reason) {
return BuddyLog.builder()
.buddyEnergy(energy)
.reason(reason)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package grep.neogul_coder.domain.buddy.exception;

import grep.neogul_coder.global.exception.business.NotFoundException;
import grep.neogul_coder.domain.buddy.exception.code.BuddyEnergyErrorCode;

public class BuddyEnergyNotFoundException extends NotFoundException {
public BuddyEnergyNotFoundException(BuddyEnergyErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package grep.neogul_coder.domain.buddy.exception.code;

import grep.neogul_coder.global.response.code.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum BuddyEnergyErrorCode implements ErrorCode {

BUDDY_ENERGY_NOT_FOUND("BE404", HttpStatus.NOT_FOUND, "해당 유저의 버디 에너지를 찾을 수 없습니다.");

private final String code;
private final HttpStatus status;
private final String message;

BuddyEnergyErrorCode(String code, HttpStatus status, String message) {
this.code = code;
this.status = status;
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package grep.neogul_coder.domain.buddy.repository;

import grep.neogul_coder.domain.buddy.entity.BuddyLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface BuddyEnergyLogRepository extends JpaRepository<BuddyLog, Long> {

// 특정 유저의 에너지 변동 로그 조회
// (BuddyEnergy -> UserId로 매핑 필요할 수 있음)
List<BuddyLog> findByBuddyEnergy_BuddyEnergyId(Long buddyEnergyId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package grep.neogul_coder.domain.buddy.service;

import grep.neogul_coder.domain.buddy.entity.BuddyEnergy;
import grep.neogul_coder.domain.buddy.entity.BuddyLog;
import grep.neogul_coder.domain.buddy.enums.BuddyEnergyReason;
import grep.neogul_coder.domain.buddy.exception.BuddyEnergyNotFoundException;
import grep.neogul_coder.domain.buddy.exception.code.BuddyEnergyErrorCode;
import grep.neogul_coder.domain.buddy.repository.BuddyEnergyLogRepository;
import grep.neogul_coder.domain.buddy.repository.BuddyEnergyRepository;
import grep.neogul_coder.domain.buddy.controller.dto.response.BuddyEnergyResponse;
import grep.neogul_coder.domain.review.ReviewType;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import static grep.neogul_coder.domain.buddy.exception.code.BuddyEnergyErrorCode.BUDDY_ENERGY_NOT_FOUND;

@Service
@RequiredArgsConstructor
public class BuddyEnergyService {

private final BuddyEnergyRepository buddyEnergyRepository;
private final BuddyEnergyLogRepository buddyEnergyLogRepository;

// 현재 버디 에너지 조회
@Transactional
public BuddyEnergyResponse getBuddyEnergy(Long userId) {
BuddyEnergy energy = buddyEnergyRepository.findByUserId(userId)
.orElseThrow(() -> new BuddyEnergyNotFoundException(BUDDY_ENERGY_NOT_FOUND));
return BuddyEnergyResponse.from(energy);
}

// ReviewType 기반 업데이트 //
@Transactional
public BuddyEnergyResponse updateEnergyByReview(Long userId, ReviewType reviewType) {
BuddyEnergy energy = buddyEnergyRepository.findByUserId(userId)
.orElseThrow(() -> new BuddyEnergyNotFoundException(BUDDY_ENERGY_NOT_FOUND));

// ReviewType에 따른 에너지 증감
energy.updateEnergy(reviewType);

buddyEnergyLogRepository.save(BuddyLog.of(energy, toBuddyEnergyReason(reviewType)));
buddyEnergyRepository.save(energy);

return BuddyEnergyResponse.from(energy);
}

// 회원가입 시 기본 에너지 생성
@Transactional
public BuddyEnergyResponse createDefaultEnergy(Long userId) {
BuddyEnergy energy = BuddyEnergy.createDefault(userId);
BuddyEnergy saved = buddyEnergyRepository.save(energy);

buddyEnergyLogRepository.save(BuddyLog.of(saved, BuddyEnergyReason.SIGN_UP));
return BuddyEnergyResponse.from(saved);
}


// ReviewType → BuddyEnergyReason 매핑 메서드
private BuddyEnergyReason toBuddyEnergyReason(ReviewType reviewType) {
return switch (reviewType) {
case GOOD, EXCELLENT -> BuddyEnergyReason.POSITIVE_REVIEW;
case BAD -> BuddyEnergyReason.NEGATIVE_REVIEW;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public class PersonalCalendarController implements PersonalCalendarSpecification

// 사용자 개인 일정 등록 API
@PostMapping
public ApiResponse<Void> create(
public ApiResponse<Long> create(
@PathVariable("userId") Long userId,
@Valid @RequestBody PersonalCalendarRequest request) {
personalCalendarService.create(userId, request);
return ApiResponse.noContent();
Long calendarId = personalCalendarService.create(userId, request);
return ApiResponse.success(calendarId); // 생성된 일정 ID 반환
}

// 사용자 개인 일정 전체 조회 API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public interface PersonalCalendarSpecification {
summary = "개인 일정 생성",
description = "사용자의 개인 일정을 생성합니다.\n\n예: `/api/users/{userId}/calendar`"
)
ApiResponse<Void> create(
ApiResponse<Long> create(
@Parameter(name = "userId", description = "사용자 ID", required = true, in = ParameterIn.PATH)
@PathVariable("userId") Long userId,
@RequestBody PersonalCalendarRequest request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public class TeamCalendarController implements TeamCalendarSpecification {

// 팀 일정 생성
@PostMapping
public ApiResponse<Void> create(
public ApiResponse<Long> create(
@AuthenticationPrincipal(expression = "userId") Long userId, // 인증된 사용자의 ID를 자동 주입받음
@PathVariable("studyId") Long studyId,
@Valid @RequestBody TeamCalendarRequest request
) {
teamCalendarService.create(studyId, userId, request);
return ApiResponse.noContent();
Long calendarId = teamCalendarService.create(studyId, userId, request);
return ApiResponse.success(calendarId); // 생성된 일정 ID 반환
}

// 전체 일정 조회 API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ ApiResponse<List<TeamCalendarResponse>> findByDate(
description = "특정 팀 ID에 새로운 일정을 생성합니다.\n\n" +
"예: `/api/teams/{studyId}/calendar`"
)
ApiResponse<Void> create(
ApiResponse<Long> create(
@Parameter(hidden = true) @AuthenticationPrincipal Long userId,
@Parameter(name = "studyId", description = "일정을 생성할 팀 ID", required = true, in = ParameterIn.PATH)
@PathVariable("studyId") Long studyId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@ public class PersonalCalendarQueryRepository {

private final JPAQueryFactory queryFactory;

public List<PersonalCalendar> findByUserIdAndDate(Long userId, LocalDate date) {
public List<PersonalCalendar> findByUserIdAndDate(Long userId, LocalDateTime start, LocalDateTime end) {
// Q타입 : 쿼리DSL 에서 사용하는 엔티티 기반 쿼리 클래스
QPersonalCalendar pc = QPersonalCalendar.personalCalendar;

// 조회 기준 : 하루의 시작과 끝 시간
LocalDateTime start = date.atStartOfDay(); // 00:00:00
LocalDateTime end = date.atTime(LocalTime.MAX); // 23:59:59.999

// 쿼리 실행:
// 해당 유저의 일정 중
// 시작 시간 <= 당일 끝시간 이고
Expand All @@ -38,7 +34,7 @@ public List<PersonalCalendar> findByUserIdAndDate(Long userId, LocalDate date) {
.where(
pc.userId.eq(userId),
pc.activated.eq(true),
calendar.scheduledStart.loe(end),
calendar.scheduledStart.lt(end),
calendar.scheduledEnd.goe(start)
)
.fetch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,10 @@ public class TeamCalendarQueryRepository {

private final JPAQueryFactory queryFactory;

public List<TeamCalendar> findByStudyIdAndDate(Long studyId, LocalDate date) {
public List<TeamCalendar> findByStudyIdAndDate(Long studyId, LocalDateTime start, LocalDateTime end) {
// 쿼리DSL용 Q타입: 팀 캘린더
QTeamCalendar tc = QTeamCalendar.teamCalendar;

LocalDateTime start = date.atStartOfDay(); // 00:00:00
LocalDateTime end = date.atTime(LocalTime.MAX); // 23:59:59.999...

// 쿼리:
// - 해당 studyId인 팀 일정 중
// - 일정 시작이 하루 끝 이전
Expand All @@ -37,7 +34,7 @@ public List<TeamCalendar> findByStudyIdAndDate(Long studyId, LocalDate date) {
.where(
tc.studyId.eq(studyId),
tc.activated.eq(true),
calendar.scheduledStart.loe(end),
calendar.scheduledStart.lt(end),
calendar.scheduledEnd.goe(start)
)
.fetch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,25 @@ public List<PersonalCalendarResponse> findAll(Long userId) {
// 특정 날짜의 일정 필터링해서 조회
public List<PersonalCalendarResponse> findByDate(Long userId, LocalDate date) {
User user = userService.get(userId);

LocalDateTime startOfDay = date.atStartOfDay(); // 00:00:00
LocalDateTime endOfDay = date.atTime(LocalTime.MAX); // 23:59:59.999999999
LocalDateTime start = date.atStartOfDay(); // 2025-07-23 00:00
LocalDateTime end = date.plusDays(1).atStartOfDay(); // 2025-07-24 00:00

return personalCalendarQueryRepository
.findByUserIdAndDate(userId, date).stream()
.findByUserIdAndDate(userId, start, end).stream()
.map(pc -> PersonalCalendarResponse.from(pc, user))
.toList();
}

// 개인 일정 생성
@Transactional
public void create(Long userId, PersonalCalendarRequest request) {
public Long create(Long userId, PersonalCalendarRequest request) {
// 필수 필드 입력 안할 시 유효성 예외 발생
validateRequiredFields(request); // 메서드로 분리

Calendar calendar = request.toCalendar();
PersonalCalendar personalCalendar = new PersonalCalendar(userId, calendar);
personalCalendarRepository.save(personalCalendar);
return calendar.getId();
}

// 개인 일정 수정
Expand Down
Loading