From 3e5d840cf37918475d09e24518284bc1f30823bb Mon Sep 17 00:00:00 2001 From: Kohseoyoung Date: Sat, 25 Jul 2026 00:41:13 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9C=A0=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=83=9D=EC=84=B1=20api=20=EA=B5=AC=ED=98=84=20(#6?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ShareLinkController.java | 35 ++++++++++ .../converter/ShareLinkConverter.java | 27 ++++++++ .../dto/request/ShareLinkRequest.java | 19 ++++++ .../dto/response/ShareLinkResponse.java | 29 ++++++++ .../slatto/domain/sharelink/entity/Guest.java | 9 +++ .../domain/sharelink/entity/ShareLink.java | 31 +++++++++ .../exception/ShareLinkErrorCode.java | 25 +++++++ .../repository/ShareLinkRepository.java | 18 +++++ .../sharelink/service/ShareLinkService.java | 68 +++++++++++++++++++ 9 files changed, 261 insertions(+) create mode 100644 src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java create mode 100644 src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java create mode 100644 src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java create mode 100644 src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java create mode 100644 src/main/java/com/slatto/domain/sharelink/exception/ShareLinkErrorCode.java create mode 100644 src/main/java/com/slatto/domain/sharelink/repository/ShareLinkRepository.java create mode 100644 src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java diff --git a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java new file mode 100644 index 00000000..dba90b30 --- /dev/null +++ b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java @@ -0,0 +1,35 @@ +package com.slatto.domain.sharelink.controller; + +import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.ShareLinkCreateReqDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; +import com.slatto.domain.sharelink.service.ShareLinkService; +import com.slatto.global.response.ApiResponse; +import com.slatto.global.response.code.CommonSuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +@Tag(name = "공유 링크 API") +public class ShareLinkController { + + private final ShareLinkService shareLinkService; + + @Operation(summary = "공유 링크 생성", description = "영상당 1개만 생성 가능하며, 이미 있으면 409를 반환합니다.") + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/videos/{videoId}/share-links") + public ApiResponse createShareLink( + @PathVariable Long videoId, + @RequestBody @Valid ShareLinkCreateReqDTO req + ) { + return ApiResponse.success( + CommonSuccessCode.CREATED, + shareLinkService.createShareLink(videoId, req) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java new file mode 100644 index 00000000..674294c5 --- /dev/null +++ b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java @@ -0,0 +1,27 @@ +package com.slatto.domain.sharelink.converter; + +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; +import com.slatto.domain.sharelink.entity.ShareLink; +import com.slatto.domain.video.entity.Video; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class ShareLinkConverter { + + public ShareLink toShareLink(Video video, LocalDateTime expiredAt) { + return ShareLink.create(video, expiredAt); + } + + public ShareLinkCreateResDTO toCreateResponse(ShareLink shareLink) { + return new ShareLinkCreateResDTO( + shareLink.getId(), + shareLink.getVideo().getId(), + shareLink.getToken(), + shareLink.getIsActive(), + shareLink.getExpiredAt(), + shareLink.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java b/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java new file mode 100644 index 00000000..d0684874 --- /dev/null +++ b/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java @@ -0,0 +1,19 @@ +package com.slatto.domain.sharelink.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public class ShareLinkRequest { + + @Schema(description = "공유 링크 생성 요청") + public record ShareLinkCreateReqDTO( + @Schema(example = "1") + @NotNull(message = "사용자 ID는 필수입니다.") + Long userId, + + @Schema(description = "만료 일시. 미지정 시 무기한", example = "2026-12-31T23:59:59", nullable = true) + LocalDateTime expiredAt + ) { } +} \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java new file mode 100644 index 00000000..8b6b0905 --- /dev/null +++ b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java @@ -0,0 +1,29 @@ +package com.slatto.domain.sharelink.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +public class ShareLinkResponse { + + @Schema(description = "공유 링크 생성 응답") + public record ShareLinkCreateResDTO( + @Schema(example = "5") + Long shareLinkId, + + @Schema(example = "10") + Long videoId, + + @Schema(example = "550e8400-e29b-41d4-a716-446655440000") + String token, + + @Schema(example = "true") + Boolean isActive, + + @Schema(example = "2026-12-31T23:59:59", nullable = true) + LocalDateTime expiredAt, + + @Schema(example = "2026-07-24T14:00:00") + LocalDateTime createdAt + ) { } +} \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/entity/Guest.java b/src/main/java/com/slatto/domain/sharelink/entity/Guest.java index 93604172..214344d3 100644 --- a/src/main/java/com/slatto/domain/sharelink/entity/Guest.java +++ b/src/main/java/com/slatto/domain/sharelink/entity/Guest.java @@ -23,4 +23,13 @@ public class Guest extends BaseEntity { @Column(name = "name", nullable = false, length = 100) private String name; + + private Guest(ShareLink shareLink, String name) { + this.shareLink = shareLink; + this.name = name; + } + + public static Guest create(ShareLink shareLink, String name) { + return new Guest(shareLink, name); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java b/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java index 83eecf8a..20040fdd 100644 --- a/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java +++ b/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.UUID; @Entity @Table(name = "share_link") @@ -15,6 +16,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ShareLink extends BaseEntity { + private static final boolean DEFAULT_ACTIVE = true; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) @@ -32,4 +35,32 @@ public class ShareLink extends BaseEntity { @Column(name = "expired_at", nullable = true) private LocalDateTime expiredAt; + + private ShareLink(Video video, String token, LocalDateTime expiredAt) { + this.video = video; + this.token = token; + this.isActive = DEFAULT_ACTIVE; + this.expiredAt = expiredAt; + } + + // 토큰은 엔티티가 직접 생성 — 서비스에서 실수로 예측 가능한 값을 넣는 걸 막음 + public static ShareLink create(Video video, LocalDateTime expiredAt) { + return new ShareLink(video, UUID.randomUUID().toString(), expiredAt); + } + + // 활성/비활성 토글 — 현재 상태를 뒤집고 결과를 반환 + public boolean toggleActive() { + this.isActive = !this.isActive; + return this.isActive; + } + + // 게스트가 실제로 들어올 수 있는 링크인지 + public boolean isUsable() { + if (!this.isActive) return false; + return this.expiredAt == null || this.expiredAt.isAfter(LocalDateTime.now()); + } + + public boolean isExpired() { + return this.expiredAt != null && !this.expiredAt.isAfter(LocalDateTime.now()); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/exception/ShareLinkErrorCode.java b/src/main/java/com/slatto/domain/sharelink/exception/ShareLinkErrorCode.java new file mode 100644 index 00000000..bcfd0bb7 --- /dev/null +++ b/src/main/java/com/slatto/domain/sharelink/exception/ShareLinkErrorCode.java @@ -0,0 +1,25 @@ +package com.slatto.domain.sharelink.exception; + +import com.slatto.global.response.code.BaseCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ShareLinkErrorCode implements BaseCode { + + SHARE_LINK_NOT_FOUND(HttpStatus.NOT_FOUND, "SHARELINK404", "공유 링크를 찾을 수 없습니다."), + SHARE_LINK_ALREADY_EXISTS(HttpStatus.CONFLICT, "SHARELINK409", "이미 이 영상의 공유 링크가 존재합니다."), + INVALID_EXPIRED_AT(HttpStatus.BAD_REQUEST, "SHARELINK400", "만료 일시는 현재 시각보다 이후여야 합니다."), + SHARE_LINK_UNAVAILABLE(HttpStatus.GONE, "SHARELINK410", "비활성화되었거나 만료된 링크입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public boolean isSuccess() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/repository/ShareLinkRepository.java b/src/main/java/com/slatto/domain/sharelink/repository/ShareLinkRepository.java new file mode 100644 index 00000000..d0b88bd2 --- /dev/null +++ b/src/main/java/com/slatto/domain/sharelink/repository/ShareLinkRepository.java @@ -0,0 +1,18 @@ +package com.slatto.domain.sharelink.repository; + +import com.slatto.domain.sharelink.entity.ShareLink; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ShareLinkRepository extends JpaRepository { + + // 진입 검증용 — 토큰으로 조회 + Optional findByToken(String token); + + // 소유자 조회용 — 영상 기준 + Optional findByVideoId(Long videoId); + + // 중복 생성 방지용 (409) + boolean existsByVideoId(Long videoId); +} \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java new file mode 100644 index 00000000..a000ace4 --- /dev/null +++ b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java @@ -0,0 +1,68 @@ +package com.slatto.domain.sharelink.service; + +import com.slatto.domain.project.repository.ProjectMemberRepository; +import com.slatto.domain.sharelink.converter.ShareLinkConverter; +import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.ShareLinkCreateReqDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; +import com.slatto.domain.sharelink.entity.ShareLink; +import com.slatto.domain.sharelink.exception.ShareLinkErrorCode; +import com.slatto.domain.sharelink.repository.ShareLinkRepository; +import com.slatto.domain.video.entity.Video; +import com.slatto.global.exception.BaseException; +import com.slatto.global.response.code.CommonErrorCode; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class ShareLinkService { + + private final ShareLinkRepository shareLinkRepository; + private final ShareLinkConverter shareLinkConverter; + private final ProjectMemberRepository projectMemberRepository; + private final ObjectProvider entityManagerProvider; + + @Transactional + public ShareLinkCreateResDTO createShareLink(Long videoId, ShareLinkCreateReqDTO req) { + + // 1. 영상 조회 + Video video = entityManagerProvider.getObject().createQuery(""" + select v from Video v where v.id = :videoId + """, Video.class) + .setParameter("videoId", videoId) + .getResultStream() + .findFirst() + .orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND)); + + // 2. 프로젝트 멤버인지 확인 (영상 → 프로젝트) + Long projectId = video.getProject().getId(); + + boolean isMember = projectMemberRepository + .existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, req.userId()); + + if (!isMember) { + throw new BaseException(CommonErrorCode.FORBIDDEN); + } + + // 3. 만료 일시가 과거면 거부 + if (req.expiredAt() != null && !req.expiredAt().isAfter(LocalDateTime.now())) { + throw new BaseException(ShareLinkErrorCode.INVALID_EXPIRED_AT); + } + + // 4. 영상당 링크는 1개만 (중복 시 409) + if (shareLinkRepository.existsByVideoId(videoId)) { + throw new BaseException(ShareLinkErrorCode.SHARE_LINK_ALREADY_EXISTS); + } + + // 5. 생성 (토큰은 엔티티가 UUID로 발급) + ShareLink shareLink = shareLinkConverter.toShareLink(video, req.expiredAt()); + ShareLink saved = shareLinkRepository.save(shareLink); + + return shareLinkConverter.toCreateResponse(saved); + } +} \ No newline at end of file From 630f21c060414aea25809b454bd2e48a44402e96 Mon Sep 17 00:00:00 2001 From: Kohseoyoung Date: Sat, 25 Jul 2026 20:41:51 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9C=A0=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=A7=84=EC=9E=85=20=EA=B2=80=EC=A6=9D=20api=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ShareLinkController.java | 14 +++++++++++++- .../sharelink/converter/ShareLinkConverter.java | 9 +++++++++ .../dto/response/ShareLinkResponse.java | 13 +++++++++++++ .../sharelink/service/ShareLinkService.java | 16 ++++++++++++++++ .../slatto/global/config/SecurityConfig.java | 17 ++++++++++------- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java index dba90b30..20e7dc67 100644 --- a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java +++ b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java @@ -2,6 +2,7 @@ import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.ShareLinkCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; import com.slatto.domain.sharelink.service.ShareLinkService; import com.slatto.global.response.ApiResponse; import com.slatto.global.response.code.CommonSuccessCode; @@ -15,7 +16,7 @@ @RestController @RequestMapping("/api/v1") @RequiredArgsConstructor -@Tag(name = "공유 링크 API") +@Tag(name = "ShareLink", description = "공유 링크 API") public class ShareLinkController { private final ShareLinkService shareLinkService; @@ -32,4 +33,15 @@ public ApiResponse createShareLink( shareLinkService.createShareLink(videoId, req) ); } + + @Operation(summary = "공유 링크 진입 검증", description = "게스트가 링크로 접근했을 때 유효성을 확인합니다. 인증이 필요 없습니다.") + @GetMapping("/share-links/{token}") + public ApiResponse getShareLinkByToken( + @PathVariable String token + ) { + return ApiResponse.success( + CommonSuccessCode.OK, + shareLinkService.getShareLinkByToken(token) + ); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java index 674294c5..c02cb76e 100644 --- a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java +++ b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java @@ -1,6 +1,7 @@ package com.slatto.domain.sharelink.converter; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; import com.slatto.domain.sharelink.entity.ShareLink; import com.slatto.domain.video.entity.Video; import org.springframework.stereotype.Component; @@ -24,4 +25,12 @@ public ShareLinkCreateResDTO toCreateResponse(ShareLink shareLink) { shareLink.getCreatedAt() ); } + + public ShareLinkEntryResDTO toEntryResponse(ShareLink shareLink) { + return new ShareLinkEntryResDTO( + shareLink.getVideo().getId(), + shareLink.getVideo().getTitle(), + true // 게스트는 항상 닉네임을 입력해야 함 + ); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java index 8b6b0905..957a93dc 100644 --- a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java +++ b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java @@ -25,5 +25,18 @@ public record ShareLinkCreateResDTO( @Schema(example = "2026-07-24T14:00:00") LocalDateTime createdAt + ) { + } + + @Schema(description = "공유 링크 진입 검증 응답") + public record ShareLinkEntryResDTO( + @Schema(example = "10") + Long videoId, + + @Schema(example = "1차 편집본") + String videoTitle, + + @Schema(example = "true", description = "게스트 닉네임 입력이 필요한지 여부") + Boolean requiresNickname ) { } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java index a000ace4..68ba40d4 100644 --- a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java +++ b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java @@ -4,6 +4,7 @@ import com.slatto.domain.sharelink.converter.ShareLinkConverter; import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.ShareLinkCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; import com.slatto.domain.sharelink.entity.ShareLink; import com.slatto.domain.sharelink.exception.ShareLinkErrorCode; import com.slatto.domain.sharelink.repository.ShareLinkRepository; @@ -65,4 +66,19 @@ public ShareLinkCreateResDTO createShareLink(Long videoId, ShareLinkCreateReqDTO return shareLinkConverter.toCreateResponse(saved); } + + @Transactional(readOnly = true) + public ShareLinkEntryResDTO getShareLinkByToken(String token) { + + // 1. 토큰으로 조회 + ShareLink shareLink = shareLinkRepository.findByToken(token) + .orElseThrow(() -> new BaseException(ShareLinkErrorCode.SHARE_LINK_NOT_FOUND)); + + // 2. 비활성화되었거나 만료되었으면 410 + if (!shareLink.isUsable()) { + throw new BaseException(ShareLinkErrorCode.SHARE_LINK_UNAVAILABLE); + } + + return shareLinkConverter.toEntryResponse(shareLink); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/global/config/SecurityConfig.java b/src/main/java/com/slatto/global/config/SecurityConfig.java index 631fb460..03d2d715 100644 --- a/src/main/java/com/slatto/global/config/SecurityConfig.java +++ b/src/main/java/com/slatto/global/config/SecurityConfig.java @@ -33,13 +33,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/v1/auth/callback/**", "/api/v1/auth/refresh" ).permitAll() - .requestMatchers( - HttpMethod.GET, - "/api/v1/health", - "/swagger-ui/**", - "/v3/api-docs/**", - "/api/v1/project-invitations/*" - ).permitAll() + // 게스트 등록 — 로그인 없이 접근 (게스트 접근 기능) + .requestMatchers(HttpMethod.POST, "/api/v1/share-links/*/guests").permitAll() + .requestMatchers( + HttpMethod.GET, + "/api/v1/health", + "/swagger-ui/**", + "/v3/api-docs/**", + "/api/v1/project-invitations/*", + "/api/v1/share-links/*" // 진입 검증 — 로그인 없이 접근 + ).permitAll() .anyRequest().authenticated() ) .exceptionHandling(handling -> handling.authenticationEntryPoint(jwtAuthenticationEntryPoint)) From 1e8368c1a8e7e82a7f18c29f03a12ebb934eb86e Mon Sep 17 00:00:00 2001 From: Kohseoyoung Date: Sat, 25 Jul 2026 21:06:36 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20api=20=EA=B5=AC=ED=98=84=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ShareLinkController.java | 15 ++++++++++++ .../converter/ShareLinkConverter.java | 15 ++++++++++++ .../dto/request/ShareLinkRequest.java | 8 +++++++ .../dto/response/ShareLinkResponse.java | 18 ++++++++++++-- .../domain/sharelink/entity/ShareLink.java | 4 ---- .../sharelink/service/ShareLinkService.java | 24 +++++++++++++++++++ 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java index 20e7dc67..7c02721a 100644 --- a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java +++ b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java @@ -3,6 +3,8 @@ import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.ShareLinkCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; +import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.GuestCreateReqDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; import com.slatto.domain.sharelink.service.ShareLinkService; import com.slatto.global.response.ApiResponse; import com.slatto.global.response.code.CommonSuccessCode; @@ -44,4 +46,17 @@ public ApiResponse getShareLinkByToken( shareLinkService.getShareLinkByToken(token) ); } + + @Operation(summary = "게스트 등록", description = "링크로 진입한 게스트가 이름을 등록하고 guestId를 발급받습니다. 인증이 필요 없습니다.") + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/share-links/{token}/guests") + public ApiResponse registerGuest( + @PathVariable String token, + @RequestBody @Valid GuestCreateReqDTO req + ) { + return ApiResponse.success( + CommonSuccessCode.CREATED, + shareLinkService.registerGuest(token, req) + ); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java index c02cb76e..2539fb23 100644 --- a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java +++ b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java @@ -2,6 +2,8 @@ import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; +import com.slatto.domain.sharelink.entity.Guest; import com.slatto.domain.sharelink.entity.ShareLink; import com.slatto.domain.video.entity.Video; import org.springframework.stereotype.Component; @@ -33,4 +35,17 @@ public ShareLinkEntryResDTO toEntryResponse(ShareLink shareLink) { true // 게스트는 항상 닉네임을 입력해야 함 ); } + + public Guest toGuest(ShareLink shareLink, String name) { + return Guest.create(shareLink, name); + } + + public GuestCreateResDTO toGuestCreateResponse(Guest guest) { + return new GuestCreateResDTO( + guest.getId(), + guest.getShareLink().getId(), + guest.getName(), + guest.getCreatedAt() + ); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java b/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java index d0684874..31a527b6 100644 --- a/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java +++ b/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java @@ -1,6 +1,7 @@ package com.slatto.domain.sharelink.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; @@ -16,4 +17,11 @@ public record ShareLinkCreateReqDTO( @Schema(description = "만료 일시. 미지정 시 무기한", example = "2026-12-31T23:59:59", nullable = true) LocalDateTime expiredAt ) { } + + @Schema(description = "게스트 등록 요청") + public record GuestCreateReqDTO( + @Schema(example = "홍길동") + @NotBlank(message = "이름은 필수입니다.") + String name + ) { } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java index 957a93dc..0725c85b 100644 --- a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java +++ b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java @@ -25,8 +25,7 @@ public record ShareLinkCreateResDTO( @Schema(example = "2026-07-24T14:00:00") LocalDateTime createdAt - ) { - } + ) { } @Schema(description = "공유 링크 진입 검증 응답") public record ShareLinkEntryResDTO( @@ -39,4 +38,19 @@ public record ShareLinkEntryResDTO( @Schema(example = "true", description = "게스트 닉네임 입력이 필요한지 여부") Boolean requiresNickname ) { } + + @Schema(description = "게스트 등록 응답") + public record GuestCreateResDTO( + @Schema(example = "20") + Long guestId, + + @Schema(example = "5") + Long shareLinkId, + + @Schema(example = "홍길동") + String name, + + @Schema(example = "2026-07-24T14:00:00") + LocalDateTime createdAt + ) { } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java b/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java index 20040fdd..4e7ef1d1 100644 --- a/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java +++ b/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java @@ -59,8 +59,4 @@ public boolean isUsable() { if (!this.isActive) return false; return this.expiredAt == null || this.expiredAt.isAfter(LocalDateTime.now()); } - - public boolean isExpired() { - return this.expiredAt != null && !this.expiredAt.isAfter(LocalDateTime.now()); - } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java index 68ba40d4..8f1a0427 100644 --- a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java +++ b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java @@ -5,6 +5,10 @@ import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.ShareLinkCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; +import com.slatto.domain.sharelink.repository.GuestRepository; +import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.GuestCreateReqDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; +import com.slatto.domain.sharelink.entity.Guest; import com.slatto.domain.sharelink.entity.ShareLink; import com.slatto.domain.sharelink.exception.ShareLinkErrorCode; import com.slatto.domain.sharelink.repository.ShareLinkRepository; @@ -27,6 +31,7 @@ public class ShareLinkService { private final ShareLinkConverter shareLinkConverter; private final ProjectMemberRepository projectMemberRepository; private final ObjectProvider entityManagerProvider; + private final GuestRepository guestRepository; @Transactional public ShareLinkCreateResDTO createShareLink(Long videoId, ShareLinkCreateReqDTO req) { @@ -81,4 +86,23 @@ public ShareLinkEntryResDTO getShareLinkByToken(String token) { return shareLinkConverter.toEntryResponse(shareLink); } + + @Transactional + public GuestCreateResDTO registerGuest(String token, GuestCreateReqDTO req) { + + // 1. 토큰으로 링크 조회 + ShareLink shareLink = shareLinkRepository.findByToken(token) + .orElseThrow(() -> new BaseException(ShareLinkErrorCode.SHARE_LINK_NOT_FOUND)); + + // 2. 사용 가능한 링크인지 (비활성/만료면 410) + if (!shareLink.isUsable()) { + throw new BaseException(ShareLinkErrorCode.SHARE_LINK_UNAVAILABLE); + } + + // 3. 게스트 생성 + Guest guest = shareLinkConverter.toGuest(shareLink, req.name()); + Guest saved = guestRepository.save(guest); + + return shareLinkConverter.toGuestCreateResponse(saved); + } } \ No newline at end of file From 8719ba6ce23ecf1240de6927085dd9ecd960a38d Mon Sep 17 00:00:00 2001 From: Kohseoyoung Date: Sat, 25 Jul 2026 21:15:58 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9C=A0=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=86=8C=EC=9C=A0=EC=9E=90=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EA=B5=AC=ED=98=84=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ShareLinkController.java | 15 ++++++++++ .../converter/ShareLinkConverter.java | 12 ++++++++ .../dto/response/ShareLinkResponse.java | 21 +++++++++++++ .../sharelink/service/ShareLinkService.java | 30 +++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java index 7c02721a..86e2de22 100644 --- a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java +++ b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java @@ -5,6 +5,8 @@ import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.GuestCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkInfoResDTO; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import com.slatto.domain.sharelink.service.ShareLinkService; import com.slatto.global.response.ApiResponse; import com.slatto.global.response.code.CommonSuccessCode; @@ -59,4 +61,17 @@ public ApiResponse registerGuest( shareLinkService.registerGuest(token, req) ); } + + @Operation(summary = "공유 링크 조회 (소유자용)", description = "영상의 공유 링크를 조회합니다. 프로젝트 멤버만 가능합니다.") + @GetMapping("/videos/{videoId}/share-links") + public ApiResponse< + ShareLinkInfoResDTO> getShareLinkByVideo( + @PathVariable Long videoId, + @AuthenticationPrincipal Long userId + ) { + return ApiResponse.success( + CommonSuccessCode.OK, + shareLinkService.getShareLinkByVideo(videoId, userId) + ); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java index 2539fb23..21a99bce 100644 --- a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java +++ b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java @@ -3,6 +3,7 @@ import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkInfoResDTO; import com.slatto.domain.sharelink.entity.Guest; import com.slatto.domain.sharelink.entity.ShareLink; import com.slatto.domain.video.entity.Video; @@ -48,4 +49,15 @@ public GuestCreateResDTO toGuestCreateResponse(Guest guest) { guest.getCreatedAt() ); } + + public ShareLinkInfoResDTO toInfoResponse(ShareLink shareLink) { + return new ShareLinkInfoResDTO( + shareLink.getId(), + shareLink.getVideo().getId(), + shareLink.getToken(), + shareLink.getIsActive(), + shareLink.getExpiredAt(), + shareLink.getCreatedAt() + ); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java index 0725c85b..dcaf4b90 100644 --- a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java +++ b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java @@ -53,4 +53,25 @@ public record GuestCreateResDTO( @Schema(example = "2026-07-24T14:00:00") LocalDateTime createdAt ) { } + + @Schema(description = "공유 링크 조회 응답 (소유자용)") + public record ShareLinkInfoResDTO( + @Schema(example = "5") + Long shareLinkId, + + @Schema(example = "10") + Long videoId, + + @Schema(example = "550e8400-e29b-41d4-a716-446655440000") + String token, + + @Schema(example = "true") + Boolean isActive, + + @Schema(example = "2026-12-31T23:59:59", nullable = true) + LocalDateTime expiredAt, + + @Schema(example = "2026-07-24T14:00:00") + LocalDateTime createdAt + ) { } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java index 8f1a0427..fecc03e2 100644 --- a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java +++ b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java @@ -5,6 +5,7 @@ import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.ShareLinkCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkInfoResDTO; import com.slatto.domain.sharelink.repository.GuestRepository; import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.GuestCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; @@ -105,4 +106,33 @@ public GuestCreateResDTO registerGuest(String token, GuestCreateReqDTO req) { return shareLinkConverter.toGuestCreateResponse(saved); } + + @Transactional(readOnly = true) + public ShareLinkInfoResDTO getShareLinkByVideo(Long videoId, Long userId) { + + // 1. 영상 조회 + Video video = entityManagerProvider.getObject().createQuery(""" + select v from Video v where v.id = :videoId + """, Video.class) + .setParameter("videoId", videoId) + .getResultStream() + .findFirst() + .orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND)); + + // 2. 프로젝트 멤버인지 확인 + Long projectId = video.getProject().getId(); + + boolean isMember = projectMemberRepository + .existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, userId); + + if (!isMember) { + throw new BaseException(CommonErrorCode.FORBIDDEN); + } + + // 3. 링크 조회 (없으면 404) + ShareLink shareLink = shareLinkRepository.findByVideoId(videoId) + .orElseThrow(() -> new BaseException(ShareLinkErrorCode.SHARE_LINK_NOT_FOUND)); + + return shareLinkConverter.toInfoResponse(shareLink); + } } \ No newline at end of file From 283b7e9e475d68788711f1e521a488bf864a0f51 Mon Sep 17 00:00:00 2001 From: Kohseoyoung Date: Sat, 25 Jul 2026 21:24:20 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9C=A0=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=ED=99=9C=EC=84=B1/=EB=B9=84=ED=99=9C=EC=84=B1=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20api=20=EA=B5=AC=ED=98=84=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ShareLinkController.java | 14 +++++++++++ .../converter/ShareLinkConverter.java | 8 +++++++ .../dto/response/ShareLinkResponse.java | 9 +++++++ .../sharelink/service/ShareLinkService.java | 24 +++++++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java index 86e2de22..7693b56b 100644 --- a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java +++ b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java @@ -6,6 +6,7 @@ import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.GuestCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkInfoResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkToggleResDTO; import org.springframework.security.core.annotation.AuthenticationPrincipal; import com.slatto.domain.sharelink.service.ShareLinkService; import com.slatto.global.response.ApiResponse; @@ -74,4 +75,17 @@ ShareLinkInfoResDTO> getShareLinkByVideo( shareLinkService.getShareLinkByVideo(videoId, userId) ); } + + @Operation(summary = "공유 링크 활성/비활성 토글", description = "공유 링크의 활성 상태를 뒤집습니다. 프로젝트 멤버만 가능합니다.") + @PatchMapping("/share-links/{shareLinkId}") + public ApiResponse toggleShareLink( + @PathVariable Long shareLinkId, + @AuthenticationPrincipal Long userId + ) { + return ApiResponse.success( + CommonSuccessCode.OK, + shareLinkService.toggleShareLink(shareLinkId, userId) + ); + } + } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java index 21a99bce..1163b33d 100644 --- a/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java +++ b/src/main/java/com/slatto/domain/sharelink/converter/ShareLinkConverter.java @@ -4,6 +4,7 @@ import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkInfoResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkToggleResDTO; import com.slatto.domain.sharelink.entity.Guest; import com.slatto.domain.sharelink.entity.ShareLink; import com.slatto.domain.video.entity.Video; @@ -60,4 +61,11 @@ public ShareLinkInfoResDTO toInfoResponse(ShareLink shareLink) { shareLink.getCreatedAt() ); } + + public ShareLinkToggleResDTO toToggleResponse(ShareLink shareLink) { + return new ShareLinkToggleResDTO( + shareLink.getId(), + shareLink.getIsActive() + ); + } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java index dcaf4b90..58bb0421 100644 --- a/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java +++ b/src/main/java/com/slatto/domain/sharelink/dto/response/ShareLinkResponse.java @@ -74,4 +74,13 @@ public record ShareLinkInfoResDTO( @Schema(example = "2026-07-24T14:00:00") LocalDateTime createdAt ) { } + + @Schema(description = "공유 링크 활성/비활성 토글 응답") + public record ShareLinkToggleResDTO( + @Schema(example = "5") + Long shareLinkId, + + @Schema(example = "false", description = "토글 후의 활성 상태") + Boolean isActive + ) { } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java index fecc03e2..b2aeffca 100644 --- a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java +++ b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java @@ -6,6 +6,7 @@ import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkCreateResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkEntryResDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkInfoResDTO; +import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.ShareLinkToggleResDTO; import com.slatto.domain.sharelink.repository.GuestRepository; import com.slatto.domain.sharelink.dto.request.ShareLinkRequest.GuestCreateReqDTO; import com.slatto.domain.sharelink.dto.response.ShareLinkResponse.GuestCreateResDTO; @@ -135,4 +136,27 @@ public ShareLinkInfoResDTO getShareLinkByVideo(Long videoId, Long userId) { return shareLinkConverter.toInfoResponse(shareLink); } + + @Transactional + public ShareLinkToggleResDTO toggleShareLink(Long shareLinkId, Long userId) { + + // 1. 링크 조회 + ShareLink shareLink = shareLinkRepository.findById(shareLinkId) + .orElseThrow(() -> new BaseException(ShareLinkErrorCode.SHARE_LINK_NOT_FOUND)); + + // 2. 프로젝트 멤버인지 확인 (링크 → 영상 → 프로젝트) + Long projectId = shareLink.getVideo().getProject().getId(); + + boolean isMember = projectMemberRepository + .existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, userId); + + if (!isMember) { + throw new BaseException(CommonErrorCode.FORBIDDEN); + } + + // 3. 상태 토글 (더티 체킹) + shareLink.toggleActive(); + + return shareLinkConverter.toToggleResponse(shareLink); + } } \ No newline at end of file From 5f583df41e7331746db481c9f2204c6011a63b1b Mon Sep 17 00:00:00 2001 From: Kohseoyoung Date: Sat, 25 Jul 2026 21:45:57 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=EA=B3=B5=EC=9C=A0=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=83=9D=EC=84=B1=20=EC=9E=91=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EB=B3=B4=EA=B0=95=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/sharelink/controller/ShareLinkController.java | 3 ++- .../domain/sharelink/dto/request/ShareLinkRequest.java | 6 ++---- .../java/com/slatto/domain/sharelink/entity/ShareLink.java | 2 +- .../slatto/domain/sharelink/service/ShareLinkService.java | 5 ++--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java index 7693b56b..b31df128 100644 --- a/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java +++ b/src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java @@ -31,11 +31,12 @@ public class ShareLinkController { @PostMapping("/videos/{videoId}/share-links") public ApiResponse createShareLink( @PathVariable Long videoId, + @AuthenticationPrincipal Long userId, @RequestBody @Valid ShareLinkCreateReqDTO req ) { return ApiResponse.success( CommonSuccessCode.CREATED, - shareLinkService.createShareLink(videoId, req) + shareLinkService.createShareLink(videoId, userId, req) ); } diff --git a/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java b/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java index 31a527b6..c9e3f38b 100644 --- a/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java +++ b/src/main/java/com/slatto/domain/sharelink/dto/request/ShareLinkRequest.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.time.LocalDateTime; @@ -10,10 +11,6 @@ public class ShareLinkRequest { @Schema(description = "공유 링크 생성 요청") public record ShareLinkCreateReqDTO( - @Schema(example = "1") - @NotNull(message = "사용자 ID는 필수입니다.") - Long userId, - @Schema(description = "만료 일시. 미지정 시 무기한", example = "2026-12-31T23:59:59", nullable = true) LocalDateTime expiredAt ) { } @@ -22,6 +19,7 @@ public record ShareLinkCreateReqDTO( public record GuestCreateReqDTO( @Schema(example = "홍길동") @NotBlank(message = "이름은 필수입니다.") + @Size(max = 100, message = "이름은 100자 이하여야 합니다.") String name ) { } } \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java b/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java index 4e7ef1d1..c7a3cb78 100644 --- a/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java +++ b/src/main/java/com/slatto/domain/sharelink/entity/ShareLink.java @@ -24,7 +24,7 @@ public class ShareLink extends BaseEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "video_id", nullable = false) + @JoinColumn(name = "video_id", nullable = false, unique = true) private Video video; @Column(name = "token", nullable = false, length = 255, unique = true) diff --git a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java index b2aeffca..881c088a 100644 --- a/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java +++ b/src/main/java/com/slatto/domain/sharelink/service/ShareLinkService.java @@ -36,7 +36,7 @@ public class ShareLinkService { private final GuestRepository guestRepository; @Transactional - public ShareLinkCreateResDTO createShareLink(Long videoId, ShareLinkCreateReqDTO req) { + public ShareLinkCreateResDTO createShareLink(Long videoId, Long userId, ShareLinkCreateReqDTO req) { // 1. 영상 조회 Video video = entityManagerProvider.getObject().createQuery(""" @@ -49,9 +49,8 @@ public ShareLinkCreateResDTO createShareLink(Long videoId, ShareLinkCreateReqDTO // 2. 프로젝트 멤버인지 확인 (영상 → 프로젝트) Long projectId = video.getProject().getId(); - boolean isMember = projectMemberRepository - .existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, req.userId()); + .existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, userId); if (!isMember) { throw new BaseException(CommonErrorCode.FORBIDDEN);