Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0d581f3
Merge pull request #146 from SNUH-BMI-LAB-Web-Project/hotfix/#144
wngktjd13 Nov 27, 2025
ae6f526
feat: 논문 관리 기능 구현
wngktjd13 Dec 20, 2025
2b1ca0e
feat: 저널 관리 기능 구현
wngktjd13 Dec 20, 2025
f25fdda
feat: 특허 관리 기능 구현
wngktjd13 Dec 20, 2025
1dcd5d0
feat: 저서 관리 기능 구현
wngktjd13 Dec 20, 2025
d220a2d
feat: 학회발표 관리 기능 구현
wngktjd13 Dec 20, 2025
63b8d51
feat: 수상 관리 기능 구현
wngktjd13 Dec 20, 2025
deccaeb
feat: 연구 실적 Excel 다운로드 구현
wngktjd13 Dec 20, 2025
8cd90b5
feat: 논문/특허 첨부파일 도메인 타입 추가
wngktjd13 Dec 20, 2025
ce0f8f9
feat: 공통 리소스 NOT_FOUND 에러 코드 추가
wngktjd13 Dec 20, 2025
417bc92
refactor: Task의 Patent/Publication Entity 분리
wngktjd13 Dec 20, 2025
a13b462
refactor: ResearchAuthorEntity 추상화 및 AuthorSyncService 도입
wngktjd13 Dec 20, 2025
f808a9c
Merge pull request #148 from SNUH-BMI-LAB-Web-Project/feat/#147
wngktjd13 Dec 20, 2025
a1e219d
feat: User 엔티티에 status, resignedAt 필드 추가
wngktjd13 Dec 24, 2025
9bb440d
feat: 비활성 계정 로그인 차단 처리
wngktjd13 Dec 24, 2025
c736aae
feat: 퇴사 처리 및 상태 변경 API 추가
wngktjd13 Dec 24, 2025
ec81086
Merge pull request #150 from SNUH-BMI-LAB-Web-Project/feat/#149
wngktjd13 Dec 24, 2025
2ea4efb
feat: 연구성과 목록 조회 응답 구조 개선 및 페이징 방식 변경
wngktjd13 Dec 28, 2025
fb29c1d
feat: 유저 목록 조회 status 필드 추가 및 필터링 기능 구현
wngktjd13 Dec 28, 2025
e86e688
feat: 저널 삭제 API 추가
wngktjd13 Dec 28, 2025
30dceb4
refactor: 퇴사 처리 API 제거 및 상태 변경 API로 통합
wngktjd13 Dec 28, 2025
eb13608
feat: 유저 상세 조회 응답에 status 필드 추가
wngktjd13 Dec 28, 2025
0089f15
feat: 금주 휴가자 텔레그램 알림 기능 추가
wngktjd13 Jan 6, 2026
c069379
fix: 연구성과 삭제 시 자식 테이블 삭제 누락 수정
wngktjd13 Jan 6, 2026
c268f9f
feat: 저널 issue 컬럼 추가
wngktjd13 Jan 6, 2026
fbafc62
feat: 과제 원내과제 여부(isInternal) 컬럼 추가
wngktjd13 Jan 6, 2026
a8daede
refactor: 연구성과 엔티티 테이블명 명시
wngktjd13 Jan 6, 2026
d1299f2
fix: Sentry 플러그인 저장소 설정 추가
wngktjd13 Jan 6, 2026
143bee4
fix: Sentry Gradle 플러그인 버전 수정 및 저장소 설정 추가
wngktjd13 Jan 6, 2026
50b0119
Merge remote-tracking branch 'origin/develop' into develop
wngktjd13 Jan 6, 2026
3cf9b5b
feat: 휴가 알림 소속 카테고리 표시
wngktjd13 Jan 12, 2026
55f262d
refactor: Task 임시 연구성과 엔티티 제거
wngktjd13 Jan 12, 2026
35acadf
feat: Research 엔티티 task 연동 추가
wngktjd13 Jan 12, 2026
e2109da
feat: Task 연관 연구성과 조회 API 추가
wngktjd13 Jan 12, 2026
a72f1a5
fix: 연구 상태 동적 계산으로 변경
wngktjd13 Jan 12, 2026
fb01cc3
fix: 1월 연차 초기화 버그 수정
wngktjd13 Jan 12, 2026
e464386
feat: 휴가 중복 신청 방지 로직 추가
wngktjd13 Jan 12, 2026
12eac07
feat: 관리자 승인 후 휴가 수정 API 추가
wngktjd13 Jan 12, 2026
69b76f0
Merge pull request #155 from SNUH-BMI-LAB-Web-Project/feat/#154
wngktjd13 Jan 12, 2026
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
id "io.sentry.jvm.gradle" version "5.8.0"
id "io.sentry.jvm.gradle" version "5.12.2"
}

