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
Expand Up @@ -12,6 +12,9 @@
import com.ssafy.dash.notification.application.NotificationService;
import com.ssafy.dash.notification.domain.NotificationType;
import com.ssafy.dash.study.domain.Study.StudyType;
import com.ssafy.dash.common.exception.BusinessException;
import com.ssafy.dash.common.exception.ErrorCode;
import com.ssafy.dash.user.domain.exception.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -101,7 +104,7 @@ public List<Study> searchByKeyword(String keyword) {
@Transactional
public Study createStudy(Long userId, String name, String description, StudyVisibility visibility) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
.orElseThrow(() -> new UserNotFoundException(userId));

// 스터디 이동(Transition): 이미 다른 Group 스터디에 있다면, 탈퇴 또는 삭제 처리
if (user.getStudyId() != null) {
Expand Down Expand Up @@ -151,7 +154,7 @@ public Study createStudy(Long userId, String name, String description, StudyVisi
@Transactional
public Study createPersonalStudy(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
.orElseThrow(() -> new UserNotFoundException(userId));

if (user.getStudyId() != null) {
return studyRepository.findById(user.getStudyId()).orElse(null);
Expand All @@ -172,13 +175,13 @@ public Study createPersonalStudy(Long userId) {
@Transactional
public void applyForStudy(Long userId, Long studyId, String message) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
.orElseThrow(() -> new UserNotFoundException(userId));

// 스터디 이동 로직은 승인 시점에 처리됩니다. (applyForStudy에서는 체크하지 않음)

// 스터디 존재 여부 확인
if (studyRepository.findById(studyId).isEmpty()) {
throw new IllegalArgumentException("Study not found");
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND);
}

// 이미 가입 신청했는지 확인
Expand Down Expand Up @@ -206,10 +209,10 @@ public void applyForStudy(Long userId, Long studyId, String message) {
@Transactional(readOnly = true)
public List<StudyApplication> getPendingApplications(Long userId, Long studyId) {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

if (!Objects.equals(study.getCreatorId(), userId)) {
throw new SecurityException("Only creator can view applications");
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}

return studyRepository.findPendingApplicationsByStudyId(studyId);
Expand All @@ -223,10 +226,10 @@ public java.util.Optional<StudyApplication> getMyPendingApplication(Long userId)
@Transactional
public void cancelApplication(Long userId, Long applicationId) {
StudyApplication application = studyRepository.findApplicationById(applicationId)
.orElseThrow(() -> new IllegalArgumentException("Application not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

if (!Objects.equals(application.getUserId(), userId)) {
throw new SecurityException("Cannot cancel other's application");
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}

application.reject();
Expand All @@ -236,18 +239,18 @@ public void cancelApplication(Long userId, Long applicationId) {
@Transactional
public void approveApplication(Long adminId, Long applicationId) {
StudyApplication application = studyRepository.findApplicationById(applicationId)
.orElseThrow(() -> new IllegalArgumentException("Application not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

Study study = studyRepository.findById(application.getStudyId())
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

if (!Objects.equals(study.getCreatorId(), adminId)) {
throw new SecurityException("Only creator can approve applications");
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}

// 유저를 스터디에 추가
User user = userRepository.findById(application.getUserId())
.orElseThrow(() -> new IllegalArgumentException("User not found"));
.orElseThrow(() -> new UserNotFoundException(application.getUserId()));

// 탈퇴한 유저인지 확인
if (user.isDeleted()) {
Expand Down Expand Up @@ -314,13 +317,13 @@ public void approveApplication(Long adminId, Long applicationId) {
@Transactional
public void rejectApplication(Long leaderId, Long applicationId, String reason) {
StudyApplication application = studyRepository.findApplicationById(applicationId)
.orElseThrow(() -> new IllegalArgumentException("Application not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

Study study = studyRepository.findById(application.getStudyId())
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

if (!Objects.equals(study.getCreatorId(), leaderId)) {
throw new SecurityException("Only creator can reject applications");
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}

// DB에서 삭제하여 재가입 가능하도록 함
Expand All @@ -342,15 +345,15 @@ public void rejectApplication(Long leaderId, Long applicationId, String reason)
@Transactional
public void leaveStudy(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
.orElseThrow(() -> new UserNotFoundException(userId));

if (user.getStudyId() == null) {
throw new IllegalStateException("User is not in a study");
}

Long oldStudyId = user.getStudyId();
Study study = studyRepository.findById(oldStudyId)
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

if (Objects.equals(study.getCreatorId(), userId)) {
// 정책: 스터디장은 탈퇴할 수 없음. 스터디를 해체하거나 권한을 위임해야 함. (아직 구현되지 않음)
Expand Down Expand Up @@ -382,18 +385,41 @@ public java.util.Optional<Study> findStudyById(Long studyId) {
return studyRepository.findById(studyId);
}

@Transactional
public void updateStudy(Long userId, Long studyId, String name, String description) {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));

boolean isAdmin = "ROLE_ADMIN".equals(user.getRole());
if (!java.util.Objects.equals(study.getCreatorId(), userId) && !isAdmin) {
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}
Comment thread
SoMin-Yoo marked this conversation as resolved.

if (name != null && !name.isBlank()) {
study.setName(name);
}
if (description != null) {
study.setDescription(description);
}

studyRepository.update(study);
}

@Transactional
public void deleteStudy(Long userId, Long studyId) {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
.orElseThrow(() -> new UserNotFoundException(userId));

// 스터디장 또는 관리자만 삭제 가능
boolean isAdmin = "ROLE_ADMIN".equals(user.getRole());
if (!Objects.equals(study.getCreatorId(), userId) && !isAdmin) {
throw new SecurityException("Only creator or admin can delete the study");
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}

// 1. 모든 멤버를 각자의 Personal 스터디로 쫓아냄
Expand All @@ -420,7 +446,7 @@ public void deleteStudy(Long userId, Long studyId) {
@Transactional(readOnly = true)
public List<UserResponse> getStudyMembers(Long studyId) {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

return userRepository.findByStudyId(studyId).stream()
.map(user -> UserResult.from(user, null, study))
Expand All @@ -439,17 +465,21 @@ public List<User> getStudyMembersRaw(Long studyId) {
@Transactional
public void delegateLeader(Long currentLeaderId, Long studyId, Long newLeaderId) {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

if (!Objects.equals(study.getCreatorId(), currentLeaderId)) {
throw new SecurityException("Only creator can delegate the leader role");
User user = userRepository.findById(currentLeaderId)
.orElseThrow(() -> new UserNotFoundException(currentLeaderId));

boolean isAdmin = "ROLE_ADMIN".equals(user.getRole());
if (!Objects.equals(study.getCreatorId(), currentLeaderId) && !isAdmin) {
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}

User newLeader = userRepository.findById(newLeaderId)
.orElseThrow(() -> new IllegalArgumentException("New leader not found"));
.orElseThrow(() -> new UserNotFoundException(newLeaderId));

if (!Objects.equals(newLeader.getStudyId(), studyId)) {
throw new IllegalArgumentException("New leader must be a member of the study");
throw new BusinessException(ErrorCode.VALIDATION_FAILED);
}

study.setCreatorId(newLeaderId);
Expand All @@ -466,13 +496,13 @@ public void delegateLeader(Long currentLeaderId, Long studyId, Long newLeaderId)
@Transactional(readOnly = true)
public StudyApplication getApplicationDetail(Long userId, Long applicationId) {
StudyApplication app = studyRepository.findApplicationById(applicationId)
.orElseThrow(() -> new IllegalArgumentException("Application not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

Study study = studyRepository.findById(app.getStudyId())
.orElseThrow(() -> new IllegalArgumentException("Study not found"));
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

if (!Objects.equals(study.getCreatorId(), userId)) {
throw new SecurityException("Only creator can view application details");
throw new BusinessException(ErrorCode.UNAUTHORIZED_ACCESS);
}

return app;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.ssafy.dash.study.application.StudyService;
import com.ssafy.dash.study.domain.Study;
import com.ssafy.dash.study.presentation.dto.CreateStudyRequest;
import com.ssafy.dash.study.presentation.dto.UpdateStudyRequest;
import com.ssafy.dash.study.presentation.dto.request.ApplyStudyRequest;
import com.ssafy.dash.study.presentation.dto.request.CreateMissionRequest;
import com.ssafy.dash.study.presentation.dto.request.AddMissionProblemsRequest;
Expand Down Expand Up @@ -101,6 +102,19 @@ public ResponseEntity<CreateStudyResponse> createPersonalStudy(
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

@Operation(summary = "스터디 정보 수정", description = "스터디의 이름이나 소개를 수정합니다.")
@PatchMapping("/{studyId}")
public ResponseEntity<Void> updateStudy(
@Parameter(hidden = true) @AuthenticationPrincipal OAuth2User principal,
@PathVariable Long studyId,
@RequestBody UpdateStudyRequest request) {
if (principal instanceof CustomOAuth2User customUser) {
studyService.updateStudy(customUser.getUserId(), studyId, request.getName(), request.getDescription());
return ResponseEntity.ok().build();
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

@Operation(summary = "스터디 가입 신청", description = "스터디에 가입 신청을 보냅니다.")
@PostMapping("/{studyId}/apply")
public ResponseEntity<Void> applyForStudy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ssafy.dash.study.presentation.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateStudyRequest {
private String name;
private String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public record StudyListResponse(
LocalDate streakUpdatedAt, // streak 유효성 판단용
Double averageSubmissionRate,
List<MemberPreview> memberPreviews, // 멤버 미리보기 (프론트에서 표시 개수 조절)
String description) {
String description,
Long creatorId) {

public static StudyListResponse from(Study study, List<User> members) {
List<MemberPreview> previews = members != null
Expand All @@ -44,7 +45,8 @@ public static StudyListResponse from(Study study, List<User> members) {
study.getStreakUpdatedAt(),
study.getAverageSubmissionRate() != null ? study.getAverageSubmissionRate() : 0.0,
previews,
study.getDescription());
study.getDescription(),
study.getCreatorId());
}

private static String getTierBadge(Double tier) {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/api/study.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const studyApi = {
get(studyId) {
return http.get(`/studies/${studyId}`);
},
// Update study details
update(studyId, data) {
return http.patch(`/studies/${studyId}`, data);
},
// Get acorn logs
getAcornLogs(studyId) {
return http.get(`/studies/${studyId}/acorns`);
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ const routes = [
component: () => import("../views/study/StudyListView.vue"),
meta: { requiresAuth: true }
},
{
path: "/study/manage",
name: "StudyManage",
component: () => import("../views/study/StudyManageView.vue"),
meta: { requiresAuth: true }
},
{
path: "/admin/study/:id/dashboard",
name: "AdminStudyDashboard",
Expand Down
Loading