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
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ repositories {
dependencies {
implementation(platform("software.amazon.awssdk:bom:2.53.2"))
implementation("software.amazon.awssdk:ses")
// software.amazon.awssdk:s3에 S3Presigner가 포함되어 있어 별도 s3-presigner 아티팩트가 없다
// (AWS SDK가 이 버전대에서 통합함 — Maven Central에 s3-presigner 좌표 자체가 존재하지 않는다).
implementation("software.amazon.awssdk:s3")
implementation("org.jsoup:jsoup:1.18.3")
implementation("org.springframework.retry:spring-retry:2.0.13")
implementation("org.aspectj:aspectjweaver:1.9.25")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bcsdlab.bcsdinternalapiv2.activity.exception;

import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdException;
import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdExceptionType;

public class ActivityException extends BcsdException {

private final BcsdExceptionType exceptionType;

public ActivityException(BcsdExceptionType exceptionType) {
this.exceptionType = exceptionType;
}

@Override
public BcsdExceptionType getExceptionType() {
return exceptionType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.bcsdlab.bcsdinternalapiv2.activity.exception;

import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdExceptionType;
import org.springframework.http.HttpStatus;

public enum ActivityExceptionType implements BcsdExceptionType {

ACTIVITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 활동입니다."),
;

private final HttpStatus status;
private final String message;

ActivityExceptionType(HttpStatus status, String message) {
this.status = status;
this.message = message;
}

@Override
public HttpStatus getHttpStatus() {
return status;
}

@Override
public String getMessage() {
return message;
}

@Override
public BcsdExceptionType withDetail(String detailMessage) {
return new DetailedActivityExceptionType(this, detailMessage);
}

private record DetailedActivityExceptionType(ActivityExceptionType type, String detailMessage)
implements BcsdExceptionType {

@Override
public HttpStatus getHttpStatus() {
return type.getHttpStatus();
}

@Override
public String getMessage() {
return MESSAGE_FORMAT.formatted(type.getMessage(), detailMessage).strip();
}

@Override
public BcsdExceptionType withDetail(String detailMessage) {
return new DetailedActivityExceptionType(type, detailMessage);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bcsdlab.bcsdinternalapiv2.curriculum.exception;

import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdException;
import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdExceptionType;

public class CurriculumException extends BcsdException {

private final BcsdExceptionType exceptionType;

public CurriculumException(BcsdExceptionType exceptionType) {
this.exceptionType = exceptionType;
}

@Override
public BcsdExceptionType getExceptionType() {
return exceptionType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.bcsdlab.bcsdinternalapiv2.curriculum.exception;

import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdExceptionType;
import org.springframework.http.HttpStatus;

public enum CurriculumExceptionType implements BcsdExceptionType {

CURRICULUM_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 커리큘럼입니다."),
;

private final HttpStatus status;
private final String message;

CurriculumExceptionType(HttpStatus status, String message) {
this.status = status;
this.message = message;
}

@Override
public HttpStatus getHttpStatus() {
return status;
}

@Override
public String getMessage() {
return message;
}

@Override
public BcsdExceptionType withDetail(String detailMessage) {
return new DetailedCurriculumExceptionType(this, detailMessage);
}

private record DetailedCurriculumExceptionType(CurriculumExceptionType type, String detailMessage)
implements BcsdExceptionType {

@Override
public HttpStatus getHttpStatus() {
return type.getHttpStatus();
}

@Override
public String getMessage() {
return MESSAGE_FORMAT.formatted(type.getMessage(), detailMessage).strip();
}

@Override
public BcsdExceptionType withDetail(String detailMessage) {
return new DetailedCurriculumExceptionType(type, detailMessage);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.bcsdlab.bcsdinternalapiv2.global;

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import java.time.Instant;
import lombok.Getter;

/**
* 최상위 엔티티(트랙 페이지·커리큘럼·활동 카테고리·활동 등)에만 적용하는 soft delete 규약.
* 하위 트리(주차·토픽·세부항목·이미지)는 이 클래스를 쓰지 않고 {@code on delete cascade}로
* 물리 삭제한다.
*
* <p><b>중요</b>: {@code @SQLRestriction("deleted_at is null")}은 이 매핑된 슈퍼클래스가 아니라
* 이 클래스를 상속하는 각 {@code @Entity} 클래스에 직접 붙여야 한다. Hibernate의
* {@code @SQLRestriction}은 매핑된 슈퍼클래스로부터 상속되지 않는다.
*/
@Getter
@MappedSuperclass
public abstract class SoftDeletableEntity extends BaseTimeEntity {

@Column(name = "deleted_at")
private Instant deletedAt;

public boolean isDeleted() {
return deletedAt != null;
}

public void delete(Instant now) {
this.deletedAt = now;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.bcsdlab.bcsdinternalapiv2.global.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
@RequiredArgsConstructor
public class S3Config {

private final S3Properties s3Properties;

@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.of(s3Properties.region()))
.build();
}

@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.region(Region.of(s3Properties.region()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.bcsdlab.bcsdinternalapiv2.global.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.s3")
public record S3Properties(String region) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.bcsdlab.bcsdinternalapiv2.global.exception;

public class GlobalException extends BcsdException {

private final BcsdExceptionType exceptionType;

public GlobalException(BcsdExceptionType exceptionType) {
this.exceptionType = exceptionType;
}

@Override
public BcsdExceptionType getExceptionType() {
return exceptionType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.bcsdlab.bcsdinternalapiv2.global.exception;

import org.springframework.http.HttpStatus;

/**
* 특정 도메인에 속하지 않고 여러 도메인이 공유하는 예외 타입.
* 예: 정렬 순서 변경 요청 검증({@link com.bcsdlab.bcsdinternalapiv2.global.util.DisplayOrders})은
* 트랙·커리큘럼·활동 등 모든 도메인에서 동일한 규칙과 동일한 HTTP 상태를 쓴다.
*/
public enum GlobalExceptionType implements BcsdExceptionType {

ORDER_IDS_MISMATCH(HttpStatus.BAD_REQUEST, "순서 변경 대상이 기존 항목과 일치하지 않습니다."),
;

private final HttpStatus status;
private final String message;

GlobalExceptionType(HttpStatus status, String message) {
this.status = status;
this.message = message;
}

@Override
public HttpStatus getHttpStatus() {
return status;
}

@Override
public String getMessage() {
return message;
}

@Override
public BcsdExceptionType withDetail(String detailMessage) {
return new DetailedGlobalExceptionType(this, detailMessage);
}

private record DetailedGlobalExceptionType(GlobalExceptionType type, String detailMessage)
implements BcsdExceptionType {

@Override
public HttpStatus getHttpStatus() {
return type.getHttpStatus();
}

@Override
public String getMessage() {
return MESSAGE_FORMAT.formatted(type.getMessage(), detailMessage).strip();
}

@Override
public BcsdExceptionType withDetail(String detailMessage) {
return new DetailedGlobalExceptionType(type, detailMessage);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bcsdlab.bcsdinternalapiv2.global.util;

import com.bcsdlab.bcsdinternalapiv2.global.exception.GlobalException;
import com.bcsdlab.bcsdinternalapiv2.global.exception.GlobalExceptionType;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* {@code display_order} 재부여 순수 함수. 특정 엔티티 타입에 의존하지 않는다 —
* 트랙·커리큘럼 주차/토픽·활동 등 정렬 가능한 모든 도메인이 공유한다.
*
* <p>요청된 id 순서가 대상 id 집합과 정확히 일치하지 않으면(누락·추가·중복)
* {@link GlobalException}을 던지고 아무 값도 반환하지 않는다 — 호출자가 반환값을 받기 전에는
* 어떤 엔티티도 변경하지 않으므로 "부분 적용" 상태가 존재하지 않는다.
*/
public final class DisplayOrders {

private DisplayOrders() {
}

/**
* @param requestedIds 새 순서대로 나열된 id 목록 (배열 인덱스가 곧 새 display_order)
* @param existingIds 현재 그 부모 아래 존재하는 전체 id 집합
* @return id → 새 display_order(0-base) 매핑
* @throws GlobalException requestedIds에 중복이 있거나, existingIds와 집합이 다를 때
*/
public static Map<Long, Integer> reassign(List<Long> requestedIds, Collection<Long> existingIds) {
Set<Long> requestedSet = new HashSet<>(requestedIds);
if (requestedSet.size() != requestedIds.size()) {
throw new GlobalException(GlobalExceptionType.ORDER_IDS_MISMATCH);
}

Set<Long> existingSet = new HashSet<>(existingIds);
if (!requestedSet.equals(existingSet)) {
throw new GlobalException(GlobalExceptionType.ORDER_IDS_MISMATCH);
}

Map<Long, Integer> result = new LinkedHashMap<>();
for (int i = 0; i < requestedIds.size(); i++) {
result.put(requestedIds.get(i), i);
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bcsdlab.bcsdinternalapiv2.media.exception;

import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdException;
import com.bcsdlab.bcsdinternalapiv2.global.exception.BcsdExceptionType;

public class MediaException extends BcsdException {

private final BcsdExceptionType exceptionType;

public MediaException(BcsdExceptionType exceptionType) {
this.exceptionType = exceptionType;
}

@Override
public BcsdExceptionType getExceptionType() {
return exceptionType;
}
}
Loading