group = 'com.bmilab.backend'
Expand Down
7 changes: 7 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}

rootProject.name = 'backend'
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ public enum FileDomainType {
TASK_PERIOD_FILES,
TASK_PERIOD_INTERIM_REPORT,
TASK_PERIOD_ANNUAL_REPORT,
PAPER_ATTACHMENT,
PATENT_ATTACHMENT,
TEMP
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bmilab.backend.domain.leave.controller;

import com.bmilab.backend.domain.leave.dto.request.AdminUpdateLeaveRequest;
import com.bmilab.backend.domain.leave.dto.request.RejectLeaveRequest;
import com.bmilab.backend.domain.leave.dto.response.LeaveFindAllResponse;
import com.bmilab.backend.domain.leave.enums.LeaveStatus;
Expand Down Expand Up @@ -72,4 +73,26 @@ ResponseEntity<LeaveFindAllResponse> getLeaves(
@RequestParam(required = false) LeaveStatus status,
@PageableDefault(size = 10, sort = "applicatedAt", direction = Sort.Direction.DESC) @ParameterObject Pageable pageable
);

@Operation(summary = "승인된 휴가 수정", description = "승인된 휴가 정보를 수정하는 PATCH API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "휴가 수정 성공"
),
@ApiResponse(
responseCode = "400",
description = "승인된 휴가만 수정할 수 있습니다."
),
@ApiResponse(
responseCode = "404",
description = "휴가 정보를 찾을 수 없습니다."
)
}
)
ResponseEntity<Void> updateLeave(
@PathVariable long leaveId,
AdminUpdateLeaveRequest request
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bmilab.backend.domain.leave.controller;

import com.bmilab.backend.domain.leave.dto.request.AdminUpdateLeaveRequest;
import com.bmilab.backend.domain.leave.dto.request.RejectLeaveRequest;
import com.bmilab.backend.domain.leave.dto.response.LeaveFindAllResponse;
import com.bmilab.backend.domain.leave.enums.LeaveStatus;
Expand All @@ -15,6 +16,7 @@
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.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down Expand Up @@ -57,4 +59,13 @@ public ResponseEntity<LeaveFindAllResponse> getLeaves(
) {
return ResponseEntity.ok(leaveService.getLeavesByAdmin(status, pageable));
}

@PatchMapping("/{leaveId}")
public ResponseEntity<Void> updateLeave(
@PathVariable long leaveId,
@RequestBody @Valid AdminUpdateLeaveRequest request
) {
leaveService.updateLeaveByAdmin(leaveId, request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.bmilab.backend.domain.leave.dto.request;

import com.bmilab.backend.domain.leave.enums.LeaveType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;

public record AdminUpdateLeaveRequest(
@Schema(description = "휴가 시작일")
@NotNull(message = "휴가 시작일은 필수입니다.")
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate startDate,

@Schema(description = "휴가 종료일")
@Nullable
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate endDate,

@Schema(description = "휴가 종류", example = "ANNUAL")
@NotNull(message = "휴가 종류는 필수입니다.")
LeaveType type,

@Schema(description = "휴가 사유", example = "가족 행사 참석")
@Nullable
String reason
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ public boolean isAnnualLeave() {
return type == LeaveType.ANNUAL;
}

public void update(LocalDate startDate, LocalDate endDate, LeaveType type, Double leaveCount, String reason) {
this.startDate = startDate;
this.endDate = endDate;
this.type = type;
this.leaveCount = leaveCount;
this.reason = reason;
}

public int countDaysInYearMonth(YearMonth yearMonth) {
if (endDate == null) {
return YearMonth.from(startDate).equals(yearMonth) ? 1 : 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,11 @@ public void increaseAnnualLeaveCount(Double leaveIncrement) {
public void updateAnnualLeaveCount(Double annualLeaveCount) {
this.annualLeaveCount = annualLeaveCount;
}

public void restoreLeave(Double leaveCount, boolean isAnnualLeave) {
if (isAnnualLeave) {
annualLeaveCount += leaveCount;
}
usedLeaveCount -= leaveCount;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.bmilab.backend.domain.leave.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum LeaveType {
ANNUAL("연차"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
public enum LeaveErrorCode implements ErrorCode {
LEAVE_NOT_FOUND("휴가 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
LEAVE_ALREADY_DONE("이미 처리된 휴가입니다.", HttpStatus.BAD_REQUEST),
LEAVE_DUPLICATE("해당 날짜에 이미 신청된 휴가가 있습니다.", HttpStatus.CONFLICT),
LEAVE_NOT_APPROVED("승인된 휴가만 수정할 수 있습니다.", HttpStatus.BAD_REQUEST),
LEAVE_COUNT_REQUIRED("연가 수가 부족합니다.", HttpStatus.BAD_REQUEST),
USER_LEAVE_NOT_FOUND("사용자의 휴가 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
HOLIDAY_API_ERROR("공휴일 API 조회 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,28 @@ public interface LeaveRepository extends JpaRepository<Leave, Long> {
"OR (l.endDate IS NOT NULL AND l.startDate <= :end AND l.endDate >= :start) )")
List<Leave> findAllByBetweenDatesAndStatus(LocalDate start, LocalDate end, LeaveStatus status);

// 금주 휴가자 알림용 - 오늘 이후 종료되는 휴가만 조회 (user fetch join)
@Query("SELECT l FROM Leave l JOIN FETCH l.user " +
"WHERE l.status = :status AND " +
"( (l.endDate IS NULL AND l.startDate BETWEEN :today AND :endOfWeek) " +
"OR (l.endDate IS NOT NULL AND l.startDate <= :endOfWeek AND l.endDate >= :today) ) " +
"ORDER BY l.startDate ASC, l.user.name ASC")
List<Leave> findWeeklyLeavesEndingAfterToday(LocalDate today, LocalDate endOfWeek, LeaveStatus status);

Page<Leave> findAllByUserId(Long userId, Pageable pageable);

List<Leave> findAllByUserId(Long userId);

Page<Leave> findAllByStatus(LeaveStatus status, Pageable pageable);

List<Leave> findAllByStatus(LeaveStatus status);

@Query("SELECT COUNT(l) > 0 FROM Leave l " +
"WHERE l.user.id = :userId " +
"AND l.status != 'REJECTED' " +
"AND ( " +
" (l.endDate IS NULL AND l.startDate BETWEEN :startDate AND :endDate) " +
" OR (l.endDate IS NOT NULL AND l.startDate <= :endDate AND l.endDate >= :startDate) " +
")")
boolean existsOverlappingLeave(Long userId, LocalDate startDate, LocalDate endDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ public class LeaveSchedulerService {
public void updateUserAnnualLeaves() {
YearMonth lastMonthWithYear = YearMonth.now().minusMonths(1);

if (lastMonthWithYear.getYear() == 2025) {
return;
}

int endOfMonth = lastMonthWithYear.lengthOfMonth();
int holidayCount = countHolidays(lastMonthWithYear);
int weekendCount = (int) countWeekends(lastMonthWithYear);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bmilab.backend.domain.leave.service;

import com.bmilab.backend.domain.leave.dto.request.AdminUpdateLeaveRequest;
import com.bmilab.backend.domain.leave.dto.request.ApplyLeaveRequest;
import com.bmilab.backend.domain.leave.dto.request.RejectLeaveRequest;
import com.bmilab.backend.domain.leave.dto.response.LeaveFindAllResponse;
Expand Down Expand Up @@ -75,9 +76,12 @@ public UserLeaveResponse getLeavesByUser(Long userId, Pageable pageable) {
public void applyLeave(Long userId, ApplyLeaveRequest request) {
User user = userService.findUserById(userId);
LeaveType type = request.type();
double leaveCount = (request.endDate() == null) ? 1 : calculateLeaveCount(request.startDate(), request.endDate());
LocalDate startDate = request.startDate();
LocalDate endDate = request.endDate() != null ? request.endDate() : startDate;
double leaveCount = (request.endDate() == null) ? 1 : calculateLeaveCount(startDate, endDate);

validateLeaveTypeAccessPermission(user, type);
validateNoDuplicateLeave(userId, startDate, endDate);

if (type.isHalf()) {
leaveCount *= 0.5;
Expand Down Expand Up @@ -136,6 +140,45 @@ public void rejectLeave(Long processorId, long leaveId, RejectLeaveRequest reque
leave.reject(request.rejectReason(), processor, LocalDateTime.now());
}

@Transactional
public void updateLeaveByAdmin(long leaveId, AdminUpdateLeaveRequest request) {
Leave leave = leaveRepository.findById(leaveId)
.orElseThrow(() -> new ApiException(LeaveErrorCode.LEAVE_NOT_FOUND));

if (!leave.isApproved()) {
throw new ApiException(LeaveErrorCode.LEAVE_NOT_APPROVED);
}

User user = leave.getUser();
UserLeave userLeave = userLeaveRepository.findByUserId(user.getId())
.orElseThrow(() -> new ApiException(LeaveErrorCode.USER_LEAVE_NOT_FOUND));

// 기존 휴가 일수 복원
userLeave.restoreLeave(leave.getLeaveCount(), leave.isAnnualLeave());

// 새로운 휴가 일수 계산
LocalDate startDate = request.startDate();
LocalDate endDate = request.endDate() != null ? request.endDate() : startDate;
LeaveType newType = request.type();
double newLeaveCount = (request.endDate() == null) ? 1 : calculateLeaveCount(startDate, endDate);

if (newType.isHalf()) {
newLeaveCount *= 0.5;
}

// 새로운 휴가 일수 차감
if (newType == LeaveType.ANNUAL && userLeave.getAnnualLeaveCount() < newLeaveCount) {
// 복원한 것을 다시 되돌림
userLeave.useLeave(leave.getLeaveCount(), leave.isAnnualLeave());
throw new ApiException(LeaveErrorCode.LEAVE_COUNT_REQUIRED);
}

userLeave.useLeave(newLeaveCount, newType == LeaveType.ANNUAL);

// 휴가 정보 업데이트
leave.update(startDate, request.endDate(), newType, newLeaveCount, request.reason());
}

public int countDatesByUserIdWithMonth(long userId, YearMonth yearMonth) {
return leaveRepository.findAllByUserId(userId)
.stream()
Expand All @@ -149,4 +192,10 @@ private void validateLeaveTypeAccessPermission(User user, LeaveType type) {
throw new ApiException(LeaveErrorCode.ACCESS_DENIED);
}
}

private void validateNoDuplicateLeave(Long userId, LocalDate startDate, LocalDate endDate) {
if (leaveRepository.existsOverlappingLeave(userId, startDate, endDate)) {
throw new ApiException(LeaveErrorCode.LEAVE_DUPLICATE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,22 @@ public class GetAllProjectsQueryResult {
private boolean isAccessible;

private boolean isPinned;

public ProjectStatus getEffectiveStatus() {
if (this.status == ProjectStatus.WAITING) {
return ProjectStatus.WAITING;
}

LocalDate today = LocalDate.now();

if (this.endDate != null && today.isAfter(this.endDate)) {
return ProjectStatus.COMPLETED;
}

if (this.startDate != null && !today.isBefore(this.startDate)) {
return ProjectStatus.IN_PROGRESS;
}

return ProjectStatus.PENDING;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public static ProjectDetail from(Project project, List<ProjectParticipant> parti
.category(ProjectCategorySummary.from(project.getCategory()))
.isPrivate(project.isPrivate())
.isAccessible(isAccessible)
.status(project.getStatus())
.status(project.getEffectiveStatus())
.irbId(project.getIrbId())
.drbId(project.getDrbId())
.piList(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static ProjectSummary from(GetAllProjectsQueryResult queryResult) {
.toList()
)
.participantCount(queryResult.getParticipantCount().intValue())
.status(queryResult.getStatus())
.status(queryResult.getEffectiveStatus())
.isPrivate(queryResult.isPrivate())
.isAccessible(queryResult.isAccessible())
.isPinned(queryResult.isPinned())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static UserProjectItem from(Project project) {
.builder()
.projectId(project.getId())
.title(project.getTitle())
.status(project.getStatus())
.status(project.getEffectiveStatus())
.category(project.getCategory() == null ? null : ProjectCategorySummary.from(project.getCategory()))
.startDate(project.getStartDate())
.endDate(project.getEndDate())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,28 @@ public void complete(LocalDate endDate) {
this.status = ProjectStatus.COMPLETED;
}

public void updateStatus(ProjectStatus status) {
this.status = status;
}

public ProjectStatus getEffectiveStatus() {
if (this.status == ProjectStatus.WAITING) {
return ProjectStatus.WAITING;
}

LocalDate today = LocalDate.now();

if (this.endDate != null && today.isAfter(this.endDate)) {
return ProjectStatus.COMPLETED;
}

if (this.startDate != null && !today.isBefore(this.startDate)) {
return ProjectStatus.IN_PROGRESS;
}

return ProjectStatus.PENDING;
}

public List<String> getPIList() {
return List.of(pi.split(","));
}
Expand Down
Loading
Loading