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,6 +1,7 @@
package com.sniptext.domain.template.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -11,6 +12,7 @@

import com.sniptext.domain.template.controller.dto.TemplateCreateRequest;
import com.sniptext.domain.template.controller.dto.TemplateCreateResponse;
import com.sniptext.domain.template.controller.dto.TemplateDeleteResponse;
import com.sniptext.domain.template.controller.dto.TemplateDetailResponse;
import com.sniptext.domain.template.controller.dto.TemplateListResponse;
import com.sniptext.domain.template.controller.dto.TemplateUpdateRequest;
Expand All @@ -19,8 +21,10 @@
import com.sniptext.domain.template.controller.dto.TemplateUsageResponse;
import com.sniptext.domain.template.service.TemplateCommandService;
import com.sniptext.domain.template.service.TemplateQueryService;
import com.sniptext.domain.template.service.dto.command.TemplateDeleteCommand;
import com.sniptext.domain.template.service.dto.query.TemplateDetailQuery;
import com.sniptext.domain.template.service.dto.result.TemplateCreateResult;
import com.sniptext.domain.template.service.dto.result.TemplateDeleteResult;
import com.sniptext.domain.template.service.dto.result.TemplateDetailResult;
import com.sniptext.domain.template.service.dto.result.TemplateListResult;
import com.sniptext.domain.template.service.dto.result.TemplateUpdateResult;
Expand Down Expand Up @@ -80,6 +84,16 @@ public ResponseEntity<ApiResponse<TemplateUpdateResponse>> updateTemplate(
return ApiResponse.success(TemplateSuccessCode.TEMPLATE_UPDATED, TemplateUpdateResponse.from(result));
}

@DeleteMapping("/{templateId}")
public ResponseEntity<ApiResponse<TemplateDeleteResponse>> deleteTemplate(
@LoginUser Long userId,
@PathVariable Long templateId) {

TemplateDeleteResult result = templateCommandService.delete(new TemplateDeleteCommand(userId, templateId));

return ApiResponse.success(TemplateSuccessCode.TEMPLATE_DELETED, TemplateDeleteResponse.from(result));
}

@PostMapping("/{templateId}/usages")
public ResponseEntity<ApiResponse<TemplateUsageResponse>> recordUsage(
@LoginUser Long userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum TemplateSuccessCode implements ResultCode {
TEMPLATE_LIST_FOUND(HttpStatus.OK, "TEMPLATE200_1", "템플릿 목록 조회에 성공했습니다."),
TEMPLATE_FOUND(HttpStatus.OK, "TEMPLATE200_2", "템플릿 조회에 성공했습니다."),
TEMPLATE_UPDATED(HttpStatus.OK, "TEMPLATE200_3", "수정 내용이 저장되었습니다."),
TEMPLATE_DELETED(HttpStatus.OK, "TEMPLATE200_4", "템플릿을 휴지통으로 옮겼습니다."),
TEMPLATE_USAGE_RECORDED(HttpStatus.OK, "TEMPLATE200_6", "사용 기록이 저장되었습니다."),
TEMPLATE_CREATED(HttpStatus.CREATED, "TEMPLATE201_1", "템플릿이 저장되었습니다.");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.sniptext.domain.template.controller.dto;

import java.time.LocalDateTime;

import com.sniptext.domain.template.service.dto.result.TemplateDeleteResult;

/** 템플릿 삭제(휴지통 이동) 결과 (명세 12번). 시각은 전역 직렬화 설정이 +09:00을 붙여 내보낸다. */
public record TemplateDeleteResponse(
Long templateId,
String title,
LocalDateTime deletedAt,
LocalDateTime purgeAt
) {

public static TemplateDeleteResponse from(TemplateDeleteResult result) {
return new TemplateDeleteResponse(result.templateId(), result.title(), result.deletedAt(), result.purgeAt());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,15 @@ public void markUsed(LocalDateTime now) {
this.lastUsedAt = now;
}

/**
* 휴지통으로 옮긴다. 수정·사용 시각은 그대로 둔다.
* 이미 휴지통인 템플릿에 부르면 휴지통 시각을 덮어써 30일 기한이 다시 시작되므로 막는다 (서비스가 먼저 404로 막으니 코드 실수다).
*/
public void moveToTrash(LocalDateTime now) {
require(now != null, "휴지통 이동 시각이 없다");
if (isInTrash()) {
throw new IllegalStateException("이미 휴지통에 있는 템플릿은 다시 옮길 수 없다");
}
this.deletedAt = now;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public enum TemplateErrorCode implements ResultCode {
USAGE_CHANNEL_UNSUPPORTED(HttpStatus.BAD_REQUEST, "TEMPLATE400_9", "지원하지 않는 사용 채널이에요."),
/** 활성 템플릿 수가 플랜 한도 이상 (화면은 한도 모달) */
TEMPLATE_LIMIT_REACHED(HttpStatus.FORBIDDEN, "TEMPLATE403_1", "템플릿 저장 한도에 도달했어요."),
/** 없는 템플릿, 다른 회원의 템플릿 (다른 회원의 휴지통 템플릿 포함) */
/** 없는 템플릿, 다른 회원의 템플릿 (다른 회원의 휴지통 템플릿 포함). 삭제(12번)는 내 휴지통 템플릿도 이 코드다 */
TEMPLATE_NOT_FOUND(HttpStatus.NOT_FOUND, "TEMPLATE404_1", "템플릿을 찾을 수 없어요."),
/** 내 템플릿인데 휴지통에 있음 */
TEMPLATE_IN_TRASH(HttpStatus.NOT_FOUND, "TEMPLATE404_2", "이미 삭제된 템플릿이에요."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
import com.sniptext.domain.template.repository.TemplateRepository;
import com.sniptext.domain.template.service.dto.command.TemplateCreateCommand;
import com.sniptext.domain.template.service.dto.command.TemplateCreateFromGalleryCommand;
import com.sniptext.domain.template.service.dto.command.TemplateDeleteCommand;
import com.sniptext.domain.template.service.dto.command.TemplateUpdateCommand;
import com.sniptext.domain.template.service.dto.command.TemplateUsageCommand;
import com.sniptext.domain.template.service.dto.result.TemplateCreateResult;
import com.sniptext.domain.template.service.dto.result.TemplateDeleteResult;
import com.sniptext.domain.template.service.dto.result.TemplateShortcutAssignResult;
import com.sniptext.domain.template.service.dto.result.TemplateUpdateResult;
import com.sniptext.domain.template.service.dto.result.TemplateUsageResult;
Expand Down Expand Up @@ -135,6 +137,28 @@ public TemplateUsageResult recordUsage(TemplateUsageCommand command) {
return TemplateUsageResult.from(template);
}

/**
* 템플릿을 휴지통으로 옮긴다 (소프트 삭제). deleted_at만 Clock 시각으로 바꾸고 수정·사용 시각은 그대로 둔다.
* 없거나 다른 회원 것이면 TEMPLATE404_1이고, 수정·사용 기록과 달리 내 휴지통 템플릿도 TEMPLATE404_1이다 (명세 12번).
* 개수를 줄이기만 해서 한도 검사는 없지만 회원 행을 먼저 잠근다. 잠그지 않고 일반 조회로 읽으면, 같은 템플릿을 동시에 지울 때
* 뒤 요청이 커밋 전 스냅샷에서 활성 템플릿을 보고 행 잠금만 기다렸다가 deleted_at을 덮어써 둘 다 200이 되고 30일 기한이 다시 시작된다.
* 복구·영구 삭제·비우기와 같은 순서(회원 행 → 템플릿 행)로 잠가 서로 줄을 서게 한다. 잠금 뒤의 첫 일반 조회라 앞 요청의 커밋을 본다.
* 변경 감지로 쓴다. @DynamicUpdate라 UPDATE에는 deleted_at과 감사 기능이 채우는 updated_at만 들어간다.
* 엔티티를 읽으므로 본문 복호화에 실패하는 행이면 500이다.
*/
@Transactional
public TemplateDeleteResult delete(TemplateDeleteCommand command) {
userCommandService.lockUser(command.userId());

Template template = templateRepository.findByIdAndUserId(command.templateId(), command.userId())
.filter(found -> !found.isInTrash())
.orElseThrow(() -> new BusinessException(TemplateErrorCode.TEMPLATE_NOT_FOUND));

template.moveToTrash(LocalDateTime.now(clock));

return TemplateDeleteResult.from(template);
}

private static void requireNoOuterTransaction() {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
throw new IllegalStateException("갤러리 저장은 호출자 트랜잭션 밖에서 불러야 한다");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sniptext.domain.template.service.dto.command;

/** 템플릿 삭제(휴지통 이동) 입력. */
public record TemplateDeleteCommand(
Long userId,
Long templateId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.sniptext.domain.template.service.dto.result;

import java.time.LocalDateTime;

import com.sniptext.domain.template.entity.Template;
import com.sniptext.domain.template.support.TrashRetention;

/** 휴지통으로 옮긴 템플릿. 이름은 삭제 토스트용이다. */
public record TemplateDeleteResult(
Long templateId,
String title,
LocalDateTime deletedAt,
LocalDateTime purgeAt
) {

public static TemplateDeleteResult from(Template template) {
return new TemplateDeleteResult(
template.getId(),
template.getTitle().getValue(),
template.getDeletedAt(),
TrashRetention.purgeAt(template.getDeletedAt())
);
}

}
Loading
Loading