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 @@ -9,6 +9,7 @@
import com.back.web7_9_codecrete_be.domain.plans.dto.response.PlanDetailResponse;
import com.back.web7_9_codecrete_be.domain.plans.dto.response.PlanListResponse;
import com.back.web7_9_codecrete_be.domain.plans.dto.response.PlanResponse;
import com.back.web7_9_codecrete_be.domain.plans.dto.response.PlanShareLinkResponse;
import com.back.web7_9_codecrete_be.domain.plans.dto.response.ScheduleResponse;
import com.back.web7_9_codecrete_be.domain.plans.dto.response.ScheduleListResponse;
import com.back.web7_9_codecrete_be.domain.plans.dto.response.ScheduleDeleteResponse;
Expand Down Expand Up @@ -212,75 +213,89 @@ public RsData<Void> updateParticipantRole(
}

/**
* 계획 공유 초대
* 계획 공유 인원 추방
*
* @param planId 계획 ID
* @param participantId 참가자 ID
* @return 성공 메시지 (200 OK)
*/
@PostMapping("/invite/{planId}")
@Operation(summary = "계획 공유 초대", description = "다른 사용자에게 계획 공유를 초대합니다. 계획의 소유자 또는 편집 권한이 있는 사용자만 초대할 수 있습니다. 초대된 사용자는 수락 또는 거절할 수 있습니다.")
public RsData<Void> invitePlan(@PathVariable Long planId) {
@DeleteMapping("/kick/{planId}/{participantId}")
@Operation(summary = "계획 공유 인원 추방", description = "계획에 참여 중인 사용자를 추방합니다. 계획의 소유자만 다른 참가자를 추방할 수 있으며, 소유자 자신은 추방할 수 없습니다.")
public RsData<Void> kickParticipant(
@PathVariable Long planId,
@PathVariable Long participantId) {
User user = rq.getUser();
// TODO: 구현 필요
return RsData.success("계획 공유 초대 성공", null);
return RsData.success("참가자 추방 성공", null);
}

/**
* 계획 공유 수락
* 계획 공유 나가기
*
* @param planId 계획 ID
* @return 성공 메시지 (200 OK)
*/
@PostMapping("/accept/{planId}")
@Operation(summary = "계획 공유 수락", description = "받은 계획 공유 초대를 수락합니다. 수락 시 해당 계획에 참가자로 추가되며, 초대설정된 역할(Editor 또는 Viewer)로 참여하게 됩니다.")
public RsData<Void> acceptPlanInvite(@PathVariable Long planId) {
@DeleteMapping("/quit/{planId}")
@Operation(summary = "계획 공유 나가기", description = "공유된 계획에서 나갑니다. 계획의 소유자가 아닌 참가자만 사용할 수 있으며, 나가기해당 계획의 참가자 목록에서 제거됩니다.")
public RsData<Void> quitPlan(@PathVariable Long planId) {
User user = rq.getUser();
// TODO: 구현 필요
return RsData.success("계획 공유 수락 성공", null);
return RsData.success("계획 나가기 성공", null);
}

/**
* 계획 공유 거절
* 공유 링크 생성 (UUID 기반 13자)
*
* @param planId 계획 ID
* @return 성공 메시지 (200 OK)
* @return 공유 링크 정보 (200 OK)
*/
@PostMapping("/deny/{planId}")
@Operation(summary = "계획 공유 거절", description = "받은 계획 공유 초대를 거절합니다. 거절 시 해당 계획에 참가자로 추가되지 않으며, 초대 상태가 거절로 변경됩니다.")
public RsData<Void> denyPlanInvite(@PathVariable Long planId) {
@PostMapping("/{planId}/share/link")
@Operation(summary = "공유 링크 생성", description = "플랜 공유를 위한 UUID 기반 13자 토큰 링크를 생성합니다. 계획의 소유자 또는 편집 권한이 있는 사용자만 링크를 생성할 수 있습니다.")
public RsData<PlanShareLinkResponse> generateShareLink(@PathVariable Long planId) {
User user = rq.getUser();
// TODO: 구현 필요
return RsData.success("계획 공유 거절 성공", null);
PlanShareLinkResponse response = planService.generateShareLink(planId, user);
return RsData.success("공유 링크 생성 성공", response);
}

/**
* 계획 공유 인원 추방
* 공유 링크로 플랜 조회 (참가자 생성 없이 조회만)
*
* @param planId 계획 ID
* @param participantId 참가자 ID
* @return 성공 메시지 (200 OK)
* @param shareToken 공유 토큰 (UUID 기반 13자)
* @return 플랜 상세 정보 (200 OK)
*/
@DeleteMapping("/kick/{planId}/{participantId}")
@Operation(summary = "계획 공유 인원 추방", description = "계획에 참여 중인 사용자를 추방합니다. 계획의 소유자만 다른 참가자를 추방할 수 있으며, 소유자 자신은 추방할 수 없습니다.")
public RsData<Void> kickParticipant(
@PathVariable Long planId,
@PathVariable Long participantId) {
@GetMapping("/share/{shareToken}")
@Operation(summary = "공유 링크로 플랜 조회", description = "공유 링크 토큰을 통해 플랜을 조회합니다. 참가자 생성 없이 플랜 정보만 조회합니다.")
public RsData<PlanDetailResponse> getPlanByShareToken(@PathVariable String shareToken) {
User user = rq.getUser();
// TODO: 구현 필요
return RsData.success("참가자 추방 성공", null);
PlanDetailResponse response = planService.getPlanByShareToken(shareToken, user);
return RsData.success("플랜 조회 성공", response);
}

/**
* 계획 공유 나가기
* 공유 링크로 플랜 참가 수락
*
* @param shareToken 공유 토큰 (UUID 기반 13자)
* @return 플랜 상세 정보 (200 OK)
*/
@PostMapping("/share/{shareToken}/accept")
@Operation(summary = "공유 링크로 플랜 참가 수락", description = "공유 링크 토큰을 통해 플랜 참가를 수락합니다. 참가자가 생성되며 상태가 ACCEPTED로 설정됩니다.")
public RsData<PlanDetailResponse> acceptPlanInvitation(@PathVariable String shareToken) {
User user = rq.getUser();
PlanDetailResponse response = planService.acceptPlanInvitation(shareToken, user);
return RsData.success("플랜 참가 수락 성공", response);
}

/**
* 공유 링크 삭제
*
* @param planId 계획 ID
* @return 성공 메시지 (200 OK)
*/
@DeleteMapping("/quit/{planId}")
@Operation(summary = "계획 공유 나가기", description = "공유된 계획에서 나갑니다. 계획의 소유자가 아닌 참가자만 사용할 수 있으며, 나가기 시 해당 계획의 참가자 목록에서 제거됩니다.")
public RsData<Void> quitPlan(@PathVariable Long planId) {
@DeleteMapping("/{planId}/share/link")
@Operation(summary = "공유 링크 삭제", description = "생성된 공유 링크를 삭제합니다. 계획의 소유자 또는 편집 권한이 있는 사용자만 링크를 삭제할 수 있습니다.")
public RsData<Void> deleteShareLink(@PathVariable Long planId) {
User user = rq.getUser();
// TODO: 구현 필요
return RsData.success("계획 나가기 성공", null);
planService.deleteShareLink(planId, user);
return RsData.success("공유 링크 삭제 성공", null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.web7_9_codecrete_be.domain.plans.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@Schema(description = "플랜 공유 링크 응답 DTO")
public class PlanShareLinkResponse {

@Schema(description = "플랜 ID", example = "1")
private Long planId;

@Schema(description = "공유 토큰 (UUID 기반 13자)", example = "550e8400-e29b")
private String shareToken;

@Schema(description = "공유 링크", example = "/plans/share/550e8400-e29b")
private String shareLink;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UUID 쓰신 것은 좋아 보이는데, 너무 길어서 조금만 줄이시면 좋을 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UUID 쓰신 것은 좋아 보이는데, 너무 길어서 조금만 줄이시면 좋을 것 같습니다!

바로 반영하겠습니다!

}

Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;


@Entity
Expand Down Expand Up @@ -49,11 +51,17 @@ public class Plan {
@Column(name = "modified_date", nullable = false)
private LocalDateTime modifiedDate;

@Column(name = "share_token", unique = true, length = 13)
private String shareToken;

@Column(name = "share_token_expires_at")
private LocalDateTime shareTokenExpiresAt;

@OneToMany(mappedBy = "plan", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PlanParticipant> participants = new ArrayList<>();

@OneToMany(mappedBy = "plan", cascade = CascadeType.ALL, orphanRemoval = true)
@BatchSize(size = 20)
private List<Schedule> schedules = new ArrayList<>();

@Builder
Expand Down Expand Up @@ -83,4 +91,26 @@ public void addSchedule(Schedule schedule) {
this.schedules.add(schedule);
schedule.setPlan(this);
}

public void generateShareToken() {
this.shareToken = UUID.randomUUID().toString().substring(0, 13);
// 만료 시간: 현재 시간으로부터 1일 후
this.shareTokenExpiresAt = LocalDateTime.now().plusDays(1);
}

public void clearShareToken() {
this.shareToken = null;
this.shareTokenExpiresAt = null;
}

/**
* 공유 토큰이 만료되었는지 확인
* @return 만료되었으면 true, 아니면 false
*/
public boolean isShareTokenExpired() {
if (shareTokenExpiresAt == null) {
return true; // 만료 시간이 없으면 만료된 것으로 간주
}
return LocalDateTime.now().isAfter(shareTokenExpiresAt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,24 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface PlanParticipantRepository extends JpaRepository<PlanParticipant, Long> {

/**
* 특정 사용자와 플랜의 조합으로 참가자 존재 여부 확인
* @param userId 사용자 ID
* @param planId 플랜 ID
* @return 존재 여부
*/
boolean existsByUser_IdAndPlan_PlanId(Long userId, Long planId);

/**
* 특정 사용자와 플랜의 조합으로 참가자 조회
* @param userId 사용자 ID
* @param planId 플랜 ID
* @return PlanParticipant
*/
Optional<PlanParticipant> findByUser_IdAndPlan_PlanId(Long userId, Long planId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,19 @@ public interface PlanRepository extends JpaRepository<Plan, Long> {
/**
* - Plan 상세 조회 및 권한 체크 시
* @param id Plan ID
* @return Plan 엔티티 (concert, participants, schedules 포함)
* @EntityGraph:
* - Plan + concert + participants + schedules를 LEFT OUTER JOIN으로 한 번에 조회
* - 총 1번의 쿼리만 실행되어 N + 1 문제 방지 & 성능 향상
* @return Plan 엔티티 (concert, participants 포함)
* schedules는 @BatchSize로 배치 로드됨 (MultipleBagFetchException 방지)
*/
@EntityGraph(attributePaths = {"concert", "participants", "schedules"})
@EntityGraph(attributePaths = {"concert", "participants"})
Optional<Plan> findById(Long id);

/**
* - 특정 사용자가 참가자로 포함된 모든 Plan 조회
* @param userId 참가자로 포함된 사용자 ID
* @return 해당 사용자가 참가자인 모든 Plan 목록 (concert, participants, schedules 포함)
* @return 해당 사용자가 참가자인 모든 Plan 목록 (concert, participants 포함)
* schedules는 @BatchSize로 배치 로드됨 (MultipleBagFetchException 방지)
*/
@EntityGraph(attributePaths = {"concert", "participants", "schedules"})
@EntityGraph(attributePaths = {"concert", "participants"})
List<Plan> findDistinctByParticipants_User_Id(Long userId);

/**
Expand All @@ -38,4 +37,13 @@ public interface PlanRepository extends JpaRepository<Plan, Long> {
*/
@EntityGraph(attributePaths = {"schedules"})
List<Plan> findByUser_Id(Long userId);

/**
* - shareToken으로 Plan 조회
* @param shareToken 공유 토큰
* @return 해당 토큰을 가진 Plan 엔티티 (concert, participants 포함)
* schedules는 @BatchSize로 배치 로드됨 (MultipleBagFetchException 방지)
*/
@EntityGraph(attributePaths = {"concert", "participants"})
Optional<Plan> findByShareToken(String shareToken);
}
Loading