From 4c4a514332971c6c42cbc66f6bd2a239056dc4b6 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 13:43:29 +0900 Subject: [PATCH 001/102] =?UTF-8?q?Feat(commission):=20=EC=99=B8=EC=A3=BC?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20Entities,?= =?UTF-8?q?=20Enums,=20Repositories=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/commission/entity/Commission.java | 115 ++++++++++++++++++ .../commission/entity/CommissionColor.java | 56 +++++++++ .../commission/entity/CommissionConcept.java | 51 ++++++++ .../commission/entity/CommissionFile.java | 62 ++++++++++ .../entity/CommissionRequiredPage.java | 56 +++++++++ .../common/commission/entity/Textbook.java | 55 +++++++++ .../commission/entity/enums/CategoryType.java | 8 ++ .../commission/entity/enums/ColorRole.java | 7 ++ .../entity/enums/ColorSelectionMode.java | 6 + .../entity/enums/CommissionStatus.java | 10 ++ .../commission/entity/enums/ConceptTag.java | 21 ++++ .../commission/entity/enums/FileKind.java | 6 + .../commission/entity/enums/PageType.java | 13 ++ .../commission/entity/enums/PlanCode.java | 23 ++++ .../common/commission/entity/enums/Size.java | 8 ++ .../repository/CommissionColorRepository.java | 8 ++ .../CommissionConceptRepository.java | 8 ++ .../repository/CommissionFileRepository.java | 8 ++ .../repository/CommissionRepository.java | 8 ++ .../CommissionRequiredPageRepository.java | 8 ++ .../repository/TextbookRepository.java | 8 ++ 21 files changed, 545 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/Commission.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/Textbook.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/CategoryType.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorSelectionMode.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/CommissionStatus.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/FileKind.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/PageType.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionColorRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionConceptRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionFileRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/TextbookRepository.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java new file mode 100644 index 0000000..7226f2f --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java @@ -0,0 +1,115 @@ +package ditda.backend.domain.common.commission.entity; + +import java.time.LocalDate; + +import ditda.backend.domain.common.commission.entity.enums.CategoryType; +import ditda.backend.domain.common.commission.entity.enums.ColorSelectionMode; +import ditda.backend.domain.common.commission.entity.enums.CommissionStatus; +import ditda.backend.domain.common.commission.entity.enums.PlanCode; +import ditda.backend.domain.common.commission.entity.enums.Size; +import ditda.backend.domain.designer.auth.entity.Designer; +import ditda.backend.domain.instructor.auth.entity.Instructor; +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commissions") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Commission extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "instructor_id", nullable = false) + private Instructor instructor; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assigned_designer_id") + private Designer assignedDesigner; + + @Enumerated(EnumType.STRING) + @Column(name = "plan_code", length = 10, nullable = false) + private PlanCode planCode; + + @Column(name = "title", length = 50, nullable = false) + private String title; + + @Enumerated(EnumType.STRING) + @Column(name = "category_type", length = 50, nullable = false) + private CategoryType categoryType; + + @Enumerated(EnumType.STRING) + @Column(name = "size", length = 10, nullable = false) + private Size size; + + @Column(name = "additional_concept", length = 300) + private String additionalConcept; + + @Enumerated(EnumType.STRING) + @Column(name = "color_selection_mode", length = 30, nullable = false) + private ColorSelectionMode colorSelectionMode; + + @Column(name = "first_draft_deadline", nullable = false) + private LocalDate firstDraftDeadline; + + @Column(name = "application_deadline", nullable = false) + private LocalDate applicationDeadline; + + @Column(name = "final_deadline", nullable = false) + private LocalDate finalDeadline; + + @Enumerated(EnumType.STRING) + @Column(name = "status", length = 30, nullable = false) + private CommissionStatus status; + + @Column(name = "max_revision", nullable = false) + private int maxRevision; + + public static Commission create( + Instructor instructor, + PlanCode planCode, + String title, + CategoryType categoryType, + Size size, + String additionalConcept, + ColorSelectionMode colorSelectionMode, + LocalDate firstDraftDeadline, + LocalDate finalDeadline + ) { + return Commission.builder() + .instructor(instructor) + .planCode(planCode) + .title(title) + .categoryType(categoryType) + .size(size) + .additionalConcept(additionalConcept) + .colorSelectionMode(colorSelectionMode) + .firstDraftDeadline(firstDraftDeadline) + .applicationDeadline(firstDraftDeadline.minusDays(7)) + .finalDeadline(finalDeadline) + .status(CommissionStatus.PENDING) + .maxRevision(planCode.getMaxRevision()) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java new file mode 100644 index 0000000..858dda9 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java @@ -0,0 +1,56 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.ColorRole; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commission_colors") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommissionColor { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_color_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @Enumerated(EnumType.STRING) + @Column(name = "role", length = 10, nullable = false) + private ColorRole role; + + @Column(name = "color_code", length = 7, nullable = false) + private String colorCode; + + public static CommissionColor create( + Commission commission, + ColorRole role, + String colorCode + ) { + return CommissionColor.builder() + .commission(commission) + .role(role) + .colorCode(colorCode) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java new file mode 100644 index 0000000..e955272 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java @@ -0,0 +1,51 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.ConceptTag; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commission_concepts") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommissionConcept { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_concept_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @Enumerated(EnumType.STRING) + @Column(name = "concept", length = 30, nullable = false) + private ConceptTag concept; + + public static CommissionConcept create( + Commission commission, + ConceptTag concept + ) { + return CommissionConcept.builder() + .commission(commission) + .concept(concept) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java new file mode 100644 index 0000000..6f00bf8 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java @@ -0,0 +1,62 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.FileKind; +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commission_files") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommissionFile extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_file_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @Enumerated(EnumType.STRING) + @Column(name = "file_type", length = 20, nullable = false) + private FileKind fileType; + + @Column(name = "file_url", nullable = false) + private String fileUrl; + + @Column(name = "description", length = 300) + private String description; + + public static CommissionFile create( + Commission commission, + FileKind fileType, + String fileUrl, + String description + ) { + return CommissionFile.builder() + .commission(commission) + .fileType(fileType) + .fileUrl(fileUrl) + .description(description) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java new file mode 100644 index 0000000..d4c2cd8 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java @@ -0,0 +1,56 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.PageType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commission_required_pages") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommissionRequiredPage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_required_page_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @Enumerated(EnumType.STRING) + @Column(name = "page_type", length = 30, nullable = false) + private PageType pageType; + + @Column(name = "page_description", length = 150) + private String pageDescription; + + public static CommissionRequiredPage create( + Commission commission, + PageType pageType, + String pageDescription + ) { + return CommissionRequiredPage.builder() + .commission(commission) + .pageType(pageType) + .pageDescription(pageDescription) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Textbook.java b/src/main/java/ditda/backend/domain/common/commission/entity/Textbook.java new file mode 100644 index 0000000..48132f6 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/Textbook.java @@ -0,0 +1,55 @@ +package ditda.backend.domain.common.commission.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "textbooks") +@Getter +@Builder +@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class Textbook { + + @Id + @Column(name = "commission_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "commission_id") + private Commission commission; + + @Column(name = "title", length = 50, nullable = false) + private String title; + + @Column(name = "instructor_name", length = 50, nullable = false) + private String instructorName; + + @Column(name = "subject", length = 50, nullable = false) + private String subject; + + public static Textbook create( + Commission commission, + String title, + String instructorName, + String subject + ) { + return Textbook.builder() + .commission(commission) + .title(title) + .instructorName(instructorName) + .subject(subject) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/CategoryType.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/CategoryType.java new file mode 100644 index 0000000..f58d628 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/CategoryType.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum CategoryType { + // 유인물 - 교재 + FLYER_TEXTBOOK_COVER_INNER, // 교재 외지/내지 + + // 추후 카테고리 확장 시 추가 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java new file mode 100644 index 0000000..a89288a --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java @@ -0,0 +1,7 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum ColorRole { + MAIN, + SUB1, + SUB2 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorSelectionMode.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorSelectionMode.java new file mode 100644 index 0000000..e670a80 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorSelectionMode.java @@ -0,0 +1,6 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum ColorSelectionMode { + USER_SELECTED, + DESIGNER_DELEGATED +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/CommissionStatus.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/CommissionStatus.java new file mode 100644 index 0000000..dc57706 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/CommissionStatus.java @@ -0,0 +1,10 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum CommissionStatus { + PENDING, // 결제 대기 + RECRUITING, // 디자이너 모집/지원 + IN_PROGRESS, // 1차 시안 제출 대기 / 강사 선택 대기 + EDITING, // 수정 진행 중 + COMPLETED, // 최종 확정 + CANCELLED // 취소 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java new file mode 100644 index 0000000..33baa08 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java @@ -0,0 +1,21 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum ConceptTag { + // 밝은 + CUTE, + LIGHT, + CLEAR, + // 부드러운 + NATURAL, + SUBTLE, + WARM, + // 고급스러운 + ELEGANT, + REFINED, + MODERN, + // 강렬한 + VIVID, + DYNAMIC, + // 단정한 + CALM +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/FileKind.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/FileKind.java new file mode 100644 index 0000000..f503b5b --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/FileKind.java @@ -0,0 +1,6 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum FileKind { + MATERIAL, // 자료 첨부 + REFERENCE // 레퍼런스 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PageType.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/PageType.java new file mode 100644 index 0000000..c523662 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/PageType.java @@ -0,0 +1,13 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum PageType { + INSTRUCTOR_PROFILE, // 강사 프로필 + AUTHORS_NOTE, // 저자의 말 + TABLE_OF_CONTENTS, // 목차 + UNIT_INTRO, // 단원 시작 간지 + CONCEPT, // 개념 설명 + EXAMPLE, // 대표 유형 + PROBLEM, // 문제 풀이 + NOTE, // 노트 + COVER // 표지 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java new file mode 100644 index 0000000..efa639d --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java @@ -0,0 +1,23 @@ +package ditda.backend.domain.common.commission.entity.enums; + +import lombok.Getter; + +@Getter +public enum PlanCode { + + BASIC(3, 400000, 3, "디자이너 3명에 대한 시안을 받아볼 수 있습니다."), + PLUS(4, 480000, 3, "더 다양한 디자이너의 시안을 받아볼 수 있습니다."), + MAX(5, 560000, 3, "가장 많은 디자이너의 시안을 받아볼 수 있습니다."); + + private final int designerCount; + private final int price; + private final int maxRevision; + private final String description; + + PlanCode(int designerCount, int price, int maxRevision, String description) { + this.designerCount = designerCount; + this.price = price; + this.maxRevision = maxRevision; + this.description = description; + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java new file mode 100644 index 0000000..455cc59 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum Size { + A4, + B5, + B6, + A5 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionColorRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionColorRepository.java new file mode 100644 index 0000000..4e9aca1 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionColorRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionColor; + +public interface CommissionColorRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionConceptRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionConceptRepository.java new file mode 100644 index 0000000..84aca9d --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionConceptRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionConcept; + +public interface CommissionConceptRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionFileRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionFileRepository.java new file mode 100644 index 0000000..e147838 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionFileRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionFile; + +public interface CommissionFileRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRepository.java new file mode 100644 index 0000000..7ac0e11 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.Commission; + +public interface CommissionRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java new file mode 100644 index 0000000..9e88bf8 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionRequiredPage; + +public interface CommissionRequiredPageRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/TextbookRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/TextbookRepository.java new file mode 100644 index 0000000..5dedabe --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/TextbookRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.Textbook; + +public interface TextbookRepository extends JpaRepository { +} From 8256e0ae50b555ff91c526802a4c29fa5b9fff55 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 13:43:46 +0900 Subject: [PATCH 002/102] =?UTF-8?q?Feat(term):=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=95=BD=EA=B4=80=20Entity=20and=20Repository=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/term/entity/PaymentTerm.java | 54 +++++++++++++++++++ .../repository/PaymentTermRepository.java | 8 +++ 2 files changed, 62 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/common/term/entity/PaymentTerm.java create mode 100644 src/main/java/ditda/backend/domain/common/term/repository/PaymentTermRepository.java diff --git a/src/main/java/ditda/backend/domain/common/term/entity/PaymentTerm.java b/src/main/java/ditda/backend/domain/common/term/entity/PaymentTerm.java new file mode 100644 index 0000000..f730ba1 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/term/entity/PaymentTerm.java @@ -0,0 +1,54 @@ +package ditda.backend.domain.common.term.entity; + +import ditda.backend.domain.common.payment.entity.Payment; +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "payment_terms") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PaymentTerm extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "payment_term_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "payment_id", nullable = false) + private Payment payment; + + @Column(name = "version", length = 20, nullable = false) + private String version; + + @Column(name = "is_agreed", nullable = false) + private boolean isAgreed; + + public static PaymentTerm create( + Payment payment, + String version, + boolean isAgreed + ) { + return PaymentTerm.builder() + .payment(payment) + .version(version) + .isAgreed(isAgreed) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/term/repository/PaymentTermRepository.java b/src/main/java/ditda/backend/domain/common/term/repository/PaymentTermRepository.java new file mode 100644 index 0000000..bd24aa9 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/term/repository/PaymentTermRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.term.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.term.entity.PaymentTerm; + +public interface PaymentTermRepository extends JpaRepository { +} From 72108a7b3b4b60cdd52d0eb10f8f29702441e033 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 13:43:54 +0900 Subject: [PATCH 003/102] =?UTF-8?q?Feat(payment):=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20Entity,=20Enum,=20Repository=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/payment/entity/Payment.java | 62 +++++++++++++++++++ .../payment/entity/enums/PaymentStatus.java | 8 +++ .../payment/repository/PaymentRepository.java | 8 +++ 3 files changed, 78 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/common/payment/entity/Payment.java create mode 100644 src/main/java/ditda/backend/domain/common/payment/entity/enums/PaymentStatus.java create mode 100644 src/main/java/ditda/backend/domain/common/payment/repository/PaymentRepository.java diff --git a/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java b/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java new file mode 100644 index 0000000..fbf72d1 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java @@ -0,0 +1,62 @@ +package ditda.backend.domain.common.payment.entity; + +import java.time.LocalDateTime; + +import ditda.backend.domain.common.commission.entity.Commission; +import ditda.backend.domain.common.payment.entity.enums.PaymentStatus; +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "payments") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Payment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "payment_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @Column(name = "amount", nullable = false) + private int amount; + + @Column(name = "depositor_name", length = 50) + private String depositorName; + + @Enumerated(EnumType.STRING) + @Column(name = "status", length = 30, nullable = false) + private PaymentStatus status; + + @Column(name = "paid_at") + private LocalDateTime paidAt; + + public static Payment create(Commission commission, int amount) { + return Payment.builder() + .commission(commission) + .amount(amount) + .status(PaymentStatus.PENDING) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/payment/entity/enums/PaymentStatus.java b/src/main/java/ditda/backend/domain/common/payment/entity/enums/PaymentStatus.java new file mode 100644 index 0000000..7dae8e8 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/payment/entity/enums/PaymentStatus.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.payment.entity.enums; + +public enum PaymentStatus { + PENDING, // 외주 신청 + 입금 확인 대기 + COMPLETED, // 외주 신청 + 입금 확인 + REFUNDED, // 환불 + FAILED // 실패 +} diff --git a/src/main/java/ditda/backend/domain/common/payment/repository/PaymentRepository.java b/src/main/java/ditda/backend/domain/common/payment/repository/PaymentRepository.java new file mode 100644 index 0000000..3973cbc --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/payment/repository/PaymentRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.payment.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.payment.entity.Payment; + +public interface PaymentRepository extends JpaRepository { +} From 2227762bf5c4ae037d8c6d56271b97ffdda2d02b Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 14:39:09 +0900 Subject: [PATCH 004/102] =?UTF-8?q?Feat(commission):=20=EC=99=B8=EC=A3=BC?= =?UTF-8?q?=20=EC=A7=80=EC=9B=90=20=EA=B4=80=EB=A0=A8=20Entities,=20Enums,?= =?UTF-8?q?=20Repositories=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/ApplicationDraftPage.java | 77 +++++++++++++++++++ .../entity/CommissionApplication.java | 63 +++++++++++++++ .../entity/enums/ApplicationStatus.java | 7 ++ .../commission/entity/enums/DraftStatus.java | 7 ++ .../ApplicationDraftPageRepository.java | 8 ++ .../CommissionApplicationRepository.java | 8 ++ 6 files changed, 170 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/DraftStatus.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationRepository.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java new file mode 100644 index 0000000..07a35d9 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java @@ -0,0 +1,77 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.DraftStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "application_draft_pages") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ApplicationDraftPage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "application_draft_page_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_application_id", nullable = false) + private CommissionApplication application; + + @Column(name = "page_order", nullable = false) + private int pageOrder; + + @Column(name = "name", length = 100, nullable = false) + private String name; + + @Column(name = "file_url", nullable = false) + private String fileUrl; + + @Column(name = "watermarked_file_url") + private String watermarkedFileUrl; + + @Enumerated(EnumType.STRING) + @Column(name = "draft_status", length = 20, nullable = false) + private DraftStatus draftStatus; + + public static ApplicationDraftPage create( + CommissionApplication application, + int pageOrder, + String name, + String fileUrl + ) { + return ApplicationDraftPage.builder() + .application(application) + .pageOrder(pageOrder) + .name(name) + .fileUrl(fileUrl) + .draftStatus(DraftStatus.PROCESSING) + .build(); + } + + public void completeWatermark(String watermarkedFileUrl) { + this.watermarkedFileUrl = watermarkedFileUrl; + this.draftStatus = DraftStatus.COMPLETED; + } + + public void failWatermark() { + this.draftStatus = DraftStatus.FAILED; + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java new file mode 100644 index 0000000..c730a85 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java @@ -0,0 +1,63 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.ApplicationStatus; +import ditda.backend.domain.designer.auth.entity.Designer; +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commission_applications") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommissionApplication extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_application_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "designer_id", nullable = false) + private Designer designer; + + @Enumerated(EnumType.STRING) + @Column(name = "status", length = 20, nullable = false) + private ApplicationStatus status; + + public static CommissionApplication create(Commission commission, Designer designer) { + return CommissionApplication.builder() + .commission(commission) + .designer(designer) + .status(ApplicationStatus.PENDING) + .build(); + } + + public void approve() { + this.status = ApplicationStatus.APPROVED; + } + + public void reject() { + this.status = ApplicationStatus.REJECTED; + } +} \ No newline at end of file diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java new file mode 100644 index 0000000..2fb30bc --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java @@ -0,0 +1,7 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum ApplicationStatus { + PENDING, // 지원 완료 + APPROVED, // 강사 시안 선택 + REJECTED // 미선택 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/DraftStatus.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/DraftStatus.java new file mode 100644 index 0000000..e9f232d --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/DraftStatus.java @@ -0,0 +1,7 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum DraftStatus { + PROCESSING, // 워터마크 처리 중 + COMPLETED, // 워터마크 처리 완료 및 강사 조회 가능 + FAILED // 처리 실패 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java new file mode 100644 index 0000000..24d47fa --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.ApplicationDraftPage; + +public interface ApplicationDraftPageRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationRepository.java new file mode 100644 index 0000000..43601a3 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionApplication; + +public interface CommissionApplicationRepository extends JpaRepository { +} From 99b2fca9c1c7916b8f91f25ada568528ecc3854d Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 14:39:27 +0900 Subject: [PATCH 005/102] =?UTF-8?q?Feat(commission):=20=EC=99=B8=EC=A3=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EA=B4=80=EB=A0=A8=20Entities,=20Enums,?= =?UTF-8?q?=20Repositories=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/entity/CommissionDraft.java | 68 ++++++++++++++++ .../entity/CommissionDraftPage.java | 77 +++++++++++++++++++ .../commission/entity/RevisionDetail.java | 57 ++++++++++++++ .../commission/entity/RevisionRequest.java | 45 +++++++++++ .../entity/enums/RevisionCategory.java | 9 +++ .../CommissionDraftPageRepository.java | 8 ++ .../repository/CommissionDraftRepository.java | 8 ++ .../repository/RevisionDetailRepository.java | 8 ++ .../repository/RevisionRequestRepository.java | 8 ++ 9 files changed, 288 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/RevisionCategory.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftPageRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/RevisionDetailRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/RevisionRequestRepository.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java new file mode 100644 index 0000000..5a3de87 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java @@ -0,0 +1,68 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commission_drafts") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommissionDraft extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_draft_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "revision_request_id") + private RevisionRequest revisionRequest; + + @Column(name = "round", nullable = false) + private int round; + + @Column(name = "designer_comment", length = 500) + private String designerComment; + + @Builder.Default + @Column(name = "is_final", nullable = false) + private boolean isFinal = false; + + public static CommissionDraft create( + Commission commission, + RevisionRequest revisionRequest, + int round, + String designerComment + ) { + return CommissionDraft.builder() + .commission(commission) + .revisionRequest(revisionRequest) + .round(round) + .designerComment(designerComment) + .isFinal(false) + .build(); + } + + public void markAsFinal() { + this.isFinal = true; + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java new file mode 100644 index 0000000..30db9d3 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java @@ -0,0 +1,77 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.DraftStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "commission_draft_pages") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommissionDraftPage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "commission_draft_page_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_draft_id", nullable = false) + private CommissionDraft commissionDraft; + + @Column(name = "page_order", nullable = false) + private int pageOrder; + + @Column(name = "name", length = 100, nullable = false) + private String name; + + @Column(name = "file_url", nullable = false) + private String fileUrl; + + @Column(name = "watermarked_file_url") + private String watermarkedFileUrl; + + @Enumerated(EnumType.STRING) + @Column(name = "draft_status", length = 20, nullable = false) + private DraftStatus draftStatus; + + public static CommissionDraftPage create( + CommissionDraft commissionDraft, + int pageOrder, + String name, + String fileUrl + ) { + return CommissionDraftPage.builder() + .commissionDraft(commissionDraft) + .pageOrder(pageOrder) + .name(name) + .fileUrl(fileUrl) + .draftStatus(DraftStatus.PROCESSING) + .build(); + } + + public void completeWatermark(String watermarkedFileUrl) { + this.watermarkedFileUrl = watermarkedFileUrl; + this.draftStatus = DraftStatus.COMPLETED; + } + + public void failWatermark() { + this.draftStatus = DraftStatus.FAILED; + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java new file mode 100644 index 0000000..0fddd82 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java @@ -0,0 +1,57 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.domain.common.commission.entity.enums.RevisionCategory; +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "revision_details") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RevisionDetail extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "revision_detail_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "revision_request_id", nullable = false) + private RevisionRequest revisionRequest; + + @Enumerated(EnumType.STRING) + @Column(name = "category", length = 20, nullable = false) + private RevisionCategory category; + + @Column(name = "comment", length = 300, nullable = false) + private String comment; + + public static RevisionDetail create( + RevisionRequest revisionRequest, + RevisionCategory category, + String comment + ) { + return RevisionDetail.builder() + .revisionRequest(revisionRequest) + .category(category) + .comment(comment) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java new file mode 100644 index 0000000..07465e7 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java @@ -0,0 +1,45 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "revision_requests") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RevisionRequest extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "revision_request_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_id", nullable = false) + private Commission commission; + + @Column(name = "round", nullable = false) + private int round; + + public static RevisionRequest create(Commission commission, int round) { + return RevisionRequest.builder() + .commission(commission) + .round(round) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/RevisionCategory.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/RevisionCategory.java new file mode 100644 index 0000000..ee208dc --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/RevisionCategory.java @@ -0,0 +1,9 @@ +package ditda.backend.domain.common.commission.entity.enums; + +public enum RevisionCategory { + LAYOUT, // 레이아웃 수정 + TYPOGRAPHY, // 타이포 수정 + DESIGN, // 디자인 수정 + COLOR, // 색상 수정 + ETC // 기타 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftPageRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftPageRepository.java new file mode 100644 index 0000000..fb04ce3 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftPageRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionDraftPage; + +public interface CommissionDraftPageRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftRepository.java new file mode 100644 index 0000000..189e5b7 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionDraft; + +public interface CommissionDraftRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionDetailRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/RevisionDetailRepository.java new file mode 100644 index 0000000..a5fade7 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/RevisionDetailRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.RevisionDetail; + +public interface RevisionDetailRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionRequestRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/RevisionRequestRepository.java new file mode 100644 index 0000000..359ad80 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/RevisionRequestRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.RevisionRequest; + +public interface RevisionRequestRepository extends JpaRepository { +} From d77c5465ef780e570c8f660e6e5bec11f99b6d2c Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 14:40:24 +0900 Subject: [PATCH 006/102] =?UTF-8?q?Chore(commission):=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EB=81=9D=EC=97=90=20=EC=A4=84=EB=B0=94=EA=BF=88=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/commission/entity/CommissionApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java index c730a85..2c63a4a 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java @@ -60,4 +60,4 @@ public void approve() { public void reject() { this.status = ApplicationStatus.REJECTED; } -} \ No newline at end of file +} From 6edcd96503636c964560e8c41b7e193a105cce66 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 14:57:50 +0900 Subject: [PATCH 007/102] =?UTF-8?q?Refactor(commission):=20enum=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B3=80=EC=88=98=EB=AA=85=20=EB=A6=AC=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/commission/entity/CommissionApplication.java | 5 +++-- .../domain/common/commission/entity/CommissionFile.java | 8 ++++---- .../common/commission/entity/enums/ApplicationStatus.java | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java index 2c63a4a..bcb7ed5 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java @@ -53,8 +53,9 @@ public static CommissionApplication create(Commission commission, Designer desig .build(); } - public void approve() { - this.status = ApplicationStatus.APPROVED; + public void select(Designer selectedDesigner) { + this.designer = selectedDesigner; + this.status = ApplicationStatus.SELECTED; } public void reject() { diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java index 6f00bf8..abd90ff 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java @@ -37,8 +37,8 @@ public class CommissionFile extends BaseEntity { private Commission commission; @Enumerated(EnumType.STRING) - @Column(name = "file_type", length = 20, nullable = false) - private FileKind fileType; + @Column(name = "file_kind", length = 20, nullable = false) + private FileKind fileKind; @Column(name = "file_url", nullable = false) private String fileUrl; @@ -48,13 +48,13 @@ public class CommissionFile extends BaseEntity { public static CommissionFile create( Commission commission, - FileKind fileType, + FileKind fileKind, String fileUrl, String description ) { return CommissionFile.builder() .commission(commission) - .fileType(fileType) + .fileKind(fileKind) .fileUrl(fileUrl) .description(description) .build(); diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java index 2fb30bc..d9baa76 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java @@ -2,6 +2,6 @@ public enum ApplicationStatus { PENDING, // 지원 완료 - APPROVED, // 강사 시안 선택 + SELECTED, // 강사 시안 선택 REJECTED // 미선택 } From 437d31bd276672bf6a4b82794fc69bbd0fcbd38c Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 15:17:58 +0900 Subject: [PATCH 008/102] =?UTF-8?q?Feat(commission):=20DB=20=EC=9C=A0?= =?UTF-8?q?=EB=8B=88=ED=81=AC=20=EC=A0=9C=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/entity/ApplicationDraftPage.java | 12 +++++++++++- .../commission/entity/CommissionApplication.java | 15 ++++++++++++--- .../common/commission/entity/CommissionColor.java | 12 +++++++++++- .../commission/entity/CommissionConcept.java | 12 +++++++++++- .../common/commission/entity/CommissionDraft.java | 12 +++++++++++- .../commission/entity/CommissionDraftPage.java | 12 +++++++++++- .../commission/entity/CommissionRequiredPage.java | 12 +++++++++++- .../common/commission/entity/RevisionRequest.java | 12 +++++++++++- 8 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java index 07a35d9..bc5a1bd 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java @@ -12,6 +12,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -19,7 +20,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "application_draft_pages") +// 한 시안의 페이지 순서 중복 X +@Table( + name = "application_draft_pages", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_application_draft_order", + columnNames = {"commission_application_id", "page_order"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java index bcb7ed5..8071a60 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java @@ -14,6 +14,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -21,7 +22,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "commission_applications") +// 한 디자이너는 한 외주에 1번만 지원 +@Table( + name = "commission_applications", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_commission_application", + columnNames = {"commission_id", "designer_id"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -53,8 +63,7 @@ public static CommissionApplication create(Commission commission, Designer desig .build(); } - public void select(Designer selectedDesigner) { - this.designer = selectedDesigner; + public void select() { this.status = ApplicationStatus.SELECTED; } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java index 858dda9..e3704c2 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java @@ -12,6 +12,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -19,7 +20,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "commission_colors") +// 하나의 외주에 MAIN/SUB1/SUB2 각 1개씩만 +@Table( + name = "commission_colors", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_commission_color_role", + columnNames = {"commission_id", "role"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java index e955272..1c63936 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java @@ -12,6 +12,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -19,7 +20,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "commission_concepts") +// 같은 컨셉 태그 중복 X +@Table( + name = "commission_concepts", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_commission_concept", + columnNames = {"commission_id", "concept"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java index 5a3de87..2b26220 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java @@ -10,6 +10,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -17,7 +18,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "commission_drafts") +// 한 외주에 같은 라운드 시안 1개만 +@Table( + name = "commission_drafts", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_commission_draft_round", + columnNames = {"commission_id", "round"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java index 30db9d3..1b7f0c8 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java @@ -12,6 +12,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -19,7 +20,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "commission_draft_pages") +// 한 시안의 페이지 순서 중복 X +@Table( + name = "commission_draft_pages", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_commission_draft_page_order", + columnNames = {"commission_draft_id", "page_order"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java index d4c2cd8..3977b35 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java @@ -12,6 +12,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -19,7 +20,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "commission_required_pages") +// 같은 페이지 종류 중복 X +@Table( + name = "commission_required_pages", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_commission_required_page_type", + columnNames = {"commission_id", "page_type"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java index 07465e7..3989eb5 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java @@ -10,6 +10,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -17,7 +18,16 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "revision_requests") +// 한 외주에 같은 라운드 1개만 +@Table( + name = "revision_requests", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_revision_request_round", + columnNames = {"commission_id", "round"} + ) + } +) @Getter @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) From 30d034045017d0191237920bfd628edf7f77593b Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 17:03:34 +0900 Subject: [PATCH 009/102] =?UTF-8?q?Feat(commission):=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=95=A8=EC=88=98=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/ApplicationDraftPage.java | 28 +++++++----------- .../common/commission/entity/Commission.java | 29 ++++++++++++++++++- .../entity/CommissionApplication.java | 8 ----- .../commission/entity/CommissionDraft.java | 19 ------------ .../entity/CommissionDraftPage.java | 20 ++++--------- .../commission/entity/RevisionDetail.java | 7 +++++ .../commission/entity/RevisionRequest.java | 7 ----- .../exception/CommissionErrorCode.java | 25 ++++++++++++++++ 8 files changed, 76 insertions(+), 67 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java index bc5a1bd..8423f34 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java @@ -1,6 +1,7 @@ package ditda.backend.domain.common.commission.entity; import ditda.backend.domain.common.commission.entity.enums.DraftStatus; +import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -25,7 +26,7 @@ name = "application_draft_pages", uniqueConstraints = { @UniqueConstraint( - name = "uk_application_draft_order", + name = "uk_application_draft_page_order", columnNames = {"commission_application_id", "page_order"} ) } @@ -34,7 +35,7 @@ @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ApplicationDraftPage { +public class ApplicationDraftPage extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -43,13 +44,13 @@ public class ApplicationDraftPage { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "commission_application_id", nullable = false) - private CommissionApplication application; + private CommissionApplication commissionApplication; @Column(name = "page_order", nullable = false) private int pageOrder; - @Column(name = "name", length = 100, nullable = false) - private String name; + @Column(name = "file_name", length = 100, nullable = false) + private String fileName; @Column(name = "file_url", nullable = false) private String fileUrl; @@ -62,26 +63,17 @@ public class ApplicationDraftPage { private DraftStatus draftStatus; public static ApplicationDraftPage create( - CommissionApplication application, + CommissionApplication commissionApplication, int pageOrder, - String name, + String fileName, String fileUrl ) { return ApplicationDraftPage.builder() - .application(application) + .commissionApplication(commissionApplication) .pageOrder(pageOrder) - .name(name) + .fileName(fileName) .fileUrl(fileUrl) .draftStatus(DraftStatus.PROCESSING) .build(); } - - public void completeWatermark(String watermarkedFileUrl) { - this.watermarkedFileUrl = watermarkedFileUrl; - this.draftStatus = DraftStatus.COMPLETED; - } - - public void failWatermark() { - this.draftStatus = DraftStatus.FAILED; - } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java index 7226f2f..78f2049 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java @@ -7,8 +7,10 @@ import ditda.backend.domain.common.commission.entity.enums.CommissionStatus; import ditda.backend.domain.common.commission.entity.enums.PlanCode; import ditda.backend.domain.common.commission.entity.enums.Size; +import ditda.backend.domain.common.commission.exception.CommissionErrorCode; import ditda.backend.domain.designer.auth.entity.Designer; import ditda.backend.domain.instructor.auth.entity.Instructor; +import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -35,6 +37,13 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Commission extends BaseEntity { + // 외주 등록 ~ 1차 시안 마감 일수 + private static final int MIN_DAYS_UNTIL_FIRST_DRAFT = 10; + // 1차 시안 마감 ~ 최종 마감 일수 + private static final int MIN_DAYS_BETWEEN_DEADLINES = 14; + // 시안 지원 제출 마감 ~ 1치 시안 마감 일수 + private static final int APPLICATION_DEADLINE_OFFSET_DAYS = 7; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "commission_id") @@ -97,6 +106,24 @@ public static Commission create( LocalDate firstDraftDeadline, LocalDate finalDeadline ) { + if (firstDraftDeadline == null) { + throw new GeneralException(CommissionErrorCode.FIRST_DRAFT_DEADLINE_REQUIRED); + } + + if (finalDeadline == null) { + throw new GeneralException(CommissionErrorCode.FINAL_DEADLINE_REQUIRED); + } + + // 1차 시안 제출일은 외주 등록일로부터 최소 10일 뒤 + if (firstDraftDeadline.minusDays(MIN_DAYS_UNTIL_FIRST_DRAFT).isBefore(LocalDate.now())) { + throw new GeneralException(CommissionErrorCode.FIRST_DRAFT_DEADLINE_TOO_SOON); + } + + // 최종 제출일은 1차 시안 마감일로부터 최소 14일 뒤 + if (finalDeadline.minusDays(MIN_DAYS_BETWEEN_DEADLINES).isBefore(firstDraftDeadline)) { + throw new GeneralException(CommissionErrorCode.FINAL_DEADLINE_TOO_SOON); + } + return Commission.builder() .instructor(instructor) .planCode(planCode) @@ -106,7 +133,7 @@ public static Commission create( .additionalConcept(additionalConcept) .colorSelectionMode(colorSelectionMode) .firstDraftDeadline(firstDraftDeadline) - .applicationDeadline(firstDraftDeadline.minusDays(7)) + .applicationDeadline(firstDraftDeadline.minusDays(APPLICATION_DEADLINE_OFFSET_DAYS)) .finalDeadline(finalDeadline) .status(CommissionStatus.PENDING) .maxRevision(planCode.getMaxRevision()) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java index 8071a60..d8a29af 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java @@ -62,12 +62,4 @@ public static CommissionApplication create(Commission commission, Designer desig .status(ApplicationStatus.PENDING) .build(); } - - public void select() { - this.status = ApplicationStatus.SELECTED; - } - - public void reject() { - this.status = ApplicationStatus.REJECTED; - } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java index 2b26220..abaf690 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java @@ -56,23 +56,4 @@ public class CommissionDraft extends BaseEntity { @Builder.Default @Column(name = "is_final", nullable = false) private boolean isFinal = false; - - public static CommissionDraft create( - Commission commission, - RevisionRequest revisionRequest, - int round, - String designerComment - ) { - return CommissionDraft.builder() - .commission(commission) - .revisionRequest(revisionRequest) - .round(round) - .designerComment(designerComment) - .isFinal(false) - .build(); - } - - public void markAsFinal() { - this.isFinal = true; - } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java index 1b7f0c8..edcd71f 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java @@ -1,6 +1,7 @@ package ditda.backend.domain.common.commission.entity; import ditda.backend.domain.common.commission.entity.enums.DraftStatus; +import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -34,7 +35,7 @@ @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CommissionDraftPage { +public class CommissionDraftPage extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -48,8 +49,8 @@ public class CommissionDraftPage { @Column(name = "page_order", nullable = false) private int pageOrder; - @Column(name = "name", length = 100, nullable = false) - private String name; + @Column(name = "file_name", length = 100, nullable = false) + private String fileName; @Column(name = "file_url", nullable = false) private String fileUrl; @@ -64,24 +65,15 @@ public class CommissionDraftPage { public static CommissionDraftPage create( CommissionDraft commissionDraft, int pageOrder, - String name, + String fileName, String fileUrl ) { return CommissionDraftPage.builder() .commissionDraft(commissionDraft) .pageOrder(pageOrder) - .name(name) + .fileName(fileName) .fileUrl(fileUrl) .draftStatus(DraftStatus.PROCESSING) .build(); } - - public void completeWatermark(String watermarkedFileUrl) { - this.watermarkedFileUrl = watermarkedFileUrl; - this.draftStatus = DraftStatus.COMPLETED; - } - - public void failWatermark() { - this.draftStatus = DraftStatus.FAILED; - } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java index 0fddd82..d80757d 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java @@ -1,6 +1,8 @@ package ditda.backend.domain.common.commission.entity; import ditda.backend.domain.common.commission.entity.enums.RevisionCategory; +import ditda.backend.domain.common.commission.exception.CommissionErrorCode; +import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -48,6 +50,11 @@ public static RevisionDetail create( RevisionCategory category, String comment ) { + + if (comment == null || comment.isBlank()) { + throw new GeneralException(CommissionErrorCode.REVISION_COMMENT_REQUIRED); + } + return RevisionDetail.builder() .revisionRequest(revisionRequest) .category(category) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java index 3989eb5..fdcfd2f 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java @@ -45,11 +45,4 @@ public class RevisionRequest extends BaseEntity { @Column(name = "round", nullable = false) private int round; - - public static RevisionRequest create(Commission commission, int round) { - return RevisionRequest.builder() - .commission(commission) - .round(round) - .build(); - } } diff --git a/src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java new file mode 100644 index 0000000..25b4d2d --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java @@ -0,0 +1,25 @@ +package ditda.backend.domain.common.commission.exception; + +import org.springframework.http.HttpStatus; + +import ditda.backend.global.apipayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum CommissionErrorCode implements BaseErrorCode { + + // === 외주 생성 검증 === + FIRST_DRAFT_DEADLINE_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_01", "1차 시안 마감일은 필수입니다."), + FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_02", "1차 시안 마감일은 오늘로부터 최소 10일 뒤여야 합니다."), + FINAL_DEADLINE_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_03", "최종 마감일은 필수입니다."), + FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "최종 마감일은 1차 시안 마감일로부터 최소 14일 뒤여야 합니다."), + + // === 수정 요청 검증 === + REVISION_COMMENT_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "수정 요청 코멘트는 비어있을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} From 9b399a2844370b4b52611cb0dd62064a15067761 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 17:03:53 +0900 Subject: [PATCH 010/102] =?UTF-8?q?Feat(payment):=20=EB=B9=8C=EB=8D=94=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ditda/backend/domain/common/payment/entity/Payment.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java b/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java index fbf72d1..84765e2 100644 --- a/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java +++ b/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java @@ -52,10 +52,11 @@ public class Payment extends BaseEntity { @Column(name = "paid_at") private LocalDateTime paidAt; - public static Payment create(Commission commission, int amount) { + public static Payment create(Commission commission) { + return Payment.builder() .commission(commission) - .amount(amount) + .amount(commission.getPlanCode().getPrice()) .status(PaymentStatus.PENDING) .build(); } From bc3a746adc356853df6bd0f0ce0474d71ef45ac9 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 28 May 2026 17:24:51 +0900 Subject: [PATCH 011/102] =?UTF-8?q?Refactor(commission):=20Factory=20Metho?= =?UTF-8?q?d=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0.=20=EC=B6=94=ED=9B=84=20api=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EC=8B=9C=20=EC=88=98=EC=A0=95=20=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/commission/entity/Commission.java | 24 ------------------ .../commission/entity/CommissionDraft.java | 14 +++++++++++ .../commission/entity/RevisionDetail.java | 7 ------ .../commission/entity/RevisionRequest.java | 7 ++++++ .../exception/CommissionErrorCode.java | 25 ------------------- 5 files changed, 21 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java index 78f2049..415ce02 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java @@ -7,10 +7,8 @@ import ditda.backend.domain.common.commission.entity.enums.CommissionStatus; import ditda.backend.domain.common.commission.entity.enums.PlanCode; import ditda.backend.domain.common.commission.entity.enums.Size; -import ditda.backend.domain.common.commission.exception.CommissionErrorCode; import ditda.backend.domain.designer.auth.entity.Designer; import ditda.backend.domain.instructor.auth.entity.Instructor; -import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -37,10 +35,6 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Commission extends BaseEntity { - // 외주 등록 ~ 1차 시안 마감 일수 - private static final int MIN_DAYS_UNTIL_FIRST_DRAFT = 10; - // 1차 시안 마감 ~ 최종 마감 일수 - private static final int MIN_DAYS_BETWEEN_DEADLINES = 14; // 시안 지원 제출 마감 ~ 1치 시안 마감 일수 private static final int APPLICATION_DEADLINE_OFFSET_DAYS = 7; @@ -106,24 +100,6 @@ public static Commission create( LocalDate firstDraftDeadline, LocalDate finalDeadline ) { - if (firstDraftDeadline == null) { - throw new GeneralException(CommissionErrorCode.FIRST_DRAFT_DEADLINE_REQUIRED); - } - - if (finalDeadline == null) { - throw new GeneralException(CommissionErrorCode.FINAL_DEADLINE_REQUIRED); - } - - // 1차 시안 제출일은 외주 등록일로부터 최소 10일 뒤 - if (firstDraftDeadline.minusDays(MIN_DAYS_UNTIL_FIRST_DRAFT).isBefore(LocalDate.now())) { - throw new GeneralException(CommissionErrorCode.FIRST_DRAFT_DEADLINE_TOO_SOON); - } - - // 최종 제출일은 1차 시안 마감일로부터 최소 14일 뒤 - if (finalDeadline.minusDays(MIN_DAYS_BETWEEN_DEADLINES).isBefore(firstDraftDeadline)) { - throw new GeneralException(CommissionErrorCode.FINAL_DEADLINE_TOO_SOON); - } - return Commission.builder() .instructor(instructor) .planCode(planCode) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java index abaf690..219aded 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java @@ -56,4 +56,18 @@ public class CommissionDraft extends BaseEntity { @Builder.Default @Column(name = "is_final", nullable = false) private boolean isFinal = false; + + public static CommissionDraft create( + Commission commission, + RevisionRequest revisionRequest, + int round, + String designerComment + ) { + return CommissionDraft.builder() + .commission(commission) + .revisionRequest(revisionRequest) + .round(round) + .designerComment(designerComment) + .build(); + } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java index d80757d..0fddd82 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java @@ -1,8 +1,6 @@ package ditda.backend.domain.common.commission.entity; import ditda.backend.domain.common.commission.entity.enums.RevisionCategory; -import ditda.backend.domain.common.commission.exception.CommissionErrorCode; -import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -50,11 +48,6 @@ public static RevisionDetail create( RevisionCategory category, String comment ) { - - if (comment == null || comment.isBlank()) { - throw new GeneralException(CommissionErrorCode.REVISION_COMMENT_REQUIRED); - } - return RevisionDetail.builder() .revisionRequest(revisionRequest) .category(category) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java index fdcfd2f..3989eb5 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java @@ -45,4 +45,11 @@ public class RevisionRequest extends BaseEntity { @Column(name = "round", nullable = false) private int round; + + public static RevisionRequest create(Commission commission, int round) { + return RevisionRequest.builder() + .commission(commission) + .round(round) + .build(); + } } diff --git a/src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java deleted file mode 100644 index 25b4d2d..0000000 --- a/src/main/java/ditda/backend/domain/common/commission/exception/CommissionErrorCode.java +++ /dev/null @@ -1,25 +0,0 @@ -package ditda.backend.domain.common.commission.exception; - -import org.springframework.http.HttpStatus; - -import ditda.backend.global.apipayload.code.BaseErrorCode; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum CommissionErrorCode implements BaseErrorCode { - - // === 외주 생성 검증 === - FIRST_DRAFT_DEADLINE_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_01", "1차 시안 마감일은 필수입니다."), - FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_02", "1차 시안 마감일은 오늘로부터 최소 10일 뒤여야 합니다."), - FINAL_DEADLINE_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_03", "최종 마감일은 필수입니다."), - FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "최종 마감일은 1차 시안 마감일로부터 최소 14일 뒤여야 합니다."), - - // === 수정 요청 검증 === - REVISION_COMMENT_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "수정 요청 코멘트는 비어있을 수 없습니다."); - - private final HttpStatus httpStatus; - private final String code; - private final String message; -} From d33b4ebfbce102e3145b2539bb764f4076ce57be Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Fri, 29 May 2026 00:10:29 +0900 Subject: [PATCH 012/102] =?UTF-8?q?Fix(commission):=20Size=20Enum=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(B6=20->=20KUKBAE(=EA=B5=AD=EB=B0=B0?= =?UTF-8?q?=ED=8C=90))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/common/commission/entity/enums/Size.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java index 455cc59..b0291b6 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java @@ -3,6 +3,6 @@ public enum Size { A4, B5, - B6, + KUKBAE, // 188×257mm (국배판) A5 } From 998153af377c759a703f5c8e2cf69d94225e0245 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Fri, 29 May 2026 00:13:16 +0900 Subject: [PATCH 013/102] =?UTF-8?q?Rename(commission):=20Enum=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/enums/{DraftStatus.java => WatermarkStatus.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/ditda/backend/domain/common/commission/entity/enums/{DraftStatus.java => WatermarkStatus.java} (86%) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/DraftStatus.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/WatermarkStatus.java similarity index 86% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/DraftStatus.java rename to src/main/java/ditda/backend/domain/common/commission/entity/enums/WatermarkStatus.java index e9f232d..e011bdc 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/DraftStatus.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/WatermarkStatus.java @@ -1,6 +1,6 @@ package ditda.backend.domain.common.commission.entity.enums; -public enum DraftStatus { +public enum WatermarkStatus { PROCESSING, // 워터마크 처리 중 COMPLETED, // 워터마크 처리 완료 및 강사 조회 가능 FAILED // 처리 실패 From d513abaa3991195442154fc83b3befbac4c153b2 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Fri, 29 May 2026 00:13:44 +0900 Subject: [PATCH 014/102] =?UTF-8?q?Rename(commission):=20=EC=99=B8?= =?UTF-8?q?=EC=A3=BC=20=EA=B4=80=EB=A0=A8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/commission/entity/Commission.java | 7 ++++++- ...ge.java => CommissionApplicationFile.java} | 20 +++++++++---------- ...raftPage.java => CommissionDraftFile.java} | 20 +++++++++---------- ...ionRequiredPage.java => TextbookPage.java} | 10 +++++----- .../ApplicationDraftPageRepository.java | 8 -------- .../CommissionApplicationFileRepository.java | 8 ++++++++ ...ava => CommissionDraftFileRepository.java} | 4 ++-- .../CommissionRequiredPageRepository.java | 8 -------- .../repository/TextbookPageRepository.java | 8 ++++++++ 9 files changed, 49 insertions(+), 44 deletions(-) rename src/main/java/ditda/backend/domain/common/commission/entity/{ApplicationDraftPage.java => CommissionApplicationFile.java} (77%) rename src/main/java/ditda/backend/domain/common/commission/entity/{CommissionDraftPage.java => CommissionDraftFile.java} (78%) rename src/main/java/ditda/backend/domain/common/commission/entity/{CommissionRequiredPage.java => TextbookPage.java} (88%) delete mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java rename src/main/java/ditda/backend/domain/common/commission/repository/{CommissionDraftPageRepository.java => CommissionDraftFileRepository.java} (63%) delete mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/TextbookPageRepository.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java index 415ce02..52da2b5 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java @@ -35,7 +35,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Commission extends BaseEntity { - // 시안 지원 제출 마감 ~ 1치 시안 마감 일수 + // 시안 지원 제출 마감 ~ 1차 시안 마감 일수 private static final int APPLICATION_DEADLINE_OFFSET_DAYS = 7; @Id @@ -76,6 +76,7 @@ public class Commission extends BaseEntity { @Column(name = "first_draft_deadline", nullable = false) private LocalDate firstDraftDeadline; + // 디자이너 지원 선착순 마감일 @Column(name = "application_deadline", nullable = false) private LocalDate applicationDeadline; @@ -89,6 +90,10 @@ public class Commission extends BaseEntity { @Column(name = "max_revision", nullable = false) private int maxRevision; + @Builder.Default + @Column(name = "revision_count", nullable = false) + private int revisionCount = 0; + public static Commission create( Instructor instructor, PlanCode planCode, diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplicationFile.java similarity index 77% rename from src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java rename to src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplicationFile.java index 8423f34..51ec04b 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/ApplicationDraftPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplicationFile.java @@ -1,6 +1,6 @@ package ditda.backend.domain.common.commission.entity; -import ditda.backend.domain.common.commission.entity.enums.DraftStatus; +import ditda.backend.domain.common.commission.entity.enums.WatermarkStatus; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -23,10 +23,10 @@ @Entity // 한 시안의 페이지 순서 중복 X @Table( - name = "application_draft_pages", + name = "commission_application_files", uniqueConstraints = { @UniqueConstraint( - name = "uk_application_draft_page_order", + name = "uk_commission_application_file_order", columnNames = {"commission_application_id", "page_order"} ) } @@ -35,11 +35,11 @@ @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ApplicationDraftPage extends BaseEntity { +public class CommissionApplicationFile extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "application_draft_page_id") + @Column(name = "commission_application_file_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @@ -59,21 +59,21 @@ public class ApplicationDraftPage extends BaseEntity { private String watermarkedFileUrl; @Enumerated(EnumType.STRING) - @Column(name = "draft_status", length = 20, nullable = false) - private DraftStatus draftStatus; + @Column(name = "watermark_status", length = 20, nullable = false) + private WatermarkStatus watermarkStatus; - public static ApplicationDraftPage create( + public static CommissionApplicationFile create( CommissionApplication commissionApplication, int pageOrder, String fileName, String fileUrl ) { - return ApplicationDraftPage.builder() + return CommissionApplicationFile.builder() .commissionApplication(commissionApplication) .pageOrder(pageOrder) .fileName(fileName) .fileUrl(fileUrl) - .draftStatus(DraftStatus.PROCESSING) + .watermarkStatus(WatermarkStatus.PROCESSING) .build(); } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java similarity index 78% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java rename to src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java index edcd71f..8e005dd 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java @@ -1,6 +1,6 @@ package ditda.backend.domain.common.commission.entity; -import ditda.backend.domain.common.commission.entity.enums.DraftStatus; +import ditda.backend.domain.common.commission.entity.enums.WatermarkStatus; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -23,10 +23,10 @@ @Entity // 한 시안의 페이지 순서 중복 X @Table( - name = "commission_draft_pages", + name = "commission_draft_files", uniqueConstraints = { @UniqueConstraint( - name = "uk_commission_draft_page_order", + name = "uk_commission_draft_file_order", columnNames = {"commission_draft_id", "page_order"} ) } @@ -35,11 +35,11 @@ @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CommissionDraftPage extends BaseEntity { +public class CommissionDraftFile extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "commission_draft_page_id") + @Column(name = "commission_draft_file_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @@ -59,21 +59,21 @@ public class CommissionDraftPage extends BaseEntity { private String watermarkedFileUrl; @Enumerated(EnumType.STRING) - @Column(name = "draft_status", length = 20, nullable = false) - private DraftStatus draftStatus; + @Column(name = "watermark_status", length = 20, nullable = false) + private WatermarkStatus watermarkStatus; - public static CommissionDraftPage create( + public static CommissionDraftFile create( CommissionDraft commissionDraft, int pageOrder, String fileName, String fileUrl ) { - return CommissionDraftPage.builder() + return CommissionDraftFile.builder() .commissionDraft(commissionDraft) .pageOrder(pageOrder) .fileName(fileName) .fileUrl(fileUrl) - .draftStatus(DraftStatus.PROCESSING) + .watermarkStatus(WatermarkStatus.PROCESSING) .build(); } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java b/src/main/java/ditda/backend/domain/common/commission/entity/TextbookPage.java similarity index 88% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java rename to src/main/java/ditda/backend/domain/common/commission/entity/TextbookPage.java index 3977b35..5c12ffb 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionRequiredPage.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/TextbookPage.java @@ -22,7 +22,7 @@ @Entity // 같은 페이지 종류 중복 X @Table( - name = "commission_required_pages", + name = "textbook_pages", uniqueConstraints = { @UniqueConstraint( name = "uk_commission_required_page_type", @@ -34,11 +34,11 @@ @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CommissionRequiredPage { +public class TextbookPage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "commission_required_page_id") + @Column(name = "textbook_page_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @@ -52,12 +52,12 @@ public class CommissionRequiredPage { @Column(name = "page_description", length = 150) private String pageDescription; - public static CommissionRequiredPage create( + public static TextbookPage create( Commission commission, PageType pageType, String pageDescription ) { - return CommissionRequiredPage.builder() + return TextbookPage.builder() .commission(commission) .pageType(pageType) .pageDescription(pageDescription) diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java deleted file mode 100644 index 24d47fa..0000000 --- a/src/main/java/ditda/backend/domain/common/commission/repository/ApplicationDraftPageRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package ditda.backend.domain.common.commission.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import ditda.backend.domain.common.commission.entity.ApplicationDraftPage; - -public interface ApplicationDraftPageRepository extends JpaRepository { -} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java new file mode 100644 index 0000000..adabfde --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.CommissionApplicationFile; + +public interface CommissionApplicationFileRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftPageRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftFileRepository.java similarity index 63% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftPageRepository.java rename to src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftFileRepository.java index fb04ce3..b468cbc 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftPageRepository.java +++ b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftFileRepository.java @@ -2,7 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.CommissionDraftPage; +import ditda.backend.domain.common.commission.entity.CommissionDraftFile; -public interface CommissionDraftPageRepository extends JpaRepository { +public interface CommissionDraftFileRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java deleted file mode 100644 index 9e88bf8..0000000 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRequiredPageRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package ditda.backend.domain.common.commission.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import ditda.backend.domain.common.commission.entity.CommissionRequiredPage; - -public interface CommissionRequiredPageRepository extends JpaRepository { -} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/TextbookPageRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/TextbookPageRepository.java new file mode 100644 index 0000000..8e401be --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/TextbookPageRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.TextbookPage; + +public interface TextbookPageRepository extends JpaRepository { +} From 1ad8aa0476af809fbed71f9911ae75b2e1629344 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Fri, 29 May 2026 02:28:32 +0900 Subject: [PATCH 015/102] =?UTF-8?q?Refactor(commission):=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20ERD=EC=97=90=20=EB=A7=9E=EA=B2=8C=20Entity=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/CommissionApplicationFile.java | 79 ------------------- .../commission/entity/CommissionDraft.java | 36 ++++----- .../commission/entity/RevisionResponse.java | 53 +++++++++++++ .../CommissionApplicationFileRepository.java | 8 -- .../RevisionResponseRepository.java | 8 ++ 5 files changed, 78 insertions(+), 106 deletions(-) delete mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplicationFile.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/RevisionResponse.java delete mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java create mode 100644 src/main/java/ditda/backend/domain/common/commission/repository/RevisionResponseRepository.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplicationFile.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplicationFile.java deleted file mode 100644 index 51ec04b..0000000 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplicationFile.java +++ /dev/null @@ -1,79 +0,0 @@ -package ditda.backend.domain.common.commission.entity; - -import ditda.backend.domain.common.commission.entity.enums.WatermarkStatus; -import ditda.backend.global.entity.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -// 한 시안의 페이지 순서 중복 X -@Table( - name = "commission_application_files", - uniqueConstraints = { - @UniqueConstraint( - name = "uk_commission_application_file_order", - columnNames = {"commission_application_id", "page_order"} - ) - } -) -@Getter -@Builder -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CommissionApplicationFile extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "commission_application_file_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "commission_application_id", nullable = false) - private CommissionApplication commissionApplication; - - @Column(name = "page_order", nullable = false) - private int pageOrder; - - @Column(name = "file_name", length = 100, nullable = false) - private String fileName; - - @Column(name = "file_url", nullable = false) - private String fileUrl; - - @Column(name = "watermarked_file_url") - private String watermarkedFileUrl; - - @Enumerated(EnumType.STRING) - @Column(name = "watermark_status", length = 20, nullable = false) - private WatermarkStatus watermarkStatus; - - public static CommissionApplicationFile create( - CommissionApplication commissionApplication, - int pageOrder, - String fileName, - String fileUrl - ) { - return CommissionApplicationFile.builder() - .commissionApplication(commissionApplication) - .pageOrder(pageOrder) - .fileName(fileName) - .fileUrl(fileUrl) - .watermarkStatus(WatermarkStatus.PROCESSING) - .build(); - } -} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java index 219aded..c8f335a 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java @@ -18,13 +18,13 @@ import lombok.NoArgsConstructor; @Entity -// 한 외주에 같은 라운드 시안 1개만 +// 한 지원에 같은 라운드 시안 1개만 @Table( name = "commission_drafts", uniqueConstraints = { @UniqueConstraint( name = "uk_commission_draft_round", - columnNames = {"commission_id", "round"} + columnNames = {"commission_application_id", "round"} ) } ) @@ -40,34 +40,32 @@ public class CommissionDraft extends BaseEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "commission_id", nullable = false) - private Commission commission; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "revision_request_id") - private RevisionRequest revisionRequest; + @JoinColumn(name = "commission_application_id", nullable = false) + private CommissionApplication commissionApplication; @Column(name = "round", nullable = false) private int round; - @Column(name = "designer_comment", length = 500) - private String designerComment; - @Builder.Default @Column(name = "is_final", nullable = false) private boolean isFinal = false; - public static CommissionDraft create( - Commission commission, - RevisionRequest revisionRequest, - int round, - String designerComment + // 1차 시안 (디자이너 지원 완료 후 제출, 수정 0회) + public static CommissionDraft createInitial(CommissionApplication commissionApplication) { + return CommissionDraft.builder() + .commissionApplication(commissionApplication) + .round(0) + .build(); + } + + // 수정 시안 (강사의 수정요청에 대한 응답) + public static CommissionDraft createRevision( + CommissionApplication commissionApplication, + int round ) { return CommissionDraft.builder() - .commission(commission) - .revisionRequest(revisionRequest) + .commissionApplication(commissionApplication) .round(round) - .designerComment(designerComment) .build(); } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionResponse.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionResponse.java new file mode 100644 index 0000000..7a5339d --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionResponse.java @@ -0,0 +1,53 @@ +package ditda.backend.domain.common.commission.entity; + +import ditda.backend.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "revision_responses") +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RevisionResponse extends BaseEntity { + + @Id + @Column(name = "revision_request_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "revision_request_id") + private RevisionRequest revisionRequest; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commission_draft_id", nullable = false, unique = true) + private CommissionDraft commissionDraft; + + @Column(name = "designer_comment", length = 500, nullable = false) + private String designerComment; + + public static RevisionResponse create( + RevisionRequest revisionRequest, + CommissionDraft commissionDraft, + String designerComment + ) { + return RevisionResponse.builder() + .revisionRequest(revisionRequest) + .commissionDraft(commissionDraft) + .designerComment(designerComment) + .build(); + } +} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java deleted file mode 100644 index adabfde..0000000 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationFileRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package ditda.backend.domain.common.commission.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import ditda.backend.domain.common.commission.entity.CommissionApplicationFile; - -public interface CommissionApplicationFileRepository extends JpaRepository { -} diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionResponseRepository.java b/src/main/java/ditda/backend/domain/common/commission/repository/RevisionResponseRepository.java new file mode 100644 index 0000000..be1bffa --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/repository/RevisionResponseRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.common.commission.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.common.commission.entity.RevisionResponse; + +public interface RevisionResponseRepository extends JpaRepository { +} From b36e5c13118454313080ab82cc454f0ccd8c6942 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 1 Jun 2026 12:38:33 +0900 Subject: [PATCH 016/102] =?UTF-8?q?Refactor(commission):=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=9A=8C=EC=B0=A8=20=EA=B4=80=EB=A6=AC=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/commission/entity/Commission.java | 6 +--- .../entity/CommissionDraftFile.java | 10 +++--- .../commission/entity/RevisionRequest.java | 15 +++++---- .../entity/enums/ConceptCategory.java | 16 ++++++++++ .../commission/entity/enums/ConceptTag.java | 32 ++++++++++++------- .../commission/entity/enums/PlanCode.java | 11 ++----- 6 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptCategory.java diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java index 52da2b5..af3eb38 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java @@ -90,10 +90,6 @@ public class Commission extends BaseEntity { @Column(name = "max_revision", nullable = false) private int maxRevision; - @Builder.Default - @Column(name = "revision_count", nullable = false) - private int revisionCount = 0; - public static Commission create( Instructor instructor, PlanCode planCode, @@ -117,7 +113,7 @@ public static Commission create( .applicationDeadline(firstDraftDeadline.minusDays(APPLICATION_DEADLINE_OFFSET_DAYS)) .finalDeadline(finalDeadline) .status(CommissionStatus.PENDING) - .maxRevision(planCode.getMaxRevision()) + .maxRevision(planCode.getBaseRevision()) .build(); } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java index 8e005dd..2c7ae63 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java @@ -27,7 +27,7 @@ uniqueConstraints = { @UniqueConstraint( name = "uk_commission_draft_file_order", - columnNames = {"commission_draft_id", "page_order"} + columnNames = {"commission_draft_id", "file_order"} ) } ) @@ -46,8 +46,8 @@ public class CommissionDraftFile extends BaseEntity { @JoinColumn(name = "commission_draft_id", nullable = false) private CommissionDraft commissionDraft; - @Column(name = "page_order", nullable = false) - private int pageOrder; + @Column(name = "file_order", nullable = false) + private int fileOrder; @Column(name = "file_name", length = 100, nullable = false) private String fileName; @@ -64,13 +64,13 @@ public class CommissionDraftFile extends BaseEntity { public static CommissionDraftFile create( CommissionDraft commissionDraft, - int pageOrder, + int fileOrder, String fileName, String fileUrl ) { return CommissionDraftFile.builder() .commissionDraft(commissionDraft) - .pageOrder(pageOrder) + .fileOrder(fileOrder) .fileName(fileName) .fileUrl(fileUrl) .watermarkStatus(WatermarkStatus.PROCESSING) diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java index 3989eb5..68a584a 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java @@ -18,13 +18,13 @@ import lombok.NoArgsConstructor; @Entity -// 한 외주에 같은 라운드 1개만 +// 한 시안에 대한 수정요청 1개만 @Table( name = "revision_requests", uniqueConstraints = { @UniqueConstraint( - name = "uk_revision_request_round", - columnNames = {"commission_id", "round"} + name = "uk_revision_request_target_draft", + columnNames = {"target_draft_id"} ) } ) @@ -43,13 +43,14 @@ public class RevisionRequest extends BaseEntity { @JoinColumn(name = "commission_id", nullable = false) private Commission commission; - @Column(name = "round", nullable = false) - private int round; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "target_draft_id", nullable = false) + private CommissionDraft targetDraft; - public static RevisionRequest create(Commission commission, int round) { + public static RevisionRequest create(Commission commission, CommissionDraft targetDraft) { return RevisionRequest.builder() .commission(commission) - .round(round) + .targetDraft(targetDraft) .build(); } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptCategory.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptCategory.java new file mode 100644 index 0000000..12e8c63 --- /dev/null +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptCategory.java @@ -0,0 +1,16 @@ +package ditda.backend.domain.common.commission.entity.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ConceptCategory { + BRIGHT("밝은"), + SOFT("부드러운"), + LUXURIOUS("고급스러운"), + INTENSE("강렬한"), + NEAT("단정한"); + + private final String description; +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java index 33baa08..5731321 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java @@ -1,21 +1,29 @@ package ditda.backend.domain.common.commission.entity.enums; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor public enum ConceptTag { // 밝은 - CUTE, - LIGHT, - CLEAR, + CUTE(ConceptCategory.BRIGHT, "귀여운"), + LIGHT(ConceptCategory.BRIGHT, "경쾌한"), + CLEAR(ConceptCategory.BRIGHT, "맑은"), // 부드러운 - NATURAL, - SUBTLE, - WARM, + NATURAL(ConceptCategory.SOFT, "내츄럴한"), + SUBTLE(ConceptCategory.SOFT, "은은한"), + WARM(ConceptCategory.SOFT, "온화한"), // 고급스러운 - ELEGANT, - REFINED, - MODERN, + ELEGANT(ConceptCategory.LUXURIOUS, "우아한"), + REFINED(ConceptCategory.LUXURIOUS, "고상한"), + MODERN(ConceptCategory.LUXURIOUS, "모던한"), // 강렬한 - VIVID, - DYNAMIC, + VIVID(ConceptCategory.INTENSE, "화려한"), + DYNAMIC(ConceptCategory.INTENSE, "다이나믹한"), // 단정한 - CALM + CALM(ConceptCategory.NEAT, "점잖은"); + + private final ConceptCategory category; + private final String description; } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java index efa639d..6e112b3 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java +++ b/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java @@ -1,8 +1,10 @@ package ditda.backend.domain.common.commission.entity.enums; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter +@RequiredArgsConstructor public enum PlanCode { BASIC(3, 400000, 3, "디자이너 3명에 대한 시안을 받아볼 수 있습니다."), @@ -11,13 +13,6 @@ public enum PlanCode { private final int designerCount; private final int price; - private final int maxRevision; + private final int baseRevision; private final String description; - - PlanCode(int designerCount, int price, int maxRevision, String description) { - this.designerCount = designerCount; - this.price = price; - this.maxRevision = maxRevision; - this.description = description; - } } From 80ebc028bf217ac870152836b4165ab87a8c40be Mon Sep 17 00:00:00 2001 From: fervovita Date: Mon, 1 Jun 2026 12:58:46 +0900 Subject: [PATCH 017/102] =?UTF-8?q?Feat(designer):=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=ED=8F=B4=EB=A6=AC=EC=98=A4=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=EB=A5=BC=20presigned=20URL=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DesignerAuthController.java | 41 +++---- .../dto/request/DesignerSignupRequest.java | 7 +- .../dto/request/PortfolioPresignRequest.java | 19 ++++ .../response/PortfolioPresignResponse.java | 13 +++ .../auth/facade/DesignerAuthFacade.java | 19 +++- .../auth/service/PortfolioService.java | 79 ++++++++------ .../backend/global/config/SecurityConfig.java | 2 +- .../backend/global/s3/S3FileManager.java | 56 ++++++++++ .../backend/global/s3/S3FileUploader.java | 100 ------------------ .../global/s3/S3PresignedUrlGenerator.java | 22 ++++ .../ditda/backend/global/s3/S3Properties.java | 3 + .../s3/exception/S3UploadException.java | 8 -- src/main/resources/application.yaml | 7 +- 13 files changed, 200 insertions(+), 176 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/designer/auth/dto/request/PortfolioPresignRequest.java create mode 100644 src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java create mode 100644 src/main/java/ditda/backend/global/s3/S3FileManager.java delete mode 100644 src/main/java/ditda/backend/global/s3/S3FileUploader.java delete mode 100644 src/main/java/ditda/backend/global/s3/exception/S3UploadException.java diff --git a/src/main/java/ditda/backend/domain/designer/auth/controller/DesignerAuthController.java b/src/main/java/ditda/backend/domain/designer/auth/controller/DesignerAuthController.java index d697861..7d0993c 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/controller/DesignerAuthController.java +++ b/src/main/java/ditda/backend/domain/designer/auth/controller/DesignerAuthController.java @@ -1,26 +1,21 @@ package ditda.backend.domain.designer.auth.controller; -import java.util.List; - import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; import ditda.backend.domain.designer.auth.dto.DesignerAuthResult; import ditda.backend.domain.designer.auth.dto.request.DesignerSignupRequest; +import ditda.backend.domain.designer.auth.dto.request.PortfolioPresignRequest; import ditda.backend.domain.designer.auth.dto.response.DesignerSignupResponse; +import ditda.backend.domain.designer.auth.dto.response.PortfolioPresignResponse; import ditda.backend.domain.designer.auth.facade.DesignerAuthFacade; import ditda.backend.domain.designer.auth.mapper.DesignerAuthResponseMapper; import ditda.backend.global.apipayload.response.ApiResponse; import ditda.backend.global.jwt.utils.CookieUtils; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Encoding; -import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -37,23 +32,29 @@ public class DesignerAuthController { private final DesignerAuthResponseMapper authResponseMapper; @Operation( - summary = "디자이너 회원가입", - description = "**[회원가입]** 회원가입 후 자동 로그인 처리됩니다. `data`(JSON) + `portfolioFiles`(파일 최대 3개, 각 30MB)" + summary = "포트폴리오 업로드 URL 발급", + description = "**[회원가입]** 이메일 인증 완료 후, 포트폴리오 파일을 S3에 올릴 presigned PUT URL을 발급합니다. " + + "PUT URL로 S3에 직접 업로드한 뒤, 응답의 key를 회원가입 요청의 portfolioKeys에 담아 전송합니다." ) - @RequestBody( - content = @Content( - mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, - encoding = @Encoding(name = "data", contentType = MediaType.APPLICATION_JSON_VALUE) - ) + @PostMapping("/signup/portfolio/presigned-url") + public ApiResponse issuePortfolioPresignedUrls( + @Valid @RequestBody PortfolioPresignRequest request + ) { + PortfolioPresignResponse response = designerAuthFacade.issuePortfolioPresignedUrl(request); + + return ApiResponse.onSuccess("포트폴리오 업로드 URL 발급 성공", response); + } + + @Operation( + summary = "디자이너 회원가입", + description = "**[회원가입]** 회원가입 후 자동 로그인 처리됩니다. presigned PUT URL로 업로드한 포트폴리오 key를 portfolioKeys에 담아 전송합니다." ) - @PostMapping(value = "/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/signup") public ApiResponse signup( - @Valid @RequestPart("data") DesignerSignupRequest request, - @RequestPart(value = "portfolioFiles", required = false) List portfolioFiles, + @Valid @RequestBody DesignerSignupRequest request, HttpServletResponse response ) { - - DesignerAuthResult result = designerAuthFacade.signup(request, portfolioFiles); + DesignerAuthResult result = designerAuthFacade.signup(request); addRefreshTokenCookie(response, result.refreshToken()); diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java b/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java index 11339de..5afdfbb 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java +++ b/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java @@ -84,7 +84,12 @@ public record DesignerSignupRequest( @NotNull(message = "계좌 정보는 필수입니다") @Valid - BankAccountRequest bankAccount + BankAccountRequest bankAccount, + + @Schema(description = "presigned URL로 업로드한 포트폴리오 key 목록 (최대 3개)", + example = "[\"portfolio/3f2b....pdf\", \"portfolio/9a1c....png\"]") + @Size(max = 3, message = "포트폴리오 파일은 최대 3개까지 업로드 가능합니다.") + List<@NotBlank(message = "포트폴리오 key는 비어 있을 수 없습니다.") String> portfolioKeys ) { public record TermRequest( diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/request/PortfolioPresignRequest.java b/src/main/java/ditda/backend/domain/designer/auth/dto/request/PortfolioPresignRequest.java new file mode 100644 index 0000000..bb60393 --- /dev/null +++ b/src/main/java/ditda/backend/domain/designer/auth/dto/request/PortfolioPresignRequest.java @@ -0,0 +1,19 @@ +package ditda.backend.domain.designer.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "포트폴리오 presigned URL 발급 요청") +public record PortfolioPresignRequest( + + @Schema(description = "인증 완료된 이메일", example = "testid@gmail.com") + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "올바른 이메일 형식이 아닙니다.") + String email, + + @Schema(description = "파일 Content-Type", example = "application/pdf") + @NotBlank(message = "Content-Type은 필수입니다.") + String contentType +) { +} diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java b/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java new file mode 100644 index 0000000..15243d4 --- /dev/null +++ b/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java @@ -0,0 +1,13 @@ +package ditda.backend.domain.designer.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record PortfolioPresignResponse( + + @Schema(description = "S3 객체 key", example = "portfolio/5a5237a7-674a-47c2-91ca-6fdb28ba6b2d.pdf") + String key, + + @Schema(description = "S3 직접 업드드용 presigned PUT URL", example = "\"https://{bucket}.s3.{region}.amazonaws.com/portfolio/{uuid}.pdf?X-Amz-Signature=...") + String presignedUrl +) { +} diff --git a/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java b/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java index 0e379e2..f4cf9d0 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java +++ b/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java @@ -3,12 +3,13 @@ import java.util.List; import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; import ditda.backend.domain.common.auth.service.EmailVerificationService; import ditda.backend.domain.common.user.service.UserService; import ditda.backend.domain.designer.auth.dto.DesignerAuthResult; import ditda.backend.domain.designer.auth.dto.request.DesignerSignupRequest; +import ditda.backend.domain.designer.auth.dto.request.PortfolioPresignRequest; +import ditda.backend.domain.designer.auth.dto.response.PortfolioPresignResponse; import ditda.backend.domain.designer.auth.service.DesignerAuthService; import ditda.backend.domain.designer.auth.service.PortfolioService; import lombok.RequiredArgsConstructor; @@ -22,7 +23,15 @@ public class DesignerAuthFacade { private final PortfolioService portfolioService; private final UserService userService; - public DesignerAuthResult signup(DesignerSignupRequest request, List portfolioFiles) { + public PortfolioPresignResponse issuePortfolioPresignedUrl(PortfolioPresignRequest request) { + + // 이메일 인증 여부 확인 + emailVerificationService.validateVerified(request.email()); + + return portfolioService.generatePresignedUpload(request.contentType()); + } + + public DesignerAuthResult signup(DesignerSignupRequest request) { // 이메일 인증 여부 확인 emailVerificationService.validateVerified(request.email()); @@ -31,9 +40,9 @@ public DesignerAuthResult signup(DesignerSignupRequest request, List portfolioKeys = portfolioService.uploadFiles(portfolioFiles); + // 포트폴리오 key 검증 + List portfolioKeys = request.portfolioKeys() == null ? List.of() : request.portfolioKeys(); + portfolioService.validateKeys(portfolioKeys); // 회원가입 DesignerAuthResult result; diff --git a/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java b/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java index be4fefe..5a2d254 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java +++ b/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java @@ -1,22 +1,22 @@ package ditda.backend.domain.designer.auth.service; import java.util.List; -import java.util.Set; +import java.util.Map; +import java.util.UUID; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.unit.DataSize; -import org.springframework.web.multipart.MultipartFile; +import ditda.backend.domain.designer.auth.dto.response.PortfolioPresignResponse; import ditda.backend.domain.designer.auth.entity.Designer; import ditda.backend.domain.designer.auth.entity.Portfolio; import ditda.backend.domain.designer.auth.exception.DesignerErrorCode; import ditda.backend.domain.designer.auth.repository.PortfolioRepository; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.S3FileUploader; +import ditda.backend.global.s3.S3FileManager; +import ditda.backend.global.s3.S3PresignedUrlGenerator; +import ditda.backend.global.s3.S3Properties; import ditda.backend.global.s3.enums.BucketType; -import ditda.backend.global.s3.exception.S3UploadException; import lombok.RequiredArgsConstructor; @Service @@ -25,48 +25,56 @@ public class PortfolioService { private static final int MAX_PORTFOLIO_COUNT = 3; private static final String S3_KEY_PREFIX = "portfolio"; - private static final Set ALLOWED_CONTENT_TYPES = Set.of( - "application/pdf", - "image/png" + private static final Map ALLOWED_CONTENT_TYPES = Map.of( + "application/pdf", ".pdf", + "image/png", ".png" ); - private final S3FileUploader s3FileUploader; + private final S3PresignedUrlGenerator s3PresignedUrlGenerator; + private final S3FileManager s3FileManager; private final PortfolioRepository portfolioRepository; + private final S3Properties s3Properties; - @Value("${spring.servlet.multipart.max-file-size}") - private DataSize maxFileSize; + public PortfolioPresignResponse generatePresignedUpload(String contentType) { - public void validateFiles(List files) { - if (files == null || files.isEmpty()) { - return; + // 파일 타입 검증 + String extension = ALLOWED_CONTENT_TYPES.get(contentType); + if (extension == null) { + throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } - // 파일 최대 개수 검증 - long realCount = files.stream().filter(f -> !f.isEmpty()).count(); - if (realCount > MAX_PORTFOLIO_COUNT) { + String key = "%s/%s%s".formatted(S3_KEY_PREFIX, UUID.randomUUID(), extension); + String presignedUrl = s3PresignedUrlGenerator.generatePrivatePutUrl(key, contentType); + + return new PortfolioPresignResponse(key, presignedUrl); + } + + public void validateKeys(List keys) { + + // 파일 개수 검증 + if (keys.size() > MAX_PORTFOLIO_COUNT) { throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_LIMIT_EXCEEDED); } - // 파일 사이즈 및 타입 검증 - for (MultipartFile file : files) { - if (file.isEmpty()) { - continue; - } + // distinct key 검증 + if (keys.size() != keys.stream().distinct().count()) { + throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); + } - if (file.getSize() > maxFileSize.toBytes()) { - throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_SIZE_EXCEEDED); - } - if (!ALLOWED_CONTENT_TYPES.contains(file.getContentType())) { + for (String key : keys) { + // 파일 key 형식 검증 + if (key == null || !key.startsWith(S3_KEY_PREFIX + "/")) { throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } - } - } - public List uploadFiles(List files) { - try { - return s3FileUploader.uploadAll(BucketType.PRIVATE, S3_KEY_PREFIX, files); - } catch (S3UploadException e) { - throw new GeneralException(DesignerErrorCode.PORTFOLIO_UPLOAD_FAILED); + // 파일 크기 검증 + Long size = s3FileManager.getObjectSize(BucketType.PRIVATE, key); + if (size == null) { + throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); // 미업로드 key + } + if (size > s3Properties.getMaxFileSize().toBytes()) { + throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_SIZE_EXCEEDED); + } } } @@ -84,6 +92,7 @@ public void savePortfolios(Designer designer, List portfolioKeys) { } public void deleteFiles(List portfolioKeys) { - s3FileUploader.deleteAll(BucketType.PRIVATE, portfolioKeys); + s3FileManager.deleteAll(BucketType.PRIVATE, portfolioKeys); } + } diff --git a/src/main/java/ditda/backend/global/config/SecurityConfig.java b/src/main/java/ditda/backend/global/config/SecurityConfig.java index 89af4d2..29d3188 100644 --- a/src/main/java/ditda/backend/global/config/SecurityConfig.java +++ b/src/main/java/ditda/backend/global/config/SecurityConfig.java @@ -59,7 +59,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() .requestMatchers(HttpMethod.POST, "/api/v1/auth/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/v1/instructors/auth/signup").permitAll() - .requestMatchers(HttpMethod.POST, "/api/v1/designers/auth/signup").permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1/designers/auth/signup/**").permitAll() .requestMatchers(HttpMethod.GET, "/actuator/health").permitAll() .anyRequest().authenticated() ) diff --git a/src/main/java/ditda/backend/global/s3/S3FileManager.java b/src/main/java/ditda/backend/global/s3/S3FileManager.java new file mode 100644 index 0000000..ae98c71 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/S3FileManager.java @@ -0,0 +1,56 @@ +package ditda.backend.global.s3; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import ditda.backend.global.s3.enums.BucketType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.S3Exception; + +@Slf4j +@Component +@RequiredArgsConstructor +public class S3FileManager { + + private final S3Client s3Client; + private final S3Properties s3Properties; + + public Long getObjectSize(BucketType bucketType, String key) { + String bucket = resolveBucket(bucketType); + + try { + return s3Client.headObject(req -> req.bucket(bucket).key(key)).contentLength(); + } catch (S3Exception e) { + if (e.statusCode() == 404) { // 객체 없음 오류 + return null; + } + + throw e; + } + } + + public void deleteAll(BucketType bucketType, List keys) { + if (keys == null || keys.isEmpty()) { + return; + } + + String bucket = resolveBucket(bucketType); + for (String key : keys) { + try { + s3Client.deleteObject(req -> req.bucket(bucket).key(key)); + } catch (Exception e) { + log.error("S3 delete failed. bucket={}, key={}", bucket, key, e); + } + } + } + + private String resolveBucket(BucketType bucketType) { + return switch (bucketType) { + case PUBLIC -> s3Properties.getPublicBucket(); + case PRIVATE -> s3Properties.getPrivateBucket(); + }; + } +} diff --git a/src/main/java/ditda/backend/global/s3/S3FileUploader.java b/src/main/java/ditda/backend/global/s3/S3FileUploader.java deleted file mode 100644 index 12a1629..0000000 --- a/src/main/java/ditda/backend/global/s3/S3FileUploader.java +++ /dev/null @@ -1,100 +0,0 @@ -package ditda.backend.global.s3; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -import ditda.backend.global.s3.enums.BucketType; -import ditda.backend.global.s3.exception.S3UploadException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import software.amazon.awssdk.core.exception.SdkException; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.services.s3.S3Client; - -@Slf4j -@Component -@RequiredArgsConstructor -public class S3FileUploader { - - private final S3Client s3Client; - private final S3Properties s3Properties; - - public String upload(BucketType bucketType, String prefix, MultipartFile file) { - String bucket = resolveBucket(bucketType); - String key = generateKey(prefix, file); - - try { - s3Client.putObject( - req -> req.bucket(bucket) - .key(key) - .contentType(file.getContentType()), - RequestBody.fromInputStream(file.getInputStream(), file.getSize()) - ); - return key; - } catch (IOException | SdkException e) { - log.error("Failed to upload file to S3. bucket={}, originalName={}", bucket, file.getOriginalFilename(), e); - throw new S3UploadException(file.getOriginalFilename(), e); - } - } - - public List uploadAll(BucketType bucketType, String prefix, List files) { - if (files == null || files.isEmpty()) { - return List.of(); - } - - List uploadedKeys = new ArrayList<>(); - try { - for (MultipartFile file : files) { - if (file.isEmpty()) { - continue; - } - uploadedKeys.add(upload(bucketType, prefix, file)); - } - return uploadedKeys; - } catch (Exception e) { - // 부분 업로드된 파일 보상 삭제 - deleteAll(bucketType, uploadedKeys); - throw e; - } - } - - public void deleteAll(BucketType bucketType, List keys) { - if (keys == null || keys.isEmpty()) { - return; - } - - String bucket = resolveBucket(bucketType); - for (String key : keys) { - try { - s3Client.deleteObject(req -> req.bucket(bucket).key(key)); - } catch (Exception e) { - log.error("S3 delete failed. bucket={}, key={}", bucket, key, e); - } - } - } - - private String resolveBucket(BucketType bucketType) { - return switch (bucketType) { - case PUBLIC -> s3Properties.getPublicBucket(); - case PRIVATE -> s3Properties.getPrivateBucket(); - }; - } - - private String generateKey(String prefix, MultipartFile file) { - String extension = extractExtension(file.getOriginalFilename()); - return "%s/%s%s".formatted(prefix, UUID.randomUUID(), extension); - } - - // 파일명에서 확장자 추출 - private String extractExtension(String filename) { - if (filename == null || !filename.contains(".")) { - return ""; - } - return filename.substring(filename.lastIndexOf('.')); - } -} diff --git a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java index f2d6025..c51fb23 100644 --- a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java +++ b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java @@ -6,9 +6,11 @@ import lombok.RequiredArgsConstructor; import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; @Component @RequiredArgsConstructor @@ -36,4 +38,24 @@ public String generatePrivateGetUrl(String key, Duration ttl) { PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest); return presignedRequest.url().toString(); } + + public String generatePrivatePutUrl(String key, String contentType) { + Duration ttl = Duration.ofMinutes(s3Properties.getPresignedUrlTtlMinutes()); + return generatePrivatePutUrl(key, contentType, ttl); + } + + public String generatePrivatePutUrl(String key, String contentType, Duration ttl) { + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(s3Properties.getPrivateBucket()) + .key(key) + .contentType(contentType) + .build(); + + PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder() + .signatureDuration(ttl) + .putObjectRequest(putObjectRequest) + .build(); + + return s3Presigner.presignPutObject(presignRequest).url().toString(); + } } diff --git a/src/main/java/ditda/backend/global/s3/S3Properties.java b/src/main/java/ditda/backend/global/s3/S3Properties.java index 05d79cb..bb44bf8 100644 --- a/src/main/java/ditda/backend/global/s3/S3Properties.java +++ b/src/main/java/ditda/backend/global/s3/S3Properties.java @@ -2,6 +2,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.util.unit.DataSize; import lombok.Getter; import lombok.Setter; @@ -12,6 +13,8 @@ @ConfigurationProperties(prefix = "app.s3") public class S3Properties { + private DataSize maxFileSize = DataSize.ofMegabytes(30); + private String publicBucket; private String privateBucket; diff --git a/src/main/java/ditda/backend/global/s3/exception/S3UploadException.java b/src/main/java/ditda/backend/global/s3/exception/S3UploadException.java deleted file mode 100644 index 94e2bf0..0000000 --- a/src/main/java/ditda/backend/global/s3/exception/S3UploadException.java +++ /dev/null @@ -1,8 +0,0 @@ -package ditda.backend.global.s3.exception; - -public class S3UploadException extends RuntimeException { - - public S3UploadException(String filename, Throwable cause) { - super("Failed to upload file to S3: " + filename, cause); - } -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 324f3c6..69519e1 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -31,12 +31,6 @@ spring: starttls: enable: true - servlet: - multipart: - max-file-size: 30MB - max-request-size: 100MB - enabled: true - jwt: secret: ${JWT_SECRET_KEY} access-token-expiration: ${JWT_ACCESS_TOKEN_EXPIRATION} @@ -53,6 +47,7 @@ admin: app: s3: + max-file-size: 30MB public-bucket: ${S3_PUBLIC_BUCKET} private-bucket: ${S3_PRIVATE_BUCKET} presigned-url-ttl-minutes: ${S3_PRESIGNED_URL_TTL_MINUTES:10} From 16a6df49fd60c9c8d748bb6b32eff0899b64551a Mon Sep 17 00:00:00 2001 From: fervovita Date: Mon, 1 Jun 2026 18:04:51 +0900 Subject: [PATCH 018/102] =?UTF-8?q?Feat(s3):=20=ED=8F=AC=ED=8A=B8=ED=8F=B4?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=EC=97=85=EB=A1=9C=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C(tmp)=20=EC=A0=80=EC=9E=A5=20=ED=9B=84=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=99=84=EB=A3=8C=EC=8B=9C=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/PortfolioPresignResponse.java | 2 +- .../auth/facade/DesignerAuthFacade.java | 7 ++- .../auth/service/PortfolioService.java | 27 +++++---- .../backend/global/s3/PresignedUpload.java | 4 ++ .../backend/global/s3/S3FileManager.java | 17 +++--- .../global/s3/S3PresignedUrlGenerator.java | 9 +-- .../ditda/backend/global/s3/S3Properties.java | 8 +++ .../backend/global/s3/S3UploadManager.java | 59 +++++++++++++++++++ 8 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/main/java/ditda/backend/global/s3/PresignedUpload.java create mode 100644 src/main/java/ditda/backend/global/s3/S3UploadManager.java diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java b/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java index 15243d4..a87e6a9 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java +++ b/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java @@ -7,7 +7,7 @@ public record PortfolioPresignResponse( @Schema(description = "S3 객체 key", example = "portfolio/5a5237a7-674a-47c2-91ca-6fdb28ba6b2d.pdf") String key, - @Schema(description = "S3 직접 업드드용 presigned PUT URL", example = "\"https://{bucket}.s3.{region}.amazonaws.com/portfolio/{uuid}.pdf?X-Amz-Signature=...") + @Schema(description = "S3 직접 업로드용 presigned PUT URL", example = "https://{bucket}.s3.{region}.amazonaws.com/portfolio/{uuid}.pdf?X-Amz-Signature=...") String presignedUrl ) { } diff --git a/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java b/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java index f4cf9d0..200895d 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java +++ b/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java @@ -41,8 +41,11 @@ public DesignerAuthResult signup(DesignerSignupRequest request) { userService.validateEmailAvailable(request.email()); // 포트폴리오 key 검증 - List portfolioKeys = request.portfolioKeys() == null ? List.of() : request.portfolioKeys(); - portfolioService.validateKeys(portfolioKeys); + List tempKeys = request.portfolioKeys() == null ? List.of() : request.portfolioKeys(); + portfolioService.validateKeys(tempKeys); + + // 임시('portfolio/tmp') -> 정식('portfolio/') 승격 + List portfolioKeys = portfolioService.promote(tempKeys); // 회원가입 DesignerAuthResult result; diff --git a/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java b/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java index 5a2d254..618ed80 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java +++ b/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Map; -import java.util.UUID; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,9 +12,9 @@ import ditda.backend.domain.designer.auth.exception.DesignerErrorCode; import ditda.backend.domain.designer.auth.repository.PortfolioRepository; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.S3FileManager; -import ditda.backend.global.s3.S3PresignedUrlGenerator; +import ditda.backend.global.s3.PresignedUpload; import ditda.backend.global.s3.S3Properties; +import ditda.backend.global.s3.S3UploadManager; import ditda.backend.global.s3.enums.BucketType; import lombok.RequiredArgsConstructor; @@ -24,14 +23,14 @@ public class PortfolioService { private static final int MAX_PORTFOLIO_COUNT = 3; - private static final String S3_KEY_PREFIX = "portfolio"; + private static final String DIR = "portfolio"; + private static final BucketType BUCKET = BucketType.PRIVATE; private static final Map ALLOWED_CONTENT_TYPES = Map.of( "application/pdf", ".pdf", "image/png", ".png" ); - private final S3PresignedUrlGenerator s3PresignedUrlGenerator; - private final S3FileManager s3FileManager; + private final S3UploadManager s3UploadManager; private final PortfolioRepository portfolioRepository; private final S3Properties s3Properties; @@ -43,10 +42,8 @@ public PortfolioPresignResponse generatePresignedUpload(String contentType) { throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } - String key = "%s/%s%s".formatted(S3_KEY_PREFIX, UUID.randomUUID(), extension); - String presignedUrl = s3PresignedUrlGenerator.generatePrivatePutUrl(key, contentType); - - return new PortfolioPresignResponse(key, presignedUrl); + PresignedUpload upload = s3UploadManager.issueTempUpload(BUCKET, DIR, extension, contentType); + return new PortfolioPresignResponse(upload.key(), upload.presignedUrl()); } public void validateKeys(List keys) { @@ -63,12 +60,12 @@ public void validateKeys(List keys) { for (String key : keys) { // 파일 key 형식 검증 - if (key == null || !key.startsWith(S3_KEY_PREFIX + "/")) { + if (!s3UploadManager.isTempKey(key, DIR)) { throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } // 파일 크기 검증 - Long size = s3FileManager.getObjectSize(BucketType.PRIVATE, key); + Long size = s3UploadManager.getObjectSize(BUCKET, key); if (size == null) { throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); // 미업로드 key } @@ -91,8 +88,12 @@ public void savePortfolios(Designer designer, List portfolioKeys) { portfolioRepository.saveAll(portfolios); } + public List promote(List tempKeys) { + return s3UploadManager.promote(BUCKET, tempKeys); + } + public void deleteFiles(List portfolioKeys) { - s3FileManager.deleteAll(BucketType.PRIVATE, portfolioKeys); + s3UploadManager.deleteAll(BUCKET, portfolioKeys); } } diff --git a/src/main/java/ditda/backend/global/s3/PresignedUpload.java b/src/main/java/ditda/backend/global/s3/PresignedUpload.java new file mode 100644 index 0000000..a45ac19 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/PresignedUpload.java @@ -0,0 +1,4 @@ +package ditda.backend.global.s3; + +public record PresignedUpload(String key, String presignedUrl) { +} diff --git a/src/main/java/ditda/backend/global/s3/S3FileManager.java b/src/main/java/ditda/backend/global/s3/S3FileManager.java index ae98c71..26cb17c 100644 --- a/src/main/java/ditda/backend/global/s3/S3FileManager.java +++ b/src/main/java/ditda/backend/global/s3/S3FileManager.java @@ -19,7 +19,7 @@ public class S3FileManager { private final S3Properties s3Properties; public Long getObjectSize(BucketType bucketType, String key) { - String bucket = resolveBucket(bucketType); + String bucket = s3Properties.getBucket(bucketType); try { return s3Client.headObject(req -> req.bucket(bucket).key(key)).contentLength(); @@ -37,7 +37,7 @@ public void deleteAll(BucketType bucketType, List keys) { return; } - String bucket = resolveBucket(bucketType); + String bucket = s3Properties.getBucket(bucketType); for (String key : keys) { try { s3Client.deleteObject(req -> req.bucket(bucket).key(key)); @@ -47,10 +47,13 @@ public void deleteAll(BucketType bucketType, List keys) { } } - private String resolveBucket(BucketType bucketType) { - return switch (bucketType) { - case PUBLIC -> s3Properties.getPublicBucket(); - case PRIVATE -> s3Properties.getPrivateBucket(); - }; + public void copy(BucketType bucketType, String sourceKey, String destinationKey) { + String bucket = s3Properties.getBucket(bucketType); + + s3Client.copyObject(req -> req + .sourceBucket(bucket) + .sourceKey(sourceKey) + .destinationBucket(bucket) + .destinationKey(destinationKey)); } } diff --git a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java index c51fb23..cb652d7 100644 --- a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java +++ b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Component; +import ditda.backend.global.s3.enums.BucketType; import lombok.RequiredArgsConstructor; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -39,14 +40,14 @@ public String generatePrivateGetUrl(String key, Duration ttl) { return presignedRequest.url().toString(); } - public String generatePrivatePutUrl(String key, String contentType) { + public String generatePutUrl(BucketType bucketType, String key, String contentType) { Duration ttl = Duration.ofMinutes(s3Properties.getPresignedUrlTtlMinutes()); - return generatePrivatePutUrl(key, contentType, ttl); + return generatePutUrl(bucketType, key, contentType, ttl); } - public String generatePrivatePutUrl(String key, String contentType, Duration ttl) { + public String generatePutUrl(BucketType bucketType, String key, String contentType, Duration ttl) { PutObjectRequest putObjectRequest = PutObjectRequest.builder() - .bucket(s3Properties.getPrivateBucket()) + .bucket(s3Properties.getBucket(bucketType)) .key(key) .contentType(contentType) .build(); diff --git a/src/main/java/ditda/backend/global/s3/S3Properties.java b/src/main/java/ditda/backend/global/s3/S3Properties.java index bb44bf8..78d4163 100644 --- a/src/main/java/ditda/backend/global/s3/S3Properties.java +++ b/src/main/java/ditda/backend/global/s3/S3Properties.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.unit.DataSize; +import ditda.backend.global.s3.enums.BucketType; import lombok.Getter; import lombok.Setter; @@ -20,4 +21,11 @@ public class S3Properties { private String privateBucket; private int presignedUrlTtlMinutes = 10; + + public String getBucket(BucketType bucketType) { + return switch (bucketType) { + case PUBLIC -> publicBucket; + case PRIVATE -> privateBucket; + }; + } } diff --git a/src/main/java/ditda/backend/global/s3/S3UploadManager.java b/src/main/java/ditda/backend/global/s3/S3UploadManager.java new file mode 100644 index 0000000..b02e513 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/S3UploadManager.java @@ -0,0 +1,59 @@ +package ditda.backend.global.s3; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.springframework.stereotype.Component; + +import ditda.backend.global.s3.enums.BucketType; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class S3UploadManager { + + private static final String TEMP_DIR = "tmp"; + + private final S3PresignedUrlGenerator s3PresignedUrlGenerator; + private final S3FileManager s3FileManager; + + public PresignedUpload issueTempUpload(BucketType bucketType, String dir, String extension, String contentType) { + String key = "%s/%s/%s%s".formatted(dir, TEMP_DIR, UUID.randomUUID(), extension); + String presignedUrl = s3PresignedUrlGenerator.generatePutUrl(bucketType, key, contentType); + return new PresignedUpload(key, presignedUrl); + } + + public boolean isTempKey(String key, String dir) { + return key != null && key.startsWith(dir + "/" + TEMP_DIR + "/"); + } + + public Long getObjectSize(BucketType bucketType, String key) { + return s3FileManager.getObjectSize(bucketType, key); + } + + public List promote(BucketType bucketType, List tempKeys) { + if (tempKeys.isEmpty()) { + return List.of(); + } + + List permanentKeys = new ArrayList<>(); + try { + for (String tempKey : tempKeys) { + String permanentKey = tempKey.replaceFirst("/" + TEMP_DIR + "/", "/"); // '/tmp/' -> '/' + s3FileManager.copy(bucketType, tempKey, permanentKey); + permanentKeys.add(permanentKey); + } + } catch (Exception e) { + s3FileManager.deleteAll(bucketType, permanentKeys); // 승격된 객체 되돌림 + throw e; + } + + s3FileManager.deleteAll(bucketType, tempKeys); // tmp 객체 정리 + return permanentKeys; + } + + public void deleteAll(BucketType bucketType, List keys) { + s3FileManager.deleteAll(bucketType, keys); + } +} From 71b21ed1654979041c8b8cd8e1bb5e2217c9a681 Mon Sep 17 00:00:00 2001 From: fervovita Date: Mon, 1 Jun 2026 23:11:28 +0900 Subject: [PATCH 019/102] =?UTF-8?q?Docs(designer):=20=ED=8F=AC=ED=8A=B8?= =?UTF-8?q?=ED=8F=B4=EB=A6=AC=EC=98=A4=20key=20=EC=98=88=EC=8B=9C=EC=97=90?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C(tmp)=20=EA=B2=BD=EB=A1=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/designer/auth/dto/request/DesignerSignupRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java b/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java index 5afdfbb..150d560 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java +++ b/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java @@ -87,7 +87,7 @@ public record DesignerSignupRequest( BankAccountRequest bankAccount, @Schema(description = "presigned URL로 업로드한 포트폴리오 key 목록 (최대 3개)", - example = "[\"portfolio/3f2b....pdf\", \"portfolio/9a1c....png\"]") + example = "[\"portfolio/tmp/3f2b....pdf\", \"portfolio/tmp/9a1c....png\"]") @Size(max = 3, message = "포트폴리오 파일은 최대 3개까지 업로드 가능합니다.") List<@NotBlank(message = "포트폴리오 key는 비어 있을 수 없습니다.") String> portfolioKeys ) { From 66c67944f1df5bdd9d78872449af3bcfeea8a371 Mon Sep 17 00:00:00 2001 From: fervovita Date: Mon, 1 Jun 2026 23:13:41 +0900 Subject: [PATCH 020/102] =?UTF-8?q?Docs(designer):=20=ED=8F=AC=ED=8A=B8?= =?UTF-8?q?=ED=8F=B4=EB=A6=AC=EC=98=A4=20key=EC=99=80=20presigned=20URL=20?= =?UTF-8?q?=EC=98=88=EC=8B=9C=EC=97=90=20=EC=9E=84=EC=8B=9C(tmp)=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designer/auth/dto/response/PortfolioPresignResponse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java b/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java index a87e6a9..3c8bae0 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java +++ b/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java @@ -4,10 +4,10 @@ public record PortfolioPresignResponse( - @Schema(description = "S3 객체 key", example = "portfolio/5a5237a7-674a-47c2-91ca-6fdb28ba6b2d.pdf") + @Schema(description = "S3 객체 key", example = "portfolio/tmp/5a5237a7-674a-47c2-91ca-6fdb28ba6b2d.pdf") String key, - @Schema(description = "S3 직접 업로드용 presigned PUT URL", example = "https://{bucket}.s3.{region}.amazonaws.com/portfolio/{uuid}.pdf?X-Amz-Signature=...") + @Schema(description = "S3 직접 업로드용 presigned PUT URL", example = "https://{bucket}.s3.{region}.amazonaws.com/portfolio/tmp/{uuid}.pdf?X-Amz-Signature=...") String presignedUrl ) { } From 6fa598358f5ae0b454f8cb5cd849793f2757eb5b Mon Sep 17 00:00:00 2001 From: fervovita Date: Mon, 1 Jun 2026 23:40:10 +0900 Subject: [PATCH 021/102] =?UTF-8?q?Refactor(security):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20permitAll=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EB=A5=BC=20=EB=AA=85=EC=8B=9C=EC=A0=81=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ditda/backend/global/config/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/global/config/SecurityConfig.java b/src/main/java/ditda/backend/global/config/SecurityConfig.java index 29d3188..0a34101 100644 --- a/src/main/java/ditda/backend/global/config/SecurityConfig.java +++ b/src/main/java/ditda/backend/global/config/SecurityConfig.java @@ -59,7 +59,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() .requestMatchers(HttpMethod.POST, "/api/v1/auth/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/v1/instructors/auth/signup").permitAll() - .requestMatchers(HttpMethod.POST, "/api/v1/designers/auth/signup/**").permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1/designers/auth/signup", + "/api/v1/designers/auth/signup/portfolio/presigned-url").permitAll() .requestMatchers(HttpMethod.GET, "/actuator/health").permitAll() .anyRequest().authenticated() ) From 77897671e881015aabc5a768a91c07965f27349e Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Tue, 2 Jun 2026 15:30:05 +0900 Subject: [PATCH 022/102] =?UTF-8?q?Refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C?= =?UTF-8?q?=ED=8E=B8=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 불필요한 `common` 패키지 뎁스를 제거하고 `user`, `term`, `payment`, `auth` 등을 독립적인 최상위 도메인 패키지로 격상 - `commission` 도메인을 역할에 따라 `core`, `application`, `draft`, `revision` 하위 패키지로 세분화 - 강사와 디자이너로 분리되어 있던 회원가입/인증 로직을 `auth` 도메인 하나로 병합 (`AuthResult`, `SignupResponse`로 DTO 통합) - `PortfolioService`를 `SignupPortfolioService`로 변경하는 등 일부 클래스 네이밍 및 연관된 임포트 경로 전면 수정 --- .../auth/controller/AuthController.java | 22 ++++++------- .../controller/DesignerAuthController.java | 22 ++++++------- .../controller/InstructorAuthController.java | 18 +++++----- .../{common => }/auth/dto/AuthResult.java | 2 +- .../{common => }/auth/dto/TokenResult.java | 2 +- .../dto/request/CheckUsernameRequest.java | 2 +- .../dto/request/DesignerSignupRequest.java | 6 ++-- .../request/EmailCodeVerificationRequest.java | 2 +- .../dto/request/EmailVerificationRequest.java | 2 +- .../dto/request/InstructorSignupRequest.java | 4 +-- .../auth/dto/request/LoginRequest.java | 2 +- .../dto/request/PortfolioPresignRequest.java | 2 +- .../auth/dto/response/LoginResponse.java | 2 +- .../response/PortfolioPresignResponse.java | 2 +- .../auth/dto/response/ReissueResponse.java | 2 +- .../dto/response/SignupResponse.java} | 6 ++-- .../auth/entity/RefreshToken.java | 4 +-- .../auth/event/DesignerSignedUpEvent.java | 2 +- .../auth/exception/AuthErrorCode.java | 2 +- .../exception/PortfolioErrorCode.java} | 4 +-- .../{common => }/auth/facade/AuthFacade.java | 16 ++++----- .../auth/facade/DesignerAuthFacade.java | 32 +++++++++--------- .../auth/facade/InstructorAuthFacade.java | 29 ++++++++++++++++ .../auth/mapper/AuthResponseMapper.java | 16 +++++++-- .../notification/DesignerSignupMailer.java | 2 +- .../notification/DesignerSignupNotifier.java | 4 +-- .../notification/EmailVerificationMailer.java | 2 +- .../repository/RefreshTokenRepository.java | 4 +-- .../auth/scheduler/RefreshTokenScheduler.java | 4 +-- .../auth/service/AuthService.java | 16 ++++----- .../auth/service/DesignerAuthService.java | 33 +++++++++---------- .../service/EmailVerificationService.java | 4 +-- .../auth/service/InstructorAuthService.java | 27 ++++++++------- .../service/SignupPortfolioService.java} | 26 +++++++-------- .../entity/CommissionApplication.java | 7 ++-- .../entity/enums/ApplicationStatus.java | 2 +- .../CommissionApplicationRepository.java | 4 +-- .../core}/entity/Commission.java | 16 ++++----- .../core}/entity/CommissionColor.java | 4 +-- .../core}/entity/CommissionConcept.java | 4 +-- .../core}/entity/CommissionFile.java | 4 +-- .../core}/entity/Textbook.java | 2 +- .../core}/entity/TextbookPage.java | 4 +-- .../core}/entity/enums/CategoryType.java | 2 +- .../core/entity/enums/ColorRole.java | 7 ++++ .../entity/enums/ColorSelectionMode.java | 2 +- .../core}/entity/enums/CommissionStatus.java | 2 +- .../core}/entity/enums/ConceptCategory.java | 2 +- .../core}/entity/enums/ConceptTag.java | 2 +- .../core}/entity/enums/FileKind.java | 2 +- .../core}/entity/enums/PageType.java | 2 +- .../core}/entity/enums/PlanCode.java | 2 +- .../core}/entity/enums/Size.java | 2 +- .../repository/CommissionColorRepository.java | 4 +-- .../CommissionConceptRepository.java | 4 +-- .../repository/CommissionFileRepository.java | 4 +-- .../repository/CommissionRepository.java | 4 +-- .../repository/TextbookPageRepository.java | 4 +-- .../core}/repository/TextbookRepository.java | 4 +-- .../draft}/entity/CommissionDraft.java | 3 +- .../draft}/entity/CommissionDraftFile.java | 4 +-- .../draft}/entity/enums/WatermarkStatus.java | 2 +- .../CommissionDraftFileRepository.java | 4 +-- .../repository/CommissionDraftRepository.java | 4 +-- .../revision}/entity/RevisionDetail.java | 4 +-- .../revision}/entity/RevisionRequest.java | 4 ++- .../revision}/entity/RevisionResponse.java | 3 +- .../entity/enums/RevisionCategory.java | 2 +- .../repository/RevisionDetailRepository.java | 4 +-- .../repository/RevisionRequestRepository.java | 4 +-- .../RevisionResponseRepository.java | 4 +-- .../commission/entity/enums/ColorRole.java | 7 ---- .../domain/common/term/dto/TermAgreement.java | 10 ------ .../common/user/entity/enums/UserRole.java | 6 ---- .../designer/auth/dto/DesignerAuthResult.java | 15 --------- .../dto/response/DesignerSignupResponse.java | 20 ----------- .../mapper/DesignerAuthResponseMapper.java | 24 -------------- .../designer/{auth => }/entity/Designer.java | 6 ++-- .../designer/{auth => }/entity/Portfolio.java | 2 +- .../{auth => }/entity/enums/BankName.java | 2 +- .../repository/DesignerRepository.java | 4 +-- .../repository/PortfolioRepository.java | 4 +-- .../auth/dto/InstructorAuthResult.java | 15 --------- .../auth/facade/InstructorAuthFacade.java | 29 ---------------- .../mapper/InstructorAuthResponseMapper.java | 24 -------------- .../{auth => }/entity/Instructor.java | 4 +-- .../repository/InstructorRepository.java | 4 +-- .../{common => }/payment/entity/Payment.java | 6 ++-- .../payment/entity/enums/PaymentStatus.java | 2 +- .../payment/repository/PaymentRepository.java | 4 +-- .../domain/term/dto/TermAgreement.java | 10 ++++++ .../{common => }/term/entity/PaymentTerm.java | 4 +-- .../{common => }/term/entity/UserTerm.java | 6 ++-- .../term/entity/enums/TermType.java | 2 +- .../term/exception/TermErrorCode.java | 2 +- .../repository/PaymentTermRepository.java | 4 +-- .../term/repository/UserTermRepository.java | 4 +-- .../term/service/TermService.java | 14 ++++---- .../domain/{common => }/user/entity/User.java | 4 +-- .../domain/user/entity/enums/UserRole.java | 6 ++++ .../user/exception/UserErrorCode.java | 2 +- .../user/repository/UserRepository.java | 4 +-- .../user/service/UserService.java | 10 +++--- 103 files changed, 319 insertions(+), 404 deletions(-) rename src/main/java/ditda/backend/domain/{common => }/auth/controller/AuthController.java (85%) rename src/main/java/ditda/backend/domain/{designer => }/auth/controller/DesignerAuthController.java (75%) rename src/main/java/ditda/backend/domain/{instructor => }/auth/controller/InstructorAuthController.java (70%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/AuthResult.java (73%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/TokenResult.java (62%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/request/CheckUsernameRequest.java (90%) rename src/main/java/ditda/backend/domain/{designer => }/auth/dto/request/DesignerSignupRequest.java (95%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/request/EmailCodeVerificationRequest.java (93%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/request/EmailVerificationRequest.java (91%) rename src/main/java/ditda/backend/domain/{instructor => }/auth/dto/request/InstructorSignupRequest.java (96%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/request/LoginRequest.java (89%) rename src/main/java/ditda/backend/domain/{designer => }/auth/dto/request/PortfolioPresignRequest.java (91%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/response/LoginResponse.java (89%) rename src/main/java/ditda/backend/domain/{designer => }/auth/dto/response/PortfolioPresignResponse.java (87%) rename src/main/java/ditda/backend/domain/{common => }/auth/dto/response/ReissueResponse.java (78%) rename src/main/java/ditda/backend/domain/{instructor/auth/dto/response/InstructorSignupResponse.java => auth/dto/response/SignupResponse.java} (71%) rename src/main/java/ditda/backend/domain/{common => }/auth/entity/RefreshToken.java (94%) rename src/main/java/ditda/backend/domain/{designer => }/auth/event/DesignerSignedUpEvent.java (73%) rename src/main/java/ditda/backend/domain/{common => }/auth/exception/AuthErrorCode.java (93%) rename src/main/java/ditda/backend/domain/{designer/auth/exception/DesignerErrorCode.java => auth/exception/PortfolioErrorCode.java} (88%) rename src/main/java/ditda/backend/domain/{common => }/auth/facade/AuthFacade.java (70%) rename src/main/java/ditda/backend/domain/{designer => }/auth/facade/DesignerAuthFacade.java (55%) create mode 100644 src/main/java/ditda/backend/domain/auth/facade/InstructorAuthFacade.java rename src/main/java/ditda/backend/domain/{common => }/auth/mapper/AuthResponseMapper.java (52%) rename src/main/java/ditda/backend/domain/{designer => }/auth/notification/DesignerSignupMailer.java (93%) rename src/main/java/ditda/backend/domain/{designer => }/auth/notification/DesignerSignupNotifier.java (92%) rename src/main/java/ditda/backend/domain/{common => }/auth/notification/EmailVerificationMailer.java (90%) rename src/main/java/ditda/backend/domain/{common => }/auth/repository/RefreshTokenRepository.java (88%) rename src/main/java/ditda/backend/domain/{common => }/auth/scheduler/RefreshTokenScheduler.java (84%) rename src/main/java/ditda/backend/domain/{common => }/auth/service/AuthService.java (91%) rename src/main/java/ditda/backend/domain/{designer => }/auth/service/DesignerAuthService.java (67%) rename src/main/java/ditda/backend/domain/{common => }/auth/service/EmailVerificationService.java (95%) rename src/main/java/ditda/backend/domain/{instructor => }/auth/service/InstructorAuthService.java (64%) rename src/main/java/ditda/backend/domain/{designer/auth/service/PortfolioService.java => auth/service/SignupPortfolioService.java} (72%) rename src/main/java/ditda/backend/domain/{common/commission => commission/application}/entity/CommissionApplication.java (87%) rename src/main/java/ditda/backend/domain/{common/commission => commission/application}/entity/enums/ApplicationStatus.java (65%) rename src/main/java/ditda/backend/domain/{common/commission => commission/application}/repository/CommissionApplicationRepository.java (53%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/Commission.java (85%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/CommissionColor.java (92%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/CommissionConcept.java (92%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/CommissionFile.java (92%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/Textbook.java (95%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/TextbookPage.java (93%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/CategoryType.java (71%) create mode 100644 src/main/java/ditda/backend/domain/commission/core/entity/enums/ColorRole.java rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/ColorSelectionMode.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/CommissionStatus.java (80%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/ConceptCategory.java (81%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/ConceptTag.java (93%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/FileKind.java (58%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/PageType.java (86%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/PlanCode.java (89%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/entity/enums/Size.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/repository/CommissionColorRepository.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/repository/CommissionConceptRepository.java (55%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/repository/CommissionFileRepository.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/repository/CommissionRepository.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/repository/TextbookPageRepository.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/core}/repository/TextbookRepository.java (53%) rename src/main/java/ditda/backend/domain/{common/commission => commission/draft}/entity/CommissionDraft.java (93%) rename src/main/java/ditda/backend/domain/{common/commission => commission/draft}/entity/CommissionDraftFile.java (94%) rename src/main/java/ditda/backend/domain/{common/commission => commission/draft}/entity/enums/WatermarkStatus.java (73%) rename src/main/java/ditda/backend/domain/{common/commission => commission/draft}/repository/CommissionDraftFileRepository.java (55%) rename src/main/java/ditda/backend/domain/{common/commission => commission/draft}/repository/CommissionDraftRepository.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/revision}/entity/RevisionDetail.java (91%) rename src/main/java/ditda/backend/domain/{common/commission => commission/revision}/entity/RevisionRequest.java (89%) rename src/main/java/ditda/backend/domain/{common/commission => commission/revision}/entity/RevisionResponse.java (91%) rename src/main/java/ditda/backend/domain/{common/commission => commission/revision}/entity/enums/RevisionCategory.java (75%) rename src/main/java/ditda/backend/domain/{common/commission => commission/revision}/repository/RevisionDetailRepository.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/revision}/repository/RevisionRequestRepository.java (54%) rename src/main/java/ditda/backend/domain/{common/commission => commission/revision}/repository/RevisionResponseRepository.java (54%) delete mode 100644 src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java delete mode 100644 src/main/java/ditda/backend/domain/common/term/dto/TermAgreement.java delete mode 100644 src/main/java/ditda/backend/domain/common/user/entity/enums/UserRole.java delete mode 100644 src/main/java/ditda/backend/domain/designer/auth/dto/DesignerAuthResult.java delete mode 100644 src/main/java/ditda/backend/domain/designer/auth/dto/response/DesignerSignupResponse.java delete mode 100644 src/main/java/ditda/backend/domain/designer/auth/mapper/DesignerAuthResponseMapper.java rename src/main/java/ditda/backend/domain/designer/{auth => }/entity/Designer.java (91%) rename src/main/java/ditda/backend/domain/designer/{auth => }/entity/Portfolio.java (95%) rename src/main/java/ditda/backend/domain/designer/{auth => }/entity/enums/BankName.java (89%) rename src/main/java/ditda/backend/domain/designer/{auth => }/repository/DesignerRepository.java (55%) rename src/main/java/ditda/backend/domain/designer/{auth => }/repository/PortfolioRepository.java (55%) delete mode 100644 src/main/java/ditda/backend/domain/instructor/auth/dto/InstructorAuthResult.java delete mode 100644 src/main/java/ditda/backend/domain/instructor/auth/facade/InstructorAuthFacade.java delete mode 100644 src/main/java/ditda/backend/domain/instructor/auth/mapper/InstructorAuthResponseMapper.java rename src/main/java/ditda/backend/domain/instructor/{auth => }/entity/Instructor.java (89%) rename src/main/java/ditda/backend/domain/instructor/{auth => }/repository/InstructorRepository.java (55%) rename src/main/java/ditda/backend/domain/{common => }/payment/entity/Payment.java (89%) rename src/main/java/ditda/backend/domain/{common => }/payment/entity/enums/PaymentStatus.java (74%) rename src/main/java/ditda/backend/domain/{common => }/payment/repository/PaymentRepository.java (55%) create mode 100644 src/main/java/ditda/backend/domain/term/dto/TermAgreement.java rename src/main/java/ditda/backend/domain/{common => }/term/entity/PaymentTerm.java (92%) rename src/main/java/ditda/backend/domain/{common => }/term/entity/UserTerm.java (90%) rename src/main/java/ditda/backend/domain/{common => }/term/entity/enums/TermType.java (63%) rename src/main/java/ditda/backend/domain/{common => }/term/exception/TermErrorCode.java (89%) rename src/main/java/ditda/backend/domain/{common => }/term/repository/PaymentTermRepository.java (56%) rename src/main/java/ditda/backend/domain/{common => }/term/repository/UserTermRepository.java (56%) rename src/main/java/ditda/backend/domain/{common => }/term/service/TermService.java (73%) rename src/main/java/ditda/backend/domain/{common => }/user/entity/User.java (94%) create mode 100644 src/main/java/ditda/backend/domain/user/entity/enums/UserRole.java rename src/main/java/ditda/backend/domain/{common => }/user/exception/UserErrorCode.java (92%) rename src/main/java/ditda/backend/domain/{common => }/user/repository/UserRepository.java (73%) rename src/main/java/ditda/backend/domain/{common => }/user/service/UserService.java (84%) diff --git a/src/main/java/ditda/backend/domain/common/auth/controller/AuthController.java b/src/main/java/ditda/backend/domain/auth/controller/AuthController.java similarity index 85% rename from src/main/java/ditda/backend/domain/common/auth/controller/AuthController.java rename to src/main/java/ditda/backend/domain/auth/controller/AuthController.java index bf3bf81..d6a26f3 100644 --- a/src/main/java/ditda/backend/domain/common/auth/controller/AuthController.java +++ b/src/main/java/ditda/backend/domain/auth/controller/AuthController.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.controller; +package ditda.backend.domain.auth.controller; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.CookieValue; @@ -7,16 +7,16 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import ditda.backend.domain.common.auth.dto.AuthResult; -import ditda.backend.domain.common.auth.dto.TokenResult; -import ditda.backend.domain.common.auth.dto.request.CheckUsernameRequest; -import ditda.backend.domain.common.auth.dto.request.EmailCodeVerificationRequest; -import ditda.backend.domain.common.auth.dto.request.EmailVerificationRequest; -import ditda.backend.domain.common.auth.dto.request.LoginRequest; -import ditda.backend.domain.common.auth.dto.response.LoginResponse; -import ditda.backend.domain.common.auth.dto.response.ReissueResponse; -import ditda.backend.domain.common.auth.facade.AuthFacade; -import ditda.backend.domain.common.auth.mapper.AuthResponseMapper; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.TokenResult; +import ditda.backend.domain.auth.dto.request.CheckUsernameRequest; +import ditda.backend.domain.auth.dto.request.EmailCodeVerificationRequest; +import ditda.backend.domain.auth.dto.request.EmailVerificationRequest; +import ditda.backend.domain.auth.dto.request.LoginRequest; +import ditda.backend.domain.auth.dto.response.LoginResponse; +import ditda.backend.domain.auth.dto.response.ReissueResponse; +import ditda.backend.domain.auth.facade.AuthFacade; +import ditda.backend.domain.auth.mapper.AuthResponseMapper; import ditda.backend.global.apipayload.response.ApiResponse; import ditda.backend.global.jwt.utils.CookieUtils; import io.swagger.v3.oas.annotations.Operation; diff --git a/src/main/java/ditda/backend/domain/designer/auth/controller/DesignerAuthController.java b/src/main/java/ditda/backend/domain/auth/controller/DesignerAuthController.java similarity index 75% rename from src/main/java/ditda/backend/domain/designer/auth/controller/DesignerAuthController.java rename to src/main/java/ditda/backend/domain/auth/controller/DesignerAuthController.java index 7d0993c..3d3a8b4 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/controller/DesignerAuthController.java +++ b/src/main/java/ditda/backend/domain/auth/controller/DesignerAuthController.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.controller; +package ditda.backend.domain.auth.controller; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.PostMapping; @@ -6,13 +6,13 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import ditda.backend.domain.designer.auth.dto.DesignerAuthResult; -import ditda.backend.domain.designer.auth.dto.request.DesignerSignupRequest; -import ditda.backend.domain.designer.auth.dto.request.PortfolioPresignRequest; -import ditda.backend.domain.designer.auth.dto.response.DesignerSignupResponse; -import ditda.backend.domain.designer.auth.dto.response.PortfolioPresignResponse; -import ditda.backend.domain.designer.auth.facade.DesignerAuthFacade; -import ditda.backend.domain.designer.auth.mapper.DesignerAuthResponseMapper; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.request.DesignerSignupRequest; +import ditda.backend.domain.auth.dto.request.PortfolioPresignRequest; +import ditda.backend.domain.auth.dto.response.PortfolioPresignResponse; +import ditda.backend.domain.auth.dto.response.SignupResponse; +import ditda.backend.domain.auth.facade.DesignerAuthFacade; +import ditda.backend.domain.auth.mapper.AuthResponseMapper; import ditda.backend.global.apipayload.response.ApiResponse; import ditda.backend.global.jwt.utils.CookieUtils; import io.swagger.v3.oas.annotations.Operation; @@ -29,7 +29,7 @@ public class DesignerAuthController { private final DesignerAuthFacade designerAuthFacade; private final CookieUtils cookieUtils; - private final DesignerAuthResponseMapper authResponseMapper; + private final AuthResponseMapper authResponseMapper; @Operation( summary = "포트폴리오 업로드 URL 발급", @@ -50,11 +50,11 @@ public ApiResponse issuePortfolioPresignedUrls( description = "**[회원가입]** 회원가입 후 자동 로그인 처리됩니다. presigned PUT URL로 업로드한 포트폴리오 key를 portfolioKeys에 담아 전송합니다." ) @PostMapping(value = "/signup") - public ApiResponse signup( + public ApiResponse signup( @Valid @RequestBody DesignerSignupRequest request, HttpServletResponse response ) { - DesignerAuthResult result = designerAuthFacade.signup(request); + AuthResult result = designerAuthFacade.signup(request); addRefreshTokenCookie(response, result.refreshToken()); diff --git a/src/main/java/ditda/backend/domain/instructor/auth/controller/InstructorAuthController.java b/src/main/java/ditda/backend/domain/auth/controller/InstructorAuthController.java similarity index 70% rename from src/main/java/ditda/backend/domain/instructor/auth/controller/InstructorAuthController.java rename to src/main/java/ditda/backend/domain/auth/controller/InstructorAuthController.java index 6314c3d..bf80c08 100644 --- a/src/main/java/ditda/backend/domain/instructor/auth/controller/InstructorAuthController.java +++ b/src/main/java/ditda/backend/domain/auth/controller/InstructorAuthController.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.instructor.auth.controller; +package ditda.backend.domain.auth.controller; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.PostMapping; @@ -6,11 +6,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import ditda.backend.domain.instructor.auth.dto.InstructorAuthResult; -import ditda.backend.domain.instructor.auth.dto.request.InstructorSignupRequest; -import ditda.backend.domain.instructor.auth.dto.response.InstructorSignupResponse; -import ditda.backend.domain.instructor.auth.facade.InstructorAuthFacade; -import ditda.backend.domain.instructor.auth.mapper.InstructorAuthResponseMapper; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.request.InstructorSignupRequest; +import ditda.backend.domain.auth.dto.response.SignupResponse; +import ditda.backend.domain.auth.facade.InstructorAuthFacade; +import ditda.backend.domain.auth.mapper.AuthResponseMapper; import ditda.backend.global.apipayload.response.ApiResponse; import ditda.backend.global.jwt.utils.CookieUtils; import io.swagger.v3.oas.annotations.Operation; @@ -27,16 +27,16 @@ public class InstructorAuthController { private final InstructorAuthFacade instructorAuthFacade; private final CookieUtils cookieUtils; - private final InstructorAuthResponseMapper authResponseMapper; + private final AuthResponseMapper authResponseMapper; @Operation(summary = "강사 회원가입", description = "**[회원가입]** 회원가입 후 자동 로그인 처리됩니다.") @PostMapping("/signup") - public ApiResponse signup( + public ApiResponse signup( @Valid @RequestBody InstructorSignupRequest request, HttpServletResponse response ) { - InstructorAuthResult result = instructorAuthFacade.signup(request); + AuthResult result = instructorAuthFacade.signup(request); addRefreshTokenCookie(response, result.refreshToken()); diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/AuthResult.java b/src/main/java/ditda/backend/domain/auth/dto/AuthResult.java similarity index 73% rename from src/main/java/ditda/backend/domain/common/auth/dto/AuthResult.java rename to src/main/java/ditda/backend/domain/auth/dto/AuthResult.java index 004e2aa..8260ea0 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/AuthResult.java +++ b/src/main/java/ditda/backend/domain/auth/dto/AuthResult.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto; +package ditda.backend.domain.auth.dto; public record AuthResult( diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/TokenResult.java b/src/main/java/ditda/backend/domain/auth/dto/TokenResult.java similarity index 62% rename from src/main/java/ditda/backend/domain/common/auth/dto/TokenResult.java rename to src/main/java/ditda/backend/domain/auth/dto/TokenResult.java index b64e7c7..645fd11 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/TokenResult.java +++ b/src/main/java/ditda/backend/domain/auth/dto/TokenResult.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto; +package ditda.backend.domain.auth.dto; public record TokenResult( String accessToken, diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/request/CheckUsernameRequest.java b/src/main/java/ditda/backend/domain/auth/dto/request/CheckUsernameRequest.java similarity index 90% rename from src/main/java/ditda/backend/domain/common/auth/dto/request/CheckUsernameRequest.java rename to src/main/java/ditda/backend/domain/auth/dto/request/CheckUsernameRequest.java index a66e83c..3dd5043 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/request/CheckUsernameRequest.java +++ b/src/main/java/ditda/backend/domain/auth/dto/request/CheckUsernameRequest.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto.request; +package ditda.backend.domain.auth.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java b/src/main/java/ditda/backend/domain/auth/dto/request/DesignerSignupRequest.java similarity index 95% rename from src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java rename to src/main/java/ditda/backend/domain/auth/dto/request/DesignerSignupRequest.java index 150d560..66dd89f 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/request/DesignerSignupRequest.java +++ b/src/main/java/ditda/backend/domain/auth/dto/request/DesignerSignupRequest.java @@ -1,11 +1,11 @@ -package ditda.backend.domain.designer.auth.dto.request; +package ditda.backend.domain.auth.dto.request; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; -import ditda.backend.domain.common.term.entity.enums.TermType; -import ditda.backend.domain.designer.auth.entity.enums.BankName; +import ditda.backend.domain.designer.entity.enums.BankName; +import ditda.backend.domain.term.entity.enums.TermType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/request/EmailCodeVerificationRequest.java b/src/main/java/ditda/backend/domain/auth/dto/request/EmailCodeVerificationRequest.java similarity index 93% rename from src/main/java/ditda/backend/domain/common/auth/dto/request/EmailCodeVerificationRequest.java rename to src/main/java/ditda/backend/domain/auth/dto/request/EmailCodeVerificationRequest.java index 4300346..ae22f1b 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/request/EmailCodeVerificationRequest.java +++ b/src/main/java/ditda/backend/domain/auth/dto/request/EmailCodeVerificationRequest.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto.request; +package ditda.backend.domain.auth.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/request/EmailVerificationRequest.java b/src/main/java/ditda/backend/domain/auth/dto/request/EmailVerificationRequest.java similarity index 91% rename from src/main/java/ditda/backend/domain/common/auth/dto/request/EmailVerificationRequest.java rename to src/main/java/ditda/backend/domain/auth/dto/request/EmailVerificationRequest.java index 7a28862..a2ed4c6 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/request/EmailVerificationRequest.java +++ b/src/main/java/ditda/backend/domain/auth/dto/request/EmailVerificationRequest.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto.request; +package ditda.backend.domain.auth.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; diff --git a/src/main/java/ditda/backend/domain/instructor/auth/dto/request/InstructorSignupRequest.java b/src/main/java/ditda/backend/domain/auth/dto/request/InstructorSignupRequest.java similarity index 96% rename from src/main/java/ditda/backend/domain/instructor/auth/dto/request/InstructorSignupRequest.java rename to src/main/java/ditda/backend/domain/auth/dto/request/InstructorSignupRequest.java index 89c288f..dac10ca 100644 --- a/src/main/java/ditda/backend/domain/instructor/auth/dto/request/InstructorSignupRequest.java +++ b/src/main/java/ditda/backend/domain/auth/dto/request/InstructorSignupRequest.java @@ -1,10 +1,10 @@ -package ditda.backend.domain.instructor.auth.dto.request; +package ditda.backend.domain.auth.dto.request; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; -import ditda.backend.domain.common.term.entity.enums.TermType; +import ditda.backend.domain.term.entity.enums.TermType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/request/LoginRequest.java b/src/main/java/ditda/backend/domain/auth/dto/request/LoginRequest.java similarity index 89% rename from src/main/java/ditda/backend/domain/common/auth/dto/request/LoginRequest.java rename to src/main/java/ditda/backend/domain/auth/dto/request/LoginRequest.java index 2fef033..7e0a3eb 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/request/LoginRequest.java +++ b/src/main/java/ditda/backend/domain/auth/dto/request/LoginRequest.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto.request; +package ditda.backend.domain.auth.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/request/PortfolioPresignRequest.java b/src/main/java/ditda/backend/domain/auth/dto/request/PortfolioPresignRequest.java similarity index 91% rename from src/main/java/ditda/backend/domain/designer/auth/dto/request/PortfolioPresignRequest.java rename to src/main/java/ditda/backend/domain/auth/dto/request/PortfolioPresignRequest.java index bb60393..644bcf9 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/request/PortfolioPresignRequest.java +++ b/src/main/java/ditda/backend/domain/auth/dto/request/PortfolioPresignRequest.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.dto.request; +package ditda.backend.domain.auth.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/response/LoginResponse.java b/src/main/java/ditda/backend/domain/auth/dto/response/LoginResponse.java similarity index 89% rename from src/main/java/ditda/backend/domain/common/auth/dto/response/LoginResponse.java rename to src/main/java/ditda/backend/domain/auth/dto/response/LoginResponse.java index 806abff..cbac86d 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/response/LoginResponse.java +++ b/src/main/java/ditda/backend/domain/auth/dto/response/LoginResponse.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto.response; +package ditda.backend.domain.auth.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java b/src/main/java/ditda/backend/domain/auth/dto/response/PortfolioPresignResponse.java similarity index 87% rename from src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java rename to src/main/java/ditda/backend/domain/auth/dto/response/PortfolioPresignResponse.java index 3c8bae0..2610156 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/response/PortfolioPresignResponse.java +++ b/src/main/java/ditda/backend/domain/auth/dto/response/PortfolioPresignResponse.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.dto.response; +package ditda.backend.domain.auth.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ditda/backend/domain/common/auth/dto/response/ReissueResponse.java b/src/main/java/ditda/backend/domain/auth/dto/response/ReissueResponse.java similarity index 78% rename from src/main/java/ditda/backend/domain/common/auth/dto/response/ReissueResponse.java rename to src/main/java/ditda/backend/domain/auth/dto/response/ReissueResponse.java index 997261a..0476f58 100644 --- a/src/main/java/ditda/backend/domain/common/auth/dto/response/ReissueResponse.java +++ b/src/main/java/ditda/backend/domain/auth/dto/response/ReissueResponse.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.dto.response; +package ditda.backend.domain.auth.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ditda/backend/domain/instructor/auth/dto/response/InstructorSignupResponse.java b/src/main/java/ditda/backend/domain/auth/dto/response/SignupResponse.java similarity index 71% rename from src/main/java/ditda/backend/domain/instructor/auth/dto/response/InstructorSignupResponse.java rename to src/main/java/ditda/backend/domain/auth/dto/response/SignupResponse.java index f989f47..0e8a54c 100644 --- a/src/main/java/ditda/backend/domain/instructor/auth/dto/response/InstructorSignupResponse.java +++ b/src/main/java/ditda/backend/domain/auth/dto/response/SignupResponse.java @@ -1,9 +1,9 @@ -package ditda.backend.domain.instructor.auth.dto.response; +package ditda.backend.domain.auth.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "강사 회원가입 응답") -public record InstructorSignupResponse( +@Schema(description = "회원가입 응답") +public record SignupResponse( @Schema(description = "User ID") Long userId, diff --git a/src/main/java/ditda/backend/domain/common/auth/entity/RefreshToken.java b/src/main/java/ditda/backend/domain/auth/entity/RefreshToken.java similarity index 94% rename from src/main/java/ditda/backend/domain/common/auth/entity/RefreshToken.java rename to src/main/java/ditda/backend/domain/auth/entity/RefreshToken.java index 744d242..2c1bf68 100644 --- a/src/main/java/ditda/backend/domain/common/auth/entity/RefreshToken.java +++ b/src/main/java/ditda/backend/domain/auth/entity/RefreshToken.java @@ -1,10 +1,10 @@ -package ditda.backend.domain.common.auth.entity; +package ditda.backend.domain.auth.entity; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.LocalDateTime; -import ditda.backend.domain.common.user.entity.User; +import ditda.backend.domain.user.entity.User; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/src/main/java/ditda/backend/domain/designer/auth/event/DesignerSignedUpEvent.java b/src/main/java/ditda/backend/domain/auth/event/DesignerSignedUpEvent.java similarity index 73% rename from src/main/java/ditda/backend/domain/designer/auth/event/DesignerSignedUpEvent.java rename to src/main/java/ditda/backend/domain/auth/event/DesignerSignedUpEvent.java index 0874f19..7044423 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/event/DesignerSignedUpEvent.java +++ b/src/main/java/ditda/backend/domain/auth/event/DesignerSignedUpEvent.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.event; +package ditda.backend.domain.auth.event; import java.util.List; diff --git a/src/main/java/ditda/backend/domain/common/auth/exception/AuthErrorCode.java b/src/main/java/ditda/backend/domain/auth/exception/AuthErrorCode.java similarity index 93% rename from src/main/java/ditda/backend/domain/common/auth/exception/AuthErrorCode.java rename to src/main/java/ditda/backend/domain/auth/exception/AuthErrorCode.java index 759ff80..4b7bfe3 100644 --- a/src/main/java/ditda/backend/domain/common/auth/exception/AuthErrorCode.java +++ b/src/main/java/ditda/backend/domain/auth/exception/AuthErrorCode.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.exception; +package ditda.backend.domain.auth.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/ditda/backend/domain/designer/auth/exception/DesignerErrorCode.java b/src/main/java/ditda/backend/domain/auth/exception/PortfolioErrorCode.java similarity index 88% rename from src/main/java/ditda/backend/domain/designer/auth/exception/DesignerErrorCode.java rename to src/main/java/ditda/backend/domain/auth/exception/PortfolioErrorCode.java index 717bf26..9c8b93f 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/exception/DesignerErrorCode.java +++ b/src/main/java/ditda/backend/domain/auth/exception/PortfolioErrorCode.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.exception; +package ditda.backend.domain.auth.exception; import org.springframework.http.HttpStatus; @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor -public enum DesignerErrorCode implements BaseErrorCode { +public enum PortfolioErrorCode implements BaseErrorCode { PORTFOLIO_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "DESIGNER_400_01", "포트폴리오 파일은 최대 3개까지 업로드 가능합니다."), INVALID_PORTFOLIO_FILE(HttpStatus.BAD_REQUEST, "DESIGNER_400_02", "지원하지 않는 파일 형식입니다."), diff --git a/src/main/java/ditda/backend/domain/common/auth/facade/AuthFacade.java b/src/main/java/ditda/backend/domain/auth/facade/AuthFacade.java similarity index 70% rename from src/main/java/ditda/backend/domain/common/auth/facade/AuthFacade.java rename to src/main/java/ditda/backend/domain/auth/facade/AuthFacade.java index d861ffe..8abfc14 100644 --- a/src/main/java/ditda/backend/domain/common/auth/facade/AuthFacade.java +++ b/src/main/java/ditda/backend/domain/auth/facade/AuthFacade.java @@ -1,14 +1,14 @@ -package ditda.backend.domain.common.auth.facade; +package ditda.backend.domain.auth.facade; import org.springframework.stereotype.Component; -import ditda.backend.domain.common.auth.dto.AuthResult; -import ditda.backend.domain.common.auth.dto.TokenResult; -import ditda.backend.domain.common.auth.dto.request.LoginRequest; -import ditda.backend.domain.common.auth.notification.EmailVerificationMailer; -import ditda.backend.domain.common.auth.service.AuthService; -import ditda.backend.domain.common.auth.service.EmailVerificationService; -import ditda.backend.domain.common.user.service.UserService; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.TokenResult; +import ditda.backend.domain.auth.dto.request.LoginRequest; +import ditda.backend.domain.auth.notification.EmailVerificationMailer; +import ditda.backend.domain.auth.service.AuthService; +import ditda.backend.domain.auth.service.EmailVerificationService; +import ditda.backend.domain.user.service.UserService; import lombok.RequiredArgsConstructor; @Component diff --git a/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java similarity index 55% rename from src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java rename to src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java index 200895d..5b52f20 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/facade/DesignerAuthFacade.java +++ b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java @@ -1,17 +1,17 @@ -package ditda.backend.domain.designer.auth.facade; +package ditda.backend.domain.auth.facade; import java.util.List; import org.springframework.stereotype.Component; -import ditda.backend.domain.common.auth.service.EmailVerificationService; -import ditda.backend.domain.common.user.service.UserService; -import ditda.backend.domain.designer.auth.dto.DesignerAuthResult; -import ditda.backend.domain.designer.auth.dto.request.DesignerSignupRequest; -import ditda.backend.domain.designer.auth.dto.request.PortfolioPresignRequest; -import ditda.backend.domain.designer.auth.dto.response.PortfolioPresignResponse; -import ditda.backend.domain.designer.auth.service.DesignerAuthService; -import ditda.backend.domain.designer.auth.service.PortfolioService; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.request.DesignerSignupRequest; +import ditda.backend.domain.auth.dto.request.PortfolioPresignRequest; +import ditda.backend.domain.auth.dto.response.PortfolioPresignResponse; +import ditda.backend.domain.auth.service.DesignerAuthService; +import ditda.backend.domain.auth.service.EmailVerificationService; +import ditda.backend.domain.auth.service.SignupPortfolioService; +import ditda.backend.domain.user.service.UserService; import lombok.RequiredArgsConstructor; @Component @@ -20,7 +20,7 @@ public class DesignerAuthFacade { private final DesignerAuthService designerAuthService; private final EmailVerificationService emailVerificationService; - private final PortfolioService portfolioService; + private final SignupPortfolioService signupPortfolioService; private final UserService userService; public PortfolioPresignResponse issuePortfolioPresignedUrl(PortfolioPresignRequest request) { @@ -28,10 +28,10 @@ public PortfolioPresignResponse issuePortfolioPresignedUrl(PortfolioPresignReque // 이메일 인증 여부 확인 emailVerificationService.validateVerified(request.email()); - return portfolioService.generatePresignedUpload(request.contentType()); + return signupPortfolioService.generatePresignedUpload(request.contentType()); } - public DesignerAuthResult signup(DesignerSignupRequest request) { + public AuthResult signup(DesignerSignupRequest request) { // 이메일 인증 여부 확인 emailVerificationService.validateVerified(request.email()); @@ -42,17 +42,17 @@ public DesignerAuthResult signup(DesignerSignupRequest request) { // 포트폴리오 key 검증 List tempKeys = request.portfolioKeys() == null ? List.of() : request.portfolioKeys(); - portfolioService.validateKeys(tempKeys); + signupPortfolioService.validateKeys(tempKeys); // 임시('portfolio/tmp') -> 정식('portfolio/') 승격 - List portfolioKeys = portfolioService.promote(tempKeys); + List portfolioKeys = signupPortfolioService.promote(tempKeys); // 회원가입 - DesignerAuthResult result; + AuthResult result; try { result = designerAuthService.signup(request, portfolioKeys); } catch (Exception e) { - portfolioService.deleteFiles(portfolioKeys); + signupPortfolioService.deleteFiles(portfolioKeys); throw e; } diff --git a/src/main/java/ditda/backend/domain/auth/facade/InstructorAuthFacade.java b/src/main/java/ditda/backend/domain/auth/facade/InstructorAuthFacade.java new file mode 100644 index 0000000..266f463 --- /dev/null +++ b/src/main/java/ditda/backend/domain/auth/facade/InstructorAuthFacade.java @@ -0,0 +1,29 @@ +package ditda.backend.domain.auth.facade; + +import org.springframework.stereotype.Component; + +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.request.InstructorSignupRequest; +import ditda.backend.domain.auth.service.EmailVerificationService; +import ditda.backend.domain.auth.service.InstructorAuthService; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class InstructorAuthFacade { + + private final InstructorAuthService instructorAuthService; + private final EmailVerificationService emailVerificationService; + + public AuthResult signup(InstructorSignupRequest request) { + + emailVerificationService.validateVerified(request.email()); + + AuthResult result = instructorAuthService.signup(request); + + emailVerificationService.deleteVerified(request.email()); + + return result; + } + +} diff --git a/src/main/java/ditda/backend/domain/common/auth/mapper/AuthResponseMapper.java b/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java similarity index 52% rename from src/main/java/ditda/backend/domain/common/auth/mapper/AuthResponseMapper.java rename to src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java index f65e694..ea784f8 100644 --- a/src/main/java/ditda/backend/domain/common/auth/mapper/AuthResponseMapper.java +++ b/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java @@ -1,9 +1,10 @@ -package ditda.backend.domain.common.auth.mapper; +package ditda.backend.domain.auth.mapper; import org.springframework.stereotype.Component; -import ditda.backend.domain.common.auth.dto.AuthResult; -import ditda.backend.domain.common.auth.dto.response.LoginResponse; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.response.LoginResponse; +import ditda.backend.domain.auth.dto.response.SignupResponse; import ditda.backend.global.s3.S3UrlResolver; import lombok.RequiredArgsConstructor; @@ -21,4 +22,13 @@ public LoginResponse toLoginResponse(AuthResult result) { result.accessToken() ); } + + public SignupResponse toSignupResponse(AuthResult result) { + return new SignupResponse( + result.userId(), + result.name(), + s3UrlResolver.toPublicS3Url(result.profileImage()), + result.accessToken() + ); + } } diff --git a/src/main/java/ditda/backend/domain/designer/auth/notification/DesignerSignupMailer.java b/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupMailer.java similarity index 93% rename from src/main/java/ditda/backend/domain/designer/auth/notification/DesignerSignupMailer.java rename to src/main/java/ditda/backend/domain/auth/notification/DesignerSignupMailer.java index 5378d6f..a6d6d50 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/notification/DesignerSignupMailer.java +++ b/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupMailer.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.notification; +package ditda.backend.domain.auth.notification; import java.util.List; import java.util.Map; diff --git a/src/main/java/ditda/backend/domain/designer/auth/notification/DesignerSignupNotifier.java b/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupNotifier.java similarity index 92% rename from src/main/java/ditda/backend/domain/designer/auth/notification/DesignerSignupNotifier.java rename to src/main/java/ditda/backend/domain/auth/notification/DesignerSignupNotifier.java index 5f7a102..783e4cd 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/notification/DesignerSignupNotifier.java +++ b/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupNotifier.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.notification; +package ditda.backend.domain.auth.notification; import java.time.Duration; import java.util.List; @@ -7,7 +7,7 @@ import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; -import ditda.backend.domain.designer.auth.event.DesignerSignedUpEvent; +import ditda.backend.domain.auth.event.DesignerSignedUpEvent; import ditda.backend.global.config.AdminProperties; import ditda.backend.global.s3.S3PresignedUrlGenerator; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ditda/backend/domain/common/auth/notification/EmailVerificationMailer.java b/src/main/java/ditda/backend/domain/auth/notification/EmailVerificationMailer.java similarity index 90% rename from src/main/java/ditda/backend/domain/common/auth/notification/EmailVerificationMailer.java rename to src/main/java/ditda/backend/domain/auth/notification/EmailVerificationMailer.java index 1ff6bd6..e9041d0 100644 --- a/src/main/java/ditda/backend/domain/common/auth/notification/EmailVerificationMailer.java +++ b/src/main/java/ditda/backend/domain/auth/notification/EmailVerificationMailer.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.notification; +package ditda.backend.domain.auth.notification; import java.util.Map; diff --git a/src/main/java/ditda/backend/domain/common/auth/repository/RefreshTokenRepository.java b/src/main/java/ditda/backend/domain/auth/repository/RefreshTokenRepository.java similarity index 88% rename from src/main/java/ditda/backend/domain/common/auth/repository/RefreshTokenRepository.java rename to src/main/java/ditda/backend/domain/auth/repository/RefreshTokenRepository.java index faa28db..d68821b 100644 --- a/src/main/java/ditda/backend/domain/common/auth/repository/RefreshTokenRepository.java +++ b/src/main/java/ditda/backend/domain/auth/repository/RefreshTokenRepository.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.repository; +package ditda.backend.domain.auth.repository; import java.time.LocalDateTime; import java.util.Optional; @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import ditda.backend.domain.common.auth.entity.RefreshToken; +import ditda.backend.domain.auth.entity.RefreshToken; public interface RefreshTokenRepository extends JpaRepository { diff --git a/src/main/java/ditda/backend/domain/common/auth/scheduler/RefreshTokenScheduler.java b/src/main/java/ditda/backend/domain/auth/scheduler/RefreshTokenScheduler.java similarity index 84% rename from src/main/java/ditda/backend/domain/common/auth/scheduler/RefreshTokenScheduler.java rename to src/main/java/ditda/backend/domain/auth/scheduler/RefreshTokenScheduler.java index 64390dd..48a8c7c 100644 --- a/src/main/java/ditda/backend/domain/common/auth/scheduler/RefreshTokenScheduler.java +++ b/src/main/java/ditda/backend/domain/auth/scheduler/RefreshTokenScheduler.java @@ -1,9 +1,9 @@ -package ditda.backend.domain.common.auth.scheduler; +package ditda.backend.domain.auth.scheduler; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import ditda.backend.domain.common.auth.service.AuthService; +import ditda.backend.domain.auth.service.AuthService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ditda/backend/domain/common/auth/service/AuthService.java b/src/main/java/ditda/backend/domain/auth/service/AuthService.java similarity index 91% rename from src/main/java/ditda/backend/domain/common/auth/service/AuthService.java rename to src/main/java/ditda/backend/domain/auth/service/AuthService.java index ee487b2..0a74e02 100644 --- a/src/main/java/ditda/backend/domain/common/auth/service/AuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/AuthService.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.service; +package ditda.backend.domain.auth.service; import java.time.LocalDateTime; import java.util.UUID; @@ -7,13 +7,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.common.auth.dto.AuthResult; -import ditda.backend.domain.common.auth.dto.TokenResult; -import ditda.backend.domain.common.auth.dto.request.LoginRequest; -import ditda.backend.domain.common.auth.entity.RefreshToken; -import ditda.backend.domain.common.auth.repository.RefreshTokenRepository; -import ditda.backend.domain.common.user.entity.User; -import ditda.backend.domain.common.user.service.UserService; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.TokenResult; +import ditda.backend.domain.auth.dto.request.LoginRequest; +import ditda.backend.domain.auth.entity.RefreshToken; +import ditda.backend.domain.auth.repository.RefreshTokenRepository; +import ditda.backend.domain.user.entity.User; +import ditda.backend.domain.user.service.UserService; import ditda.backend.global.apipayload.code.GeneralErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.hash.RefreshTokenHasher; diff --git a/src/main/java/ditda/backend/domain/designer/auth/service/DesignerAuthService.java b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java similarity index 67% rename from src/main/java/ditda/backend/domain/designer/auth/service/DesignerAuthService.java rename to src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java index 75c229b..7114241 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/service/DesignerAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.service; +package ditda.backend.domain.auth.service; import java.time.LocalDateTime; import java.util.List; @@ -8,18 +8,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.common.auth.dto.TokenResult; -import ditda.backend.domain.common.auth.service.AuthService; -import ditda.backend.domain.common.term.dto.TermAgreement; -import ditda.backend.domain.common.term.service.TermService; -import ditda.backend.domain.common.user.entity.User; -import ditda.backend.domain.common.user.entity.enums.UserRole; -import ditda.backend.domain.common.user.service.UserService; -import ditda.backend.domain.designer.auth.dto.DesignerAuthResult; -import ditda.backend.domain.designer.auth.dto.request.DesignerSignupRequest; -import ditda.backend.domain.designer.auth.entity.Designer; -import ditda.backend.domain.designer.auth.event.DesignerSignedUpEvent; -import ditda.backend.domain.designer.auth.repository.DesignerRepository; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.TokenResult; +import ditda.backend.domain.auth.dto.request.DesignerSignupRequest; +import ditda.backend.domain.auth.event.DesignerSignedUpEvent; +import ditda.backend.domain.designer.entity.Designer; +import ditda.backend.domain.designer.repository.DesignerRepository; +import ditda.backend.domain.term.dto.TermAgreement; +import ditda.backend.domain.term.service.TermService; +import ditda.backend.domain.user.entity.User; +import ditda.backend.domain.user.entity.enums.UserRole; +import ditda.backend.domain.user.service.UserService; import lombok.RequiredArgsConstructor; @Service @@ -32,12 +31,12 @@ public class DesignerAuthService { private final UserService userService; private final TermService termService; private final AuthService authService; - private final PortfolioService portfolioService; + private final SignupPortfolioService signupPortfolioService; private final PasswordEncoder passwordEncoder; private final ApplicationEventPublisher eventPublisher; @Transactional - public DesignerAuthResult signup(DesignerSignupRequest request, List portfolioKeys) { + public AuthResult signup(DesignerSignupRequest request, List portfolioKeys) { // 아이디 및 이메일 검증 userService.validateUsernameAvailable(request.username()); @@ -68,7 +67,7 @@ public DesignerAuthResult signup(DesignerSignupRequest request, List por designerRepository.save(designer); // 포트폴리오 S3 key를 DB에 일괄 저장 - portfolioService.savePortfolios(designer, portfolioKeys); + signupPortfolioService.savePortfolios(designer, portfolioKeys); // accessToken&refreshToken 발급 TokenResult tokens = authService.issueTokens(user.getId()); @@ -81,7 +80,7 @@ public DesignerAuthResult signup(DesignerSignupRequest request, List por portfolioKeys )); - return new DesignerAuthResult( + return new AuthResult( user.getId(), user.getName(), user.getProfileImage(), diff --git a/src/main/java/ditda/backend/domain/common/auth/service/EmailVerificationService.java b/src/main/java/ditda/backend/domain/auth/service/EmailVerificationService.java similarity index 95% rename from src/main/java/ditda/backend/domain/common/auth/service/EmailVerificationService.java rename to src/main/java/ditda/backend/domain/auth/service/EmailVerificationService.java index 096b894..aaa0ede 100644 --- a/src/main/java/ditda/backend/domain/common/auth/service/EmailVerificationService.java +++ b/src/main/java/ditda/backend/domain/auth/service/EmailVerificationService.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.auth.service; +package ditda.backend.domain.auth.service; import java.security.SecureRandom; import java.time.Duration; @@ -6,7 +6,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; -import ditda.backend.domain.common.auth.exception.AuthErrorCode; +import ditda.backend.domain.auth.exception.AuthErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ditda/backend/domain/instructor/auth/service/InstructorAuthService.java b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java similarity index 64% rename from src/main/java/ditda/backend/domain/instructor/auth/service/InstructorAuthService.java rename to src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java index a60b14e..65b53fd 100644 --- a/src/main/java/ditda/backend/domain/instructor/auth/service/InstructorAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.instructor.auth.service; +package ditda.backend.domain.auth.service; import java.time.LocalDateTime; import java.util.List; @@ -7,17 +7,16 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.common.auth.dto.TokenResult; -import ditda.backend.domain.common.auth.service.AuthService; -import ditda.backend.domain.common.term.dto.TermAgreement; -import ditda.backend.domain.common.term.service.TermService; -import ditda.backend.domain.common.user.entity.User; -import ditda.backend.domain.common.user.entity.enums.UserRole; -import ditda.backend.domain.common.user.service.UserService; -import ditda.backend.domain.instructor.auth.dto.InstructorAuthResult; -import ditda.backend.domain.instructor.auth.dto.request.InstructorSignupRequest; -import ditda.backend.domain.instructor.auth.entity.Instructor; -import ditda.backend.domain.instructor.auth.repository.InstructorRepository; +import ditda.backend.domain.auth.dto.AuthResult; +import ditda.backend.domain.auth.dto.TokenResult; +import ditda.backend.domain.auth.dto.request.InstructorSignupRequest; +import ditda.backend.domain.instructor.entity.Instructor; +import ditda.backend.domain.instructor.repository.InstructorRepository; +import ditda.backend.domain.term.dto.TermAgreement; +import ditda.backend.domain.term.service.TermService; +import ditda.backend.domain.user.entity.User; +import ditda.backend.domain.user.entity.enums.UserRole; +import ditda.backend.domain.user.service.UserService; import lombok.RequiredArgsConstructor; @Service @@ -33,7 +32,7 @@ public class InstructorAuthService { private final PasswordEncoder passwordEncoder; @Transactional - public InstructorAuthResult signup(InstructorSignupRequest request) { + public AuthResult signup(InstructorSignupRequest request) { userService.validateUsernameAvailable(request.username()); userService.validateEmailAvailable(request.email()); @@ -55,7 +54,7 @@ public InstructorAuthResult signup(InstructorSignupRequest request) { TokenResult tokens = authService.issueTokens(user.getId()); - return new InstructorAuthResult( + return new AuthResult( user.getId(), user.getName(), user.getProfileImage(), diff --git a/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java b/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java similarity index 72% rename from src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java rename to src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java index 618ed80..ae493e6 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/service/PortfolioService.java +++ b/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.service; +package ditda.backend.domain.auth.service; import java.util.List; import java.util.Map; @@ -6,11 +6,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.designer.auth.dto.response.PortfolioPresignResponse; -import ditda.backend.domain.designer.auth.entity.Designer; -import ditda.backend.domain.designer.auth.entity.Portfolio; -import ditda.backend.domain.designer.auth.exception.DesignerErrorCode; -import ditda.backend.domain.designer.auth.repository.PortfolioRepository; +import ditda.backend.domain.auth.dto.response.PortfolioPresignResponse; +import ditda.backend.domain.auth.exception.PortfolioErrorCode; +import ditda.backend.domain.designer.entity.Designer; +import ditda.backend.domain.designer.entity.Portfolio; +import ditda.backend.domain.designer.repository.PortfolioRepository; import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.PresignedUpload; import ditda.backend.global.s3.S3Properties; @@ -20,7 +20,7 @@ @Service @RequiredArgsConstructor -public class PortfolioService { +public class SignupPortfolioService { private static final int MAX_PORTFOLIO_COUNT = 3; private static final String DIR = "portfolio"; @@ -39,7 +39,7 @@ public PortfolioPresignResponse generatePresignedUpload(String contentType) { // 파일 타입 검증 String extension = ALLOWED_CONTENT_TYPES.get(contentType); if (extension == null) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); + throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); } PresignedUpload upload = s3UploadManager.issueTempUpload(BUCKET, DIR, extension, contentType); @@ -50,27 +50,27 @@ public void validateKeys(List keys) { // 파일 개수 검증 if (keys.size() > MAX_PORTFOLIO_COUNT) { - throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_LIMIT_EXCEEDED); + throw new GeneralException(PortfolioErrorCode.PORTFOLIO_FILE_LIMIT_EXCEEDED); } // distinct key 검증 if (keys.size() != keys.stream().distinct().count()) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); + throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); } for (String key : keys) { // 파일 key 형식 검증 if (!s3UploadManager.isTempKey(key, DIR)) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); + throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); } // 파일 크기 검증 Long size = s3UploadManager.getObjectSize(BUCKET, key); if (size == null) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); // 미업로드 key + throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); // 미업로드 key } if (size > s3Properties.getMaxFileSize().toBytes()) { - throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_SIZE_EXCEEDED); + throw new GeneralException(PortfolioErrorCode.PORTFOLIO_FILE_SIZE_EXCEEDED); } } } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java similarity index 87% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java rename to src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java index d8a29af..beb0d3f 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionApplication.java +++ b/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java @@ -1,7 +1,8 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.application.entity; -import ditda.backend.domain.common.commission.entity.enums.ApplicationStatus; -import ditda.backend.domain.designer.auth.entity.Designer; +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.designer.entity.Designer; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java b/src/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.java similarity index 65% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java rename to src/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.java index d9baa76..310b8a3 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ApplicationStatus.java +++ b/src/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.application.entity.enums; public enum ApplicationStatus { PENDING, // 지원 완료 diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationRepository.java b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java similarity index 53% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationRepository.java rename to src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java index 43601a3..31a23f2 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionApplicationRepository.java +++ b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.application.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.CommissionApplication; +import ditda.backend.domain.commission.application.entity.CommissionApplication; public interface CommissionApplicationRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java similarity index 85% rename from src/main/java/ditda/backend/domain/common/commission/entity/Commission.java rename to src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index af3eb38..552bd6f 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -1,14 +1,14 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.core.entity; import java.time.LocalDate; -import ditda.backend.domain.common.commission.entity.enums.CategoryType; -import ditda.backend.domain.common.commission.entity.enums.ColorSelectionMode; -import ditda.backend.domain.common.commission.entity.enums.CommissionStatus; -import ditda.backend.domain.common.commission.entity.enums.PlanCode; -import ditda.backend.domain.common.commission.entity.enums.Size; -import ditda.backend.domain.designer.auth.entity.Designer; -import ditda.backend.domain.instructor.auth.entity.Instructor; +import ditda.backend.domain.commission.core.entity.enums.CategoryType; +import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; +import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; +import ditda.backend.domain.commission.core.entity.enums.PlanCode; +import ditda.backend.domain.commission.core.entity.enums.Size; +import ditda.backend.domain.designer.entity.Designer; +import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java b/src/main/java/ditda/backend/domain/commission/core/entity/CommissionColor.java similarity index 92% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java rename to src/main/java/ditda/backend/domain/commission/core/entity/CommissionColor.java index e3704c2..7623c29 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionColor.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/CommissionColor.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.core.entity; -import ditda.backend.domain.common.commission.entity.enums.ColorRole; +import ditda.backend.domain.commission.core.entity.enums.ColorRole; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java b/src/main/java/ditda/backend/domain/commission/core/entity/CommissionConcept.java similarity index 92% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java rename to src/main/java/ditda/backend/domain/commission/core/entity/CommissionConcept.java index 1c63936..ac7663c 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionConcept.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/CommissionConcept.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.core.entity; -import ditda.backend.domain.common.commission.entity.enums.ConceptTag; +import ditda.backend.domain.commission.core.entity.enums.ConceptTag; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java b/src/main/java/ditda/backend/domain/commission/core/entity/CommissionFile.java similarity index 92% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java rename to src/main/java/ditda/backend/domain/commission/core/entity/CommissionFile.java index abd90ff..611fbf6 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionFile.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/CommissionFile.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.core.entity; -import ditda.backend.domain.common.commission.entity.enums.FileKind; +import ditda.backend.domain.commission.core.entity.enums.FileKind; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/Textbook.java b/src/main/java/ditda/backend/domain/commission/core/entity/Textbook.java similarity index 95% rename from src/main/java/ditda/backend/domain/common/commission/entity/Textbook.java rename to src/main/java/ditda/backend/domain/commission/core/entity/Textbook.java index 48132f6..89ff1ff 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/Textbook.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Textbook.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.core.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/TextbookPage.java b/src/main/java/ditda/backend/domain/commission/core/entity/TextbookPage.java similarity index 93% rename from src/main/java/ditda/backend/domain/common/commission/entity/TextbookPage.java rename to src/main/java/ditda/backend/domain/commission/core/entity/TextbookPage.java index 5c12ffb..8d75625 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/TextbookPage.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/TextbookPage.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.core.entity; -import ditda.backend.domain.common.commission.entity.enums.PageType; +import ditda.backend.domain.commission.core.entity.enums.PageType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/CategoryType.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/CategoryType.java similarity index 71% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/CategoryType.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/CategoryType.java index f58d628..d9d62df 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/CategoryType.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/CategoryType.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; public enum CategoryType { // 유인물 - 교재 diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/enums/ColorRole.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ColorRole.java new file mode 100644 index 0000000..735f1a1 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ColorRole.java @@ -0,0 +1,7 @@ +package ditda.backend.domain.commission.core.entity.enums; + +public enum ColorRole { + MAIN, + SUB1, + SUB2 +} diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorSelectionMode.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ColorSelectionMode.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorSelectionMode.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/ColorSelectionMode.java index e670a80..60c3c70 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorSelectionMode.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ColorSelectionMode.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; public enum ColorSelectionMode { USER_SELECTED, diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/CommissionStatus.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/CommissionStatus.java similarity index 80% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/CommissionStatus.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/CommissionStatus.java index dc57706..8cd9e4b 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/CommissionStatus.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/CommissionStatus.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; public enum CommissionStatus { PENDING, // 결제 대기 diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptCategory.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ConceptCategory.java similarity index 81% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptCategory.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/ConceptCategory.java index 12e8c63..4338e7f 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptCategory.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ConceptCategory.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ConceptTag.java similarity index 93% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/ConceptTag.java index 5731321..dae08f2 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ConceptTag.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/ConceptTag.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/FileKind.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java similarity index 58% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/FileKind.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java index f503b5b..ebb820d 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/FileKind.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; public enum FileKind { MATERIAL, // 자료 첨부 diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PageType.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/PageType.java similarity index 86% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/PageType.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/PageType.java index c523662..10c8fc2 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PageType.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/PageType.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; public enum PageType { INSTRUCTOR_PROFILE, // 강사 프로필 diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/PlanCode.java similarity index 89% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/PlanCode.java index 6e112b3..309d7a0 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/PlanCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/PlanCode.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/Size.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/Size.java index b0291b6..53e7bff 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/Size.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/Size.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.core.entity.enums; public enum Size { A4, diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionColorRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionColorRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionColorRepository.java rename to src/main/java/ditda/backend/domain/commission/core/repository/CommissionColorRepository.java index 4e9aca1..f4ac48e 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionColorRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionColorRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.core.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.CommissionColor; +import ditda.backend.domain.commission.core.entity.CommissionColor; public interface CommissionColorRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionConceptRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionConceptRepository.java similarity index 55% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionConceptRepository.java rename to src/main/java/ditda/backend/domain/commission/core/repository/CommissionConceptRepository.java index 84aca9d..95fcf16 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionConceptRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionConceptRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.core.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.CommissionConcept; +import ditda.backend.domain.commission.core.entity.CommissionConcept; public interface CommissionConceptRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionFileRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionFileRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionFileRepository.java rename to src/main/java/ditda/backend/domain/commission/core/repository/CommissionFileRepository.java index e147838..df390d0 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionFileRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionFileRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.core.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.CommissionFile; +import ditda.backend.domain.commission.core.entity.CommissionFile; public interface CommissionFileRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionRepository.java rename to src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java index 7ac0e11..fd75942 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.core.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.Commission; +import ditda.backend.domain.commission.core.entity.Commission; public interface CommissionRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/TextbookPageRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/TextbookPageRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/TextbookPageRepository.java rename to src/main/java/ditda/backend/domain/commission/core/repository/TextbookPageRepository.java index 8e401be..9215c17 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/TextbookPageRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/TextbookPageRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.core.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.TextbookPage; +import ditda.backend.domain.commission.core.entity.TextbookPage; public interface TextbookPageRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/TextbookRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/TextbookRepository.java similarity index 53% rename from src/main/java/ditda/backend/domain/common/commission/repository/TextbookRepository.java rename to src/main/java/ditda/backend/domain/commission/core/repository/TextbookRepository.java index 5dedabe..f81eb5c 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/TextbookRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/TextbookRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.core.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.Textbook; +import ditda.backend.domain.commission.core.entity.Textbook; public interface TextbookRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java similarity index 93% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java rename to src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java index c8f335a..7f00f56 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraft.java +++ b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java @@ -1,5 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.draft.entity; +import ditda.backend.domain.commission.application.entity.CommissionApplication; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraftFile.java similarity index 94% rename from src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java rename to src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraftFile.java index 2c7ae63..4872c3f 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/CommissionDraftFile.java +++ b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraftFile.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.draft.entity; -import ditda.backend.domain.common.commission.entity.enums.WatermarkStatus; +import ditda.backend.domain.commission.draft.entity.enums.WatermarkStatus; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/WatermarkStatus.java b/src/main/java/ditda/backend/domain/commission/draft/entity/enums/WatermarkStatus.java similarity index 73% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/WatermarkStatus.java rename to src/main/java/ditda/backend/domain/commission/draft/entity/enums/WatermarkStatus.java index e011bdc..9318383 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/WatermarkStatus.java +++ b/src/main/java/ditda/backend/domain/commission/draft/entity/enums/WatermarkStatus.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.draft.entity.enums; public enum WatermarkStatus { PROCESSING, // 워터마크 처리 중 diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftFileRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java similarity index 55% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftFileRepository.java rename to src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java index b468cbc..8c6786e 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftFileRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.draft.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.CommissionDraftFile; +import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; public interface CommissionDraftFileRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftRepository.java rename to src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java index 189e5b7..a7b5a96 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/CommissionDraftRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.draft.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.CommissionDraft; +import ditda.backend.domain.commission.draft.entity.CommissionDraft; public interface CommissionDraftRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionDetail.java similarity index 91% rename from src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java rename to src/main/java/ditda/backend/domain/commission/revision/entity/RevisionDetail.java index 0fddd82..f138e3b 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionDetail.java +++ b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionDetail.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.revision.entity; -import ditda.backend.domain.common.commission.entity.enums.RevisionCategory; +import ditda.backend.domain.commission.revision.entity.enums.RevisionCategory; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionRequest.java similarity index 89% rename from src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java rename to src/main/java/ditda/backend/domain/commission/revision/entity/RevisionRequest.java index 68a584a..6c73fac 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionRequest.java +++ b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionRequest.java @@ -1,5 +1,7 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.revision.entity; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionResponse.java b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionResponse.java similarity index 91% rename from src/main/java/ditda/backend/domain/common/commission/entity/RevisionResponse.java rename to src/main/java/ditda/backend/domain/commission/revision/entity/RevisionResponse.java index 7a5339d..f7ac173 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/RevisionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionResponse.java @@ -1,5 +1,6 @@ -package ditda.backend.domain.common.commission.entity; +package ditda.backend.domain.commission.revision.entity; +import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/RevisionCategory.java b/src/main/java/ditda/backend/domain/commission/revision/entity/enums/RevisionCategory.java similarity index 75% rename from src/main/java/ditda/backend/domain/common/commission/entity/enums/RevisionCategory.java rename to src/main/java/ditda/backend/domain/commission/revision/entity/enums/RevisionCategory.java index ee208dc..c3df948 100644 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/RevisionCategory.java +++ b/src/main/java/ditda/backend/domain/commission/revision/entity/enums/RevisionCategory.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.commission.entity.enums; +package ditda.backend.domain.commission.revision.entity.enums; public enum RevisionCategory { LAYOUT, // 레이아웃 수정 diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionDetailRepository.java b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionDetailRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/RevisionDetailRepository.java rename to src/main/java/ditda/backend/domain/commission/revision/repository/RevisionDetailRepository.java index a5fade7..1a2207b 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionDetailRepository.java +++ b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionDetailRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.revision.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.RevisionDetail; +import ditda.backend.domain.commission.revision.entity.RevisionDetail; public interface RevisionDetailRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionRequestRepository.java b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/RevisionRequestRepository.java rename to src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java index 359ad80..d011fba 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionRequestRepository.java +++ b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.revision.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.RevisionRequest; +import ditda.backend.domain.commission.revision.entity.RevisionRequest; public interface RevisionRequestRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionResponseRepository.java b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionResponseRepository.java similarity index 54% rename from src/main/java/ditda/backend/domain/common/commission/repository/RevisionResponseRepository.java rename to src/main/java/ditda/backend/domain/commission/revision/repository/RevisionResponseRepository.java index be1bffa..4f6db6f 100644 --- a/src/main/java/ditda/backend/domain/common/commission/repository/RevisionResponseRepository.java +++ b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionResponseRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.commission.repository; +package ditda.backend.domain.commission.revision.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.commission.entity.RevisionResponse; +import ditda.backend.domain.commission.revision.entity.RevisionResponse; public interface RevisionResponseRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java b/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java deleted file mode 100644 index a89288a..0000000 --- a/src/main/java/ditda/backend/domain/common/commission/entity/enums/ColorRole.java +++ /dev/null @@ -1,7 +0,0 @@ -package ditda.backend.domain.common.commission.entity.enums; - -public enum ColorRole { - MAIN, - SUB1, - SUB2 -} diff --git a/src/main/java/ditda/backend/domain/common/term/dto/TermAgreement.java b/src/main/java/ditda/backend/domain/common/term/dto/TermAgreement.java deleted file mode 100644 index f86f53b..0000000 --- a/src/main/java/ditda/backend/domain/common/term/dto/TermAgreement.java +++ /dev/null @@ -1,10 +0,0 @@ -package ditda.backend.domain.common.term.dto; - -import ditda.backend.domain.common.term.entity.enums.TermType; - -public record TermAgreement( - TermType type, - String version, - boolean isAgreed -) { -} diff --git a/src/main/java/ditda/backend/domain/common/user/entity/enums/UserRole.java b/src/main/java/ditda/backend/domain/common/user/entity/enums/UserRole.java deleted file mode 100644 index c98040b..0000000 --- a/src/main/java/ditda/backend/domain/common/user/entity/enums/UserRole.java +++ /dev/null @@ -1,6 +0,0 @@ -package ditda.backend.domain.common.user.entity.enums; - -public enum UserRole { - DESIGNER, - INSTRUCTOR -} diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/DesignerAuthResult.java b/src/main/java/ditda/backend/domain/designer/auth/dto/DesignerAuthResult.java deleted file mode 100644 index c50df81..0000000 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/DesignerAuthResult.java +++ /dev/null @@ -1,15 +0,0 @@ -package ditda.backend.domain.designer.auth.dto; - -public record DesignerAuthResult( - - Long userId, - - String name, - - String profileImage, - - String accessToken, - - String refreshToken -) { -} diff --git a/src/main/java/ditda/backend/domain/designer/auth/dto/response/DesignerSignupResponse.java b/src/main/java/ditda/backend/domain/designer/auth/dto/response/DesignerSignupResponse.java deleted file mode 100644 index c9dcfa3..0000000 --- a/src/main/java/ditda/backend/domain/designer/auth/dto/response/DesignerSignupResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package ditda.backend.domain.designer.auth.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "디자이너 회원가입 응답") -public record DesignerSignupResponse( - - @Schema(description = "User ID") - Long userId, - - @Schema(description = "사용자 이름", example = "홍길동") - String name, - - @Schema(description = "프로필 이미지", example = "https://example.com/profile/default.png") - String profileImageUrl, - - @Schema(description = "Access Token") - String accessToken -) { -} diff --git a/src/main/java/ditda/backend/domain/designer/auth/mapper/DesignerAuthResponseMapper.java b/src/main/java/ditda/backend/domain/designer/auth/mapper/DesignerAuthResponseMapper.java deleted file mode 100644 index 466fc5b..0000000 --- a/src/main/java/ditda/backend/domain/designer/auth/mapper/DesignerAuthResponseMapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package ditda.backend.domain.designer.auth.mapper; - -import org.springframework.stereotype.Component; - -import ditda.backend.domain.designer.auth.dto.DesignerAuthResult; -import ditda.backend.domain.designer.auth.dto.response.DesignerSignupResponse; -import ditda.backend.global.s3.S3UrlResolver; -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class DesignerAuthResponseMapper { - - private final S3UrlResolver s3UrlResolver; - - public DesignerSignupResponse toSignupResponse(DesignerAuthResult result) { - return new DesignerSignupResponse( - result.userId(), - result.name(), - s3UrlResolver.toPublicS3Url(result.profileImage()), - result.accessToken() - ); - } -} diff --git a/src/main/java/ditda/backend/domain/designer/auth/entity/Designer.java b/src/main/java/ditda/backend/domain/designer/entity/Designer.java similarity index 91% rename from src/main/java/ditda/backend/domain/designer/auth/entity/Designer.java rename to src/main/java/ditda/backend/domain/designer/entity/Designer.java index 47eaf49..bc425fd 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/entity/Designer.java +++ b/src/main/java/ditda/backend/domain/designer/entity/Designer.java @@ -1,7 +1,7 @@ -package ditda.backend.domain.designer.auth.entity; +package ditda.backend.domain.designer.entity; -import ditda.backend.domain.common.user.entity.User; -import ditda.backend.domain.designer.auth.entity.enums.BankName; +import ditda.backend.domain.designer.entity.enums.BankName; +import ditda.backend.domain.user.entity.User; import ditda.backend.global.encryption.AesEncryptConverter; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; diff --git a/src/main/java/ditda/backend/domain/designer/auth/entity/Portfolio.java b/src/main/java/ditda/backend/domain/designer/entity/Portfolio.java similarity index 95% rename from src/main/java/ditda/backend/domain/designer/auth/entity/Portfolio.java rename to src/main/java/ditda/backend/domain/designer/entity/Portfolio.java index 5063d5c..9568631 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/entity/Portfolio.java +++ b/src/main/java/ditda/backend/domain/designer/entity/Portfolio.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.entity; +package ditda.backend.domain.designer.entity; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; diff --git a/src/main/java/ditda/backend/domain/designer/auth/entity/enums/BankName.java b/src/main/java/ditda/backend/domain/designer/entity/enums/BankName.java similarity index 89% rename from src/main/java/ditda/backend/domain/designer/auth/entity/enums/BankName.java rename to src/main/java/ditda/backend/domain/designer/entity/enums/BankName.java index 358729a..6a577ad 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/entity/enums/BankName.java +++ b/src/main/java/ditda/backend/domain/designer/entity/enums/BankName.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.designer.auth.entity.enums; +package ditda.backend.domain.designer.entity.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ditda/backend/domain/designer/auth/repository/DesignerRepository.java b/src/main/java/ditda/backend/domain/designer/repository/DesignerRepository.java similarity index 55% rename from src/main/java/ditda/backend/domain/designer/auth/repository/DesignerRepository.java rename to src/main/java/ditda/backend/domain/designer/repository/DesignerRepository.java index ac23d38..cfbc997 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/repository/DesignerRepository.java +++ b/src/main/java/ditda/backend/domain/designer/repository/DesignerRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.designer.auth.repository; +package ditda.backend.domain.designer.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.designer.auth.entity.Designer; +import ditda.backend.domain.designer.entity.Designer; public interface DesignerRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/designer/auth/repository/PortfolioRepository.java b/src/main/java/ditda/backend/domain/designer/repository/PortfolioRepository.java similarity index 55% rename from src/main/java/ditda/backend/domain/designer/auth/repository/PortfolioRepository.java rename to src/main/java/ditda/backend/domain/designer/repository/PortfolioRepository.java index d1c784c..7288954 100644 --- a/src/main/java/ditda/backend/domain/designer/auth/repository/PortfolioRepository.java +++ b/src/main/java/ditda/backend/domain/designer/repository/PortfolioRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.designer.auth.repository; +package ditda.backend.domain.designer.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.designer.auth.entity.Portfolio; +import ditda.backend.domain.designer.entity.Portfolio; public interface PortfolioRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/instructor/auth/dto/InstructorAuthResult.java b/src/main/java/ditda/backend/domain/instructor/auth/dto/InstructorAuthResult.java deleted file mode 100644 index 1bc94b7..0000000 --- a/src/main/java/ditda/backend/domain/instructor/auth/dto/InstructorAuthResult.java +++ /dev/null @@ -1,15 +0,0 @@ -package ditda.backend.domain.instructor.auth.dto; - -public record InstructorAuthResult( - - Long userId, - - String name, - - String profileImage, - - String accessToken, - - String refreshToken -) { -} diff --git a/src/main/java/ditda/backend/domain/instructor/auth/facade/InstructorAuthFacade.java b/src/main/java/ditda/backend/domain/instructor/auth/facade/InstructorAuthFacade.java deleted file mode 100644 index 43d7f08..0000000 --- a/src/main/java/ditda/backend/domain/instructor/auth/facade/InstructorAuthFacade.java +++ /dev/null @@ -1,29 +0,0 @@ -package ditda.backend.domain.instructor.auth.facade; - -import org.springframework.stereotype.Component; - -import ditda.backend.domain.common.auth.service.EmailVerificationService; -import ditda.backend.domain.instructor.auth.dto.InstructorAuthResult; -import ditda.backend.domain.instructor.auth.dto.request.InstructorSignupRequest; -import ditda.backend.domain.instructor.auth.service.InstructorAuthService; -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class InstructorAuthFacade { - - private final InstructorAuthService instructorAuthService; - private final EmailVerificationService emailVerificationService; - - public InstructorAuthResult signup(InstructorSignupRequest request) { - - emailVerificationService.validateVerified(request.email()); - - InstructorAuthResult result = instructorAuthService.signup(request); - - emailVerificationService.deleteVerified(request.email()); - - return result; - } - -} diff --git a/src/main/java/ditda/backend/domain/instructor/auth/mapper/InstructorAuthResponseMapper.java b/src/main/java/ditda/backend/domain/instructor/auth/mapper/InstructorAuthResponseMapper.java deleted file mode 100644 index f368efd..0000000 --- a/src/main/java/ditda/backend/domain/instructor/auth/mapper/InstructorAuthResponseMapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package ditda.backend.domain.instructor.auth.mapper; - -import org.springframework.stereotype.Component; - -import ditda.backend.domain.instructor.auth.dto.InstructorAuthResult; -import ditda.backend.domain.instructor.auth.dto.response.InstructorSignupResponse; -import ditda.backend.global.s3.S3UrlResolver; -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class InstructorAuthResponseMapper { - - private final S3UrlResolver s3UrlResolver; - - public InstructorSignupResponse toSignupResponse(InstructorAuthResult result) { - return new InstructorSignupResponse( - result.userId(), - result.name(), - s3UrlResolver.toPublicS3Url(result.profileImage()), - result.accessToken() - ); - } -} diff --git a/src/main/java/ditda/backend/domain/instructor/auth/entity/Instructor.java b/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java similarity index 89% rename from src/main/java/ditda/backend/domain/instructor/auth/entity/Instructor.java rename to src/main/java/ditda/backend/domain/instructor/entity/Instructor.java index cca971c..8578e45 100644 --- a/src/main/java/ditda/backend/domain/instructor/auth/entity/Instructor.java +++ b/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.instructor.auth.entity; +package ditda.backend.domain.instructor.entity; -import ditda.backend.domain.common.user.entity.User; +import ditda.backend.domain.user.entity.User; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/instructor/auth/repository/InstructorRepository.java b/src/main/java/ditda/backend/domain/instructor/repository/InstructorRepository.java similarity index 55% rename from src/main/java/ditda/backend/domain/instructor/auth/repository/InstructorRepository.java rename to src/main/java/ditda/backend/domain/instructor/repository/InstructorRepository.java index 80148b2..73fffa1 100644 --- a/src/main/java/ditda/backend/domain/instructor/auth/repository/InstructorRepository.java +++ b/src/main/java/ditda/backend/domain/instructor/repository/InstructorRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.instructor.auth.repository; +package ditda.backend.domain.instructor.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.instructor.auth.entity.Instructor; +import ditda.backend.domain.instructor.entity.Instructor; public interface InstructorRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java b/src/main/java/ditda/backend/domain/payment/entity/Payment.java similarity index 89% rename from src/main/java/ditda/backend/domain/common/payment/entity/Payment.java rename to src/main/java/ditda/backend/domain/payment/entity/Payment.java index 84765e2..827667f 100644 --- a/src/main/java/ditda/backend/domain/common/payment/entity/Payment.java +++ b/src/main/java/ditda/backend/domain/payment/entity/Payment.java @@ -1,9 +1,9 @@ -package ditda.backend.domain.common.payment.entity; +package ditda.backend.domain.payment.entity; import java.time.LocalDateTime; -import ditda.backend.domain.common.commission.entity.Commission; -import ditda.backend.domain.common.payment.entity.enums.PaymentStatus; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.payment.entity.enums.PaymentStatus; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/payment/entity/enums/PaymentStatus.java b/src/main/java/ditda/backend/domain/payment/entity/enums/PaymentStatus.java similarity index 74% rename from src/main/java/ditda/backend/domain/common/payment/entity/enums/PaymentStatus.java rename to src/main/java/ditda/backend/domain/payment/entity/enums/PaymentStatus.java index 7dae8e8..9cd8d0d 100644 --- a/src/main/java/ditda/backend/domain/common/payment/entity/enums/PaymentStatus.java +++ b/src/main/java/ditda/backend/domain/payment/entity/enums/PaymentStatus.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.payment.entity.enums; +package ditda.backend.domain.payment.entity.enums; public enum PaymentStatus { PENDING, // 외주 신청 + 입금 확인 대기 diff --git a/src/main/java/ditda/backend/domain/common/payment/repository/PaymentRepository.java b/src/main/java/ditda/backend/domain/payment/repository/PaymentRepository.java similarity index 55% rename from src/main/java/ditda/backend/domain/common/payment/repository/PaymentRepository.java rename to src/main/java/ditda/backend/domain/payment/repository/PaymentRepository.java index 3973cbc..242eafb 100644 --- a/src/main/java/ditda/backend/domain/common/payment/repository/PaymentRepository.java +++ b/src/main/java/ditda/backend/domain/payment/repository/PaymentRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.payment.repository; +package ditda.backend.domain.payment.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.payment.entity.Payment; +import ditda.backend.domain.payment.entity.Payment; public interface PaymentRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/term/dto/TermAgreement.java b/src/main/java/ditda/backend/domain/term/dto/TermAgreement.java new file mode 100644 index 0000000..732fe8d --- /dev/null +++ b/src/main/java/ditda/backend/domain/term/dto/TermAgreement.java @@ -0,0 +1,10 @@ +package ditda.backend.domain.term.dto; + +import ditda.backend.domain.term.entity.enums.TermType; + +public record TermAgreement( + TermType type, + String version, + boolean isAgreed +) { +} diff --git a/src/main/java/ditda/backend/domain/common/term/entity/PaymentTerm.java b/src/main/java/ditda/backend/domain/term/entity/PaymentTerm.java similarity index 92% rename from src/main/java/ditda/backend/domain/common/term/entity/PaymentTerm.java rename to src/main/java/ditda/backend/domain/term/entity/PaymentTerm.java index f730ba1..a2a62b0 100644 --- a/src/main/java/ditda/backend/domain/common/term/entity/PaymentTerm.java +++ b/src/main/java/ditda/backend/domain/term/entity/PaymentTerm.java @@ -1,6 +1,6 @@ -package ditda.backend.domain.common.term.entity; +package ditda.backend.domain.term.entity; -import ditda.backend.domain.common.payment.entity.Payment; +import ditda.backend.domain.payment.entity.Payment; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/term/entity/UserTerm.java b/src/main/java/ditda/backend/domain/term/entity/UserTerm.java similarity index 90% rename from src/main/java/ditda/backend/domain/common/term/entity/UserTerm.java rename to src/main/java/ditda/backend/domain/term/entity/UserTerm.java index a1aba2c..a8469e7 100644 --- a/src/main/java/ditda/backend/domain/common/term/entity/UserTerm.java +++ b/src/main/java/ditda/backend/domain/term/entity/UserTerm.java @@ -1,7 +1,7 @@ -package ditda.backend.domain.common.term.entity; +package ditda.backend.domain.term.entity; -import ditda.backend.domain.common.term.entity.enums.TermType; -import ditda.backend.domain.common.user.entity.User; +import ditda.backend.domain.term.entity.enums.TermType; +import ditda.backend.domain.user.entity.User; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/common/term/entity/enums/TermType.java b/src/main/java/ditda/backend/domain/term/entity/enums/TermType.java similarity index 63% rename from src/main/java/ditda/backend/domain/common/term/entity/enums/TermType.java rename to src/main/java/ditda/backend/domain/term/entity/enums/TermType.java index 974f45a..494076c 100644 --- a/src/main/java/ditda/backend/domain/common/term/entity/enums/TermType.java +++ b/src/main/java/ditda/backend/domain/term/entity/enums/TermType.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.term.entity.enums; +package ditda.backend.domain.term.entity.enums; public enum TermType { SERVICE, diff --git a/src/main/java/ditda/backend/domain/common/term/exception/TermErrorCode.java b/src/main/java/ditda/backend/domain/term/exception/TermErrorCode.java similarity index 89% rename from src/main/java/ditda/backend/domain/common/term/exception/TermErrorCode.java rename to src/main/java/ditda/backend/domain/term/exception/TermErrorCode.java index d1fe6bb..187eba8 100644 --- a/src/main/java/ditda/backend/domain/common/term/exception/TermErrorCode.java +++ b/src/main/java/ditda/backend/domain/term/exception/TermErrorCode.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.term.exception; +package ditda.backend.domain.term.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/ditda/backend/domain/common/term/repository/PaymentTermRepository.java b/src/main/java/ditda/backend/domain/term/repository/PaymentTermRepository.java similarity index 56% rename from src/main/java/ditda/backend/domain/common/term/repository/PaymentTermRepository.java rename to src/main/java/ditda/backend/domain/term/repository/PaymentTermRepository.java index bd24aa9..dae0db3 100644 --- a/src/main/java/ditda/backend/domain/common/term/repository/PaymentTermRepository.java +++ b/src/main/java/ditda/backend/domain/term/repository/PaymentTermRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.term.repository; +package ditda.backend.domain.term.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.term.entity.PaymentTerm; +import ditda.backend.domain.term.entity.PaymentTerm; public interface PaymentTermRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/term/repository/UserTermRepository.java b/src/main/java/ditda/backend/domain/term/repository/UserTermRepository.java similarity index 56% rename from src/main/java/ditda/backend/domain/common/term/repository/UserTermRepository.java rename to src/main/java/ditda/backend/domain/term/repository/UserTermRepository.java index 4084c46..8e51f01 100644 --- a/src/main/java/ditda/backend/domain/common/term/repository/UserTermRepository.java +++ b/src/main/java/ditda/backend/domain/term/repository/UserTermRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.term.repository; +package ditda.backend.domain.term.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.term.entity.UserTerm; +import ditda.backend.domain.term.entity.UserTerm; public interface UserTermRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/common/term/service/TermService.java b/src/main/java/ditda/backend/domain/term/service/TermService.java similarity index 73% rename from src/main/java/ditda/backend/domain/common/term/service/TermService.java rename to src/main/java/ditda/backend/domain/term/service/TermService.java index 00aabfb..fa6700c 100644 --- a/src/main/java/ditda/backend/domain/common/term/service/TermService.java +++ b/src/main/java/ditda/backend/domain/term/service/TermService.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.term.service; +package ditda.backend.domain.term.service; import java.util.List; import java.util.Set; @@ -6,12 +6,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.common.term.dto.TermAgreement; -import ditda.backend.domain.common.term.entity.UserTerm; -import ditda.backend.domain.common.term.entity.enums.TermType; -import ditda.backend.domain.common.term.exception.TermErrorCode; -import ditda.backend.domain.common.term.repository.UserTermRepository; -import ditda.backend.domain.common.user.entity.User; +import ditda.backend.domain.term.dto.TermAgreement; +import ditda.backend.domain.term.entity.UserTerm; +import ditda.backend.domain.term.entity.enums.TermType; +import ditda.backend.domain.term.exception.TermErrorCode; +import ditda.backend.domain.term.repository.UserTermRepository; +import ditda.backend.domain.user.entity.User; import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ditda/backend/domain/common/user/entity/User.java b/src/main/java/ditda/backend/domain/user/entity/User.java similarity index 94% rename from src/main/java/ditda/backend/domain/common/user/entity/User.java rename to src/main/java/ditda/backend/domain/user/entity/User.java index a461c40..b930366 100644 --- a/src/main/java/ditda/backend/domain/common/user/entity/User.java +++ b/src/main/java/ditda/backend/domain/user/entity/User.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.common.user.entity; +package ditda.backend.domain.user.entity; import java.time.LocalDateTime; -import ditda.backend.domain.common.user.entity.enums.UserRole; +import ditda.backend.domain.user.entity.enums.UserRole; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/user/entity/enums/UserRole.java b/src/main/java/ditda/backend/domain/user/entity/enums/UserRole.java new file mode 100644 index 0000000..4796c5d --- /dev/null +++ b/src/main/java/ditda/backend/domain/user/entity/enums/UserRole.java @@ -0,0 +1,6 @@ +package ditda.backend.domain.user.entity.enums; + +public enum UserRole { + DESIGNER, + INSTRUCTOR +} diff --git a/src/main/java/ditda/backend/domain/common/user/exception/UserErrorCode.java b/src/main/java/ditda/backend/domain/user/exception/UserErrorCode.java similarity index 92% rename from src/main/java/ditda/backend/domain/common/user/exception/UserErrorCode.java rename to src/main/java/ditda/backend/domain/user/exception/UserErrorCode.java index 94031e1..4bc9678 100644 --- a/src/main/java/ditda/backend/domain/common/user/exception/UserErrorCode.java +++ b/src/main/java/ditda/backend/domain/user/exception/UserErrorCode.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.common.user.exception; +package ditda.backend.domain.user.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/ditda/backend/domain/common/user/repository/UserRepository.java b/src/main/java/ditda/backend/domain/user/repository/UserRepository.java similarity index 73% rename from src/main/java/ditda/backend/domain/common/user/repository/UserRepository.java rename to src/main/java/ditda/backend/domain/user/repository/UserRepository.java index 5f7fb94..9f66fa6 100644 --- a/src/main/java/ditda/backend/domain/common/user/repository/UserRepository.java +++ b/src/main/java/ditda/backend/domain/user/repository/UserRepository.java @@ -1,10 +1,10 @@ -package ditda.backend.domain.common.user.repository; +package ditda.backend.domain.user.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.common.user.entity.User; +import ditda.backend.domain.user.entity.User; public interface UserRepository extends JpaRepository { boolean existsByUsername(String username); diff --git a/src/main/java/ditda/backend/domain/common/user/service/UserService.java b/src/main/java/ditda/backend/domain/user/service/UserService.java similarity index 84% rename from src/main/java/ditda/backend/domain/common/user/service/UserService.java rename to src/main/java/ditda/backend/domain/user/service/UserService.java index e7c287e..491343e 100644 --- a/src/main/java/ditda/backend/domain/common/user/service/UserService.java +++ b/src/main/java/ditda/backend/domain/user/service/UserService.java @@ -1,14 +1,14 @@ -package ditda.backend.domain.common.user.service; +package ditda.backend.domain.user.service; import java.time.LocalDateTime; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.common.user.entity.User; -import ditda.backend.domain.common.user.entity.enums.UserRole; -import ditda.backend.domain.common.user.exception.UserErrorCode; -import ditda.backend.domain.common.user.repository.UserRepository; +import ditda.backend.domain.user.entity.User; +import ditda.backend.domain.user.entity.enums.UserRole; +import ditda.backend.domain.user.exception.UserErrorCode; +import ditda.backend.domain.user.repository.UserRepository; import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; From f9c6c43c7c2a58b7bd9adb9bf6598eebb605fb94 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Tue, 2 Jun 2026 17:33:08 +0900 Subject: [PATCH 023/102] =?UTF-8?q?Feat(commission):=20=ED=94=8C=EB=9E=9C?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controller/CommissionController.java | 27 +++++++++++ .../core/dto/response/PlanListResponse.java | 46 +++++++++++++++++++ .../core/service/CommissionService.java | 18 ++++++++ 3 files changed, 91 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/dto/response/PlanListResponse.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java diff --git a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java new file mode 100644 index 0000000..3340dee --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java @@ -0,0 +1,27 @@ +package ditda.backend.domain.commission.core.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import ditda.backend.domain.commission.core.dto.response.PlanListResponse; +import ditda.backend.domain.commission.core.service.CommissionService; +import ditda.backend.global.apipayload.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/v1/instructors") +@RequiredArgsConstructor +@Tag(name = "Instructor Commission", description = "강사 새 외주 작성 API") +public class CommissionController { + + private final CommissionService commissionService; + + @Operation(summary = "플랜 조회", description = "**[새 외주 작성]** 플랜 정보를 조회합니다.") + @GetMapping("/plans") + public ApiResponse getPlans() { + return ApiResponse.onSuccess("플랜 조회 성공", commissionService.getPlans()); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/response/PlanListResponse.java b/src/main/java/ditda/backend/domain/commission/core/dto/response/PlanListResponse.java new file mode 100644 index 0000000..d8a484d --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/dto/response/PlanListResponse.java @@ -0,0 +1,46 @@ +package ditda.backend.domain.commission.core.dto.response; + +import java.util.Arrays; +import java.util.List; + +import ditda.backend.domain.commission.core.entity.enums.PlanCode; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "플랜 목록 조회 응답") +public record PlanListResponse( + List plans +) { + + public static PlanListResponse from() { + List plans = Arrays.stream(PlanCode.values()) + .map(PlanResponse::from) + .toList(); + + return new PlanListResponse(plans); + } + + @Schema(description = "플랜 정보") + public record PlanResponse( + + @Schema(description = "플랜 코드", example = "BASIC") + String code, + + @Schema(description = "디자이너 수", example = "3") + int designerCount, + + @Schema(description = "가격", example = "450000") + int price, + + @Schema(description = "상세 내용", example = "디자이너 3명에 대한 시안을 받아볼 수 있습니다.") + String description + ) { + public static PlanResponse from(PlanCode planCode) { + return new PlanResponse( + planCode.name(), + planCode.getDesignerCount(), + planCode.getPrice(), + planCode.getDescription() + ); + } + } +} diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java new file mode 100644 index 0000000..f1d4079 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -0,0 +1,18 @@ +package ditda.backend.domain.commission.core.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.core.dto.response.PlanListResponse; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CommissionService { + + // 외주 플랜 정보 조회 + @Transactional(readOnly = true) + public PlanListResponse getPlans() { + return PlanListResponse.from(); + } +} From e6e5f9bb339b3f8feebd26fda11509db025d59a3 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Tue, 2 Jun 2026 17:33:34 +0900 Subject: [PATCH 024/102] =?UTF-8?q?Rename(commission):=20Enum=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/commission/core/entity/Commission.java | 8 ++++---- .../core/entity/enums/{Size.java => PageSize.java} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/ditda/backend/domain/commission/core/entity/enums/{Size.java => PageSize.java} (83%) diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index 552bd6f..88187bf 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -6,7 +6,7 @@ import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.commission.core.entity.enums.PlanCode; -import ditda.backend.domain.commission.core.entity.enums.Size; +import ditda.backend.domain.commission.core.entity.enums.PageSize; import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.global.entity.BaseEntity; @@ -64,7 +64,7 @@ public class Commission extends BaseEntity { @Enumerated(EnumType.STRING) @Column(name = "size", length = 10, nullable = false) - private Size size; + private PageSize pageSize; @Column(name = "additional_concept", length = 300) private String additionalConcept; @@ -95,7 +95,7 @@ public static Commission create( PlanCode planCode, String title, CategoryType categoryType, - Size size, + PageSize pageSize, String additionalConcept, ColorSelectionMode colorSelectionMode, LocalDate firstDraftDeadline, @@ -106,7 +106,7 @@ public static Commission create( .planCode(planCode) .title(title) .categoryType(categoryType) - .size(size) + .pageSize(pageSize) .additionalConcept(additionalConcept) .colorSelectionMode(colorSelectionMode) .firstDraftDeadline(firstDraftDeadline) diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/enums/Size.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/PageSize.java similarity index 83% rename from src/main/java/ditda/backend/domain/commission/core/entity/enums/Size.java rename to src/main/java/ditda/backend/domain/commission/core/entity/enums/PageSize.java index 53e7bff..c1996ce 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/enums/Size.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/PageSize.java @@ -1,6 +1,6 @@ package ditda.backend.domain.commission.core.entity.enums; -public enum Size { +public enum PageSize { A4, B5, KUKBAE, // 188×257mm (국배판) From 07c70f8571dd3f5c68b984860c527b2296a0b943 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Tue, 2 Jun 2026 17:33:57 +0900 Subject: [PATCH 025/102] =?UTF-8?q?Feat(Term):=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=95=BD=EA=B4=80=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(Settlement)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ditda/backend/domain/term/entity/PaymentTerm.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/ditda/backend/domain/term/entity/PaymentTerm.java b/src/main/java/ditda/backend/domain/term/entity/PaymentTerm.java index a2a62b0..670b6ba 100644 --- a/src/main/java/ditda/backend/domain/term/entity/PaymentTerm.java +++ b/src/main/java/ditda/backend/domain/term/entity/PaymentTerm.java @@ -1,9 +1,12 @@ package ditda.backend.domain.term.entity; import ditda.backend.domain.payment.entity.Payment; +import ditda.backend.domain.term.entity.enums.TermType; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -34,6 +37,11 @@ public class PaymentTerm extends BaseEntity { @JoinColumn(name = "payment_id", nullable = false) private Payment payment; + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "term_type", nullable = false, length = 50) + private TermType termType = TermType.SETTLEMENT; + @Column(name = "version", length = 20, nullable = false) private String version; From 7c046a5e899e410b596466708b531bba6c68f80c Mon Sep 17 00:00:00 2001 From: fervovita Date: Tue, 2 Jun 2026 20:57:20 +0900 Subject: [PATCH 026/102] =?UTF-8?q?Refactor:=20=ED=8F=AC=ED=8A=B8=ED=8F=B4?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=EB=A1=9C=EC=A7=81=EC=9D=84=20auth?= =?UTF-8?q?=EC=97=90=EC=84=9C=20designer=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/facade/DesignerAuthFacade.java | 15 +++++++++------ .../domain/auth/service/DesignerAuthService.java | 5 +++-- .../exception/PortfolioErrorCode.java | 2 +- .../service/PortfolioService.java} | 12 +++++------- 4 files changed, 18 insertions(+), 16 deletions(-) rename src/main/java/ditda/backend/domain/{auth => designer}/exception/PortfolioErrorCode.java (94%) rename src/main/java/ditda/backend/domain/{auth/service/SignupPortfolioService.java => designer/service/PortfolioService.java} (85%) diff --git a/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java index 5b52f20..fe95545 100644 --- a/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java +++ b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java @@ -10,8 +10,9 @@ import ditda.backend.domain.auth.dto.response.PortfolioPresignResponse; import ditda.backend.domain.auth.service.DesignerAuthService; import ditda.backend.domain.auth.service.EmailVerificationService; -import ditda.backend.domain.auth.service.SignupPortfolioService; +import ditda.backend.domain.designer.service.PortfolioService; import ditda.backend.domain.user.service.UserService; +import ditda.backend.global.s3.PresignedUpload; import lombok.RequiredArgsConstructor; @Component @@ -20,7 +21,7 @@ public class DesignerAuthFacade { private final DesignerAuthService designerAuthService; private final EmailVerificationService emailVerificationService; - private final SignupPortfolioService signupPortfolioService; + private final PortfolioService portfolioService; private final UserService userService; public PortfolioPresignResponse issuePortfolioPresignedUrl(PortfolioPresignRequest request) { @@ -28,7 +29,9 @@ public PortfolioPresignResponse issuePortfolioPresignedUrl(PortfolioPresignReque // 이메일 인증 여부 확인 emailVerificationService.validateVerified(request.email()); - return signupPortfolioService.generatePresignedUpload(request.contentType()); + PresignedUpload presignedUpload = portfolioService.generatePresignedUpload(request.contentType()); + + return new PortfolioPresignResponse(presignedUpload.key(), presignedUpload.presignedUrl()); } public AuthResult signup(DesignerSignupRequest request) { @@ -42,17 +45,17 @@ public AuthResult signup(DesignerSignupRequest request) { // 포트폴리오 key 검증 List tempKeys = request.portfolioKeys() == null ? List.of() : request.portfolioKeys(); - signupPortfolioService.validateKeys(tempKeys); + portfolioService.validateKeys(tempKeys); // 임시('portfolio/tmp') -> 정식('portfolio/') 승격 - List portfolioKeys = signupPortfolioService.promote(tempKeys); + List portfolioKeys = portfolioService.promote(tempKeys); // 회원가입 AuthResult result; try { result = designerAuthService.signup(request, portfolioKeys); } catch (Exception e) { - signupPortfolioService.deleteFiles(portfolioKeys); + portfolioService.deleteFiles(portfolioKeys); throw e; } diff --git a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java index 7114241..1e72e52 100644 --- a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java @@ -14,6 +14,7 @@ import ditda.backend.domain.auth.event.DesignerSignedUpEvent; import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.designer.repository.DesignerRepository; +import ditda.backend.domain.designer.service.PortfolioService; import ditda.backend.domain.term.dto.TermAgreement; import ditda.backend.domain.term.service.TermService; import ditda.backend.domain.user.entity.User; @@ -31,7 +32,7 @@ public class DesignerAuthService { private final UserService userService; private final TermService termService; private final AuthService authService; - private final SignupPortfolioService signupPortfolioService; + private final PortfolioService portfolioService; private final PasswordEncoder passwordEncoder; private final ApplicationEventPublisher eventPublisher; @@ -67,7 +68,7 @@ public AuthResult signup(DesignerSignupRequest request, List portfolioKe designerRepository.save(designer); // 포트폴리오 S3 key를 DB에 일괄 저장 - signupPortfolioService.savePortfolios(designer, portfolioKeys); + portfolioService.savePortfolios(designer, portfolioKeys); // accessToken&refreshToken 발급 TokenResult tokens = authService.issueTokens(user.getId()); diff --git a/src/main/java/ditda/backend/domain/auth/exception/PortfolioErrorCode.java b/src/main/java/ditda/backend/domain/designer/exception/PortfolioErrorCode.java similarity index 94% rename from src/main/java/ditda/backend/domain/auth/exception/PortfolioErrorCode.java rename to src/main/java/ditda/backend/domain/designer/exception/PortfolioErrorCode.java index 9c8b93f..306c513 100644 --- a/src/main/java/ditda/backend/domain/auth/exception/PortfolioErrorCode.java +++ b/src/main/java/ditda/backend/domain/designer/exception/PortfolioErrorCode.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.auth.exception; +package ditda.backend.domain.designer.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java similarity index 85% rename from src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java rename to src/main/java/ditda/backend/domain/designer/service/PortfolioService.java index ae493e6..095f218 100644 --- a/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java +++ b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.auth.service; +package ditda.backend.domain.designer.service; import java.util.List; import java.util.Map; @@ -6,10 +6,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.auth.dto.response.PortfolioPresignResponse; -import ditda.backend.domain.auth.exception.PortfolioErrorCode; import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.designer.entity.Portfolio; +import ditda.backend.domain.designer.exception.PortfolioErrorCode; import ditda.backend.domain.designer.repository.PortfolioRepository; import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.PresignedUpload; @@ -20,7 +19,7 @@ @Service @RequiredArgsConstructor -public class SignupPortfolioService { +public class PortfolioService { private static final int MAX_PORTFOLIO_COUNT = 3; private static final String DIR = "portfolio"; @@ -34,7 +33,7 @@ public class SignupPortfolioService { private final PortfolioRepository portfolioRepository; private final S3Properties s3Properties; - public PortfolioPresignResponse generatePresignedUpload(String contentType) { + public PresignedUpload generatePresignedUpload(String contentType) { // 파일 타입 검증 String extension = ALLOWED_CONTENT_TYPES.get(contentType); @@ -42,8 +41,7 @@ public PortfolioPresignResponse generatePresignedUpload(String contentType) { throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); } - PresignedUpload upload = s3UploadManager.issueTempUpload(BUCKET, DIR, extension, contentType); - return new PortfolioPresignResponse(upload.key(), upload.presignedUrl()); + return s3UploadManager.issueTempUpload(BUCKET, DIR, extension, contentType); } public void validateKeys(List keys) { From efe3fff06ab24e74e851fc7a16aeabd8f27e8cc8 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 3 Jun 2026 01:19:34 +0900 Subject: [PATCH 027/102] =?UTF-8?q?Feat(commission):=20=EC=83=88=20?= =?UTF-8?q?=EC=99=B8=EC=A3=BC=20=EC=9E=91=EC=84=B1=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/CommissionCreateRequest.java | 173 ++++++++++++++++++ .../request/CommissionFilePresignRequest.java | 19 ++ .../response/CommissionCreateResponse.java | 57 ++++++ .../CommissionFilePresignResponse.java | 14 ++ 4 files changed, 263 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java b/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java new file mode 100644 index 0000000..100ed93 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java @@ -0,0 +1,173 @@ +package ditda.backend.domain.commission.core.dto.request; + +import java.time.LocalDate; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import ditda.backend.domain.commission.core.entity.enums.CategoryType; +import ditda.backend.domain.commission.core.entity.enums.ColorRole; +import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; +import ditda.backend.domain.commission.core.entity.enums.ConceptTag; +import ditda.backend.domain.commission.core.entity.enums.FileKind; +import ditda.backend.domain.commission.core.entity.enums.PageSize; +import ditda.backend.domain.commission.core.entity.enums.PageType; +import ditda.backend.domain.commission.core.entity.enums.PlanCode; +import ditda.backend.domain.term.entity.enums.TermType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +@Schema(description = "새 외주 작성 요청") +public record CommissionCreateRequest( + + @Schema(description = "카테고리", example = "FLYER_TEXTBOOK_COVER_INNER") + @NotNull(message = "카테고리는 필수입니다.") + CategoryType category, + + @NotNull(message = "디자인 정보는 필수입니다.") + @Valid + DesignInfo designInfo, + + @Schema(description = "교재명", example = "수학의 정석") + @NotBlank(message = "교재명은 필수입니다.") + @Size(max = 50, message = "교재명은 50자 이하여야 합니다.") + String textbookName, + + @Schema(description = "강사명", example = "홍길동") + @NotBlank(message = "강사명은 필수입니다.") + @Size(max = 50, message = "강사명은 50자 이하여야 합니다.") + String instructorName, + + @Schema(description = "과목", example = "수학") + @NotBlank(message = "과목은 필수입니다.") + @Size(max = 50, message = "과목은 50자 이하여야 합니다.") + String subject, + + @NotEmpty(message = "외주 요청 페이지는 최소 1개 이상입니다.") + @Valid + List<@NotNull @Valid RequiredPage> requiredPages, + + @Schema(description = "첨부 파일 목록 (presign 업로드 후 받은 key)") + @Valid + List<@NotNull @Valid FileInfo> files, + + @Schema(description = "플랜", example = "PLUS") + @NotNull(message = "플랜은 필수입니다.") + PlanCode plan, + + @NotNull(message = "마감일 정보는 필수입니다.") + @Valid + DateInfo date, + + @NotNull(message = "결제 약관 동의는 필수입니다.") + @Valid + TermRequest term +) { + + @Schema(description = "디자인 정보") + public record DesignInfo( + + @Schema(description = "사이즈", example = "A4") + @NotNull(message = "사이즈는 필수입니다.") + PageSize pageSize, + + @Schema(description = "컨셉 태그", example = "[\"CUTE\", \"ELEGANT\"]") + @NotEmpty(message = "컨셉은 최소 1개 이상입니다.") + List<@NotNull ConceptTag> concepts, + + @Schema(description = "추가 컨셉 설명", example = "초등 저학년 대상이라 너무 무겁지 않게") + @Size(max = 300, message = "추가 컨셉 설명은 300자 이하여야 합니다.") + String additionalConcept, + + @Schema(description = "색상 선택 모드", example = "USER_SELECTED") + @NotNull(message = "색상 선택 모드는 필수입니다.") + ColorSelectionMode colorSelectionMode, + + @Schema(description = "색상 목록 (USER_SELECTED일 때 필수)") + @Valid + List<@NotNull @Valid ColorInfo> colors + ) { + } + + @Schema(description = "색상 정보") + public record ColorInfo( + + @Schema(description = "색상 역할", example = "MAIN") + @NotNull(message = "색상 역할은 필수입니다.") + ColorRole role, + + @Schema(description = "색상 코드(#RRGGBB)", example = "#194D33") + @NotBlank(message = "색상 코드는 필수입니다.") + @Pattern(regexp = "^#[0-9A-Fa-f]{6}$", message = "색상 코드 형식이 올바르지 않습니다.") + String colorCode + ) { + } + + @Schema(description = "필요 페이지") + public record RequiredPage( + + @Schema(description = "페이지 종류", example = "INSTRUCTOR_PROFILE") + @NotNull(message = "페이지 종류는 필수입니다.") + PageType pageType, + + @Schema(description = "페이지 설명", example = "프로필은 깔끔하게") + @Size(max = 150, message = "페이지 설명은 150자 이하여야 합니다.") + String description + ) { + } + + @Schema(description = "첨부 파일") + public record FileInfo( + + @Schema(description = "파일 종류", example = "MATERIAL") + @NotNull(message = "파일 종류는 필수입니다.") + FileKind fileKind, + + @Schema(description = "presigned URL로 업로드한 첨부파일 key 목록 (최대 3개)", + example = "[\"commission/tmp/3f2b....png\", \"commission/tmp/9a1c....jpg\"]") + @Size(max = 3, message = "파일은 최대 3개까지 업로드 가능합니다.") + List<@NotBlank(message = "파일 key는 비어 있을 수 없습니다.") String> keys, + + // 필수값이라고 생각하는데, 디자인에서는 선택 + @Schema(description = "파일 설명", example = "img.04는 강사 프로필 이미지") + @Size(max = 300, message = "파일 설명은 300자 이하여야 합니다.") + String description + ) { + } + + @Schema(description = "마감일 정보") + public record DateInfo( + + @Schema(description = "1차 시안 마감일", example = "2026-06-15") + @NotNull(message = "1차 시안 마감일은 필수입니다.") + LocalDate firstDraftDeadline, + + @Schema(description = "최종 마감일", example = "2026-06-23") + @NotNull(message = "최종 마감일은 필수입니다.") + LocalDate finalDeadline + ) { + } + + @Schema(description = "약관 동의") + public record TermRequest( + + @Schema(description = "약관 종류", example = "SETTLEMENT") + @NotNull(message = "약관 종류는 필수입니다.") + TermType type, + + @Schema(description = "약관 버전", example = "V1.0") + @NotBlank(message = "약관 버전은 필수입니다.") + String version, + + @Schema(description = "약관 동의 여부", example = "true") + @NotNull(message = "약관 동의 여부는 필수입니다.") + @JsonProperty("isAgreed") + Boolean isAgreed + ) { + } +} \ No newline at end of file diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java b/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java new file mode 100644 index 0000000..a7cdf57 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java @@ -0,0 +1,19 @@ +package ditda.backend.domain.commission.core.dto.request; + +import ditda.backend.domain.commission.core.entity.enums.FileKind; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "외주 첨부파일 presigned URL 발급 요청") +public record CommissionFilePresignRequest( + + @Schema(description = "파일 종류", example = "MATERIAL") + @NotNull(message = "파일 종류는 필수입니다.") + FileKind fileKind, + + @Schema(description = "파일 Content-Type", example = "image/png") + @NotBlank(message = "Content-Type은 필수입니다.") + String contentType +) { +} diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java new file mode 100644 index 0000000..33ec0f6 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java @@ -0,0 +1,57 @@ +package ditda.backend.domain.commission.core.dto.response; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CategoryType; +import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "새 외주 작성 응답") +public record CommissionCreateResponse( + + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "제목", example = "수학의 정석 - 수학") + String title, + + @Schema(description = "카테고리", example = "FLYER_TEXTBOOK_COVER_INNER") + CategoryType category, + + @Schema(description = "상태", example = "PENDING") + CommissionStatus status, + + @Schema(description = "지원 마감일", example = "2026-06-08") + LocalDate applicationDeadline, + + @Schema(description = "1차 시안 마감일", example = "2026-06-15") + LocalDate firstDraftDeadline, + + @Schema(description = "최종 마감일", example = "2026-06-23") + LocalDate finalDeadline, + + @Schema(description = "최대 수정 횟수", example = "3") + int maxRevision, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdAt +) { + + public static CommissionCreateResponse from(Commission commission) { + return new CommissionCreateResponse( + commission.getId(), + commission.getTitle(), + commission.getCategoryType(), + commission.getStatus(), + commission.getApplicationDeadline(), + commission.getFirstDraftDeadline(), + commission.getFinalDeadline(), + commission.getMaxRevision(), + commission.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java new file mode 100644 index 0000000..3c42471 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java @@ -0,0 +1,14 @@ +package ditda.backend.domain.commission.core.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "외주 첨부파일 presigned URL 발급 응답") +public record CommissionFilePresignResponse( + + @Schema(description = "S3 객체 key", example = "commission/tmp/{uuid}.png") + String key, + + @Schema(description = "S3 직접 업로드용 presigned PUT URL") + String presignedUrl +) { +} \ No newline at end of file From 24012975974e39ce95367d5557586797bf1ba915 Mon Sep 17 00:00:00 2001 From: fervovita Date: Wed, 3 Jun 2026 01:24:15 +0900 Subject: [PATCH 028/102] =?UTF-8?q?Refactor:=20Textbook=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9D=84=20category/textbook=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{core => category/textbook}/entity/Textbook.java | 3 ++- .../{core => category/textbook}/entity/TextbookPage.java | 3 ++- .../textbook}/repository/TextbookPageRepository.java | 4 ++-- .../category/textbook/repository/TextbookRepository.java | 8 ++++++++ .../commission/core/repository/TextbookRepository.java | 8 -------- 5 files changed, 14 insertions(+), 12 deletions(-) rename src/main/java/ditda/backend/domain/commission/{core => category/textbook}/entity/Textbook.java (90%) rename src/main/java/ditda/backend/domain/commission/{core => category/textbook}/entity/TextbookPage.java (93%) rename src/main/java/ditda/backend/domain/commission/{core => category/textbook}/repository/TextbookPageRepository.java (50%) create mode 100644 src/main/java/ditda/backend/domain/commission/category/textbook/repository/TextbookRepository.java delete mode 100644 src/main/java/ditda/backend/domain/commission/core/repository/TextbookRepository.java diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Textbook.java b/src/main/java/ditda/backend/domain/commission/category/textbook/entity/Textbook.java similarity index 90% rename from src/main/java/ditda/backend/domain/commission/core/entity/Textbook.java rename to src/main/java/ditda/backend/domain/commission/category/textbook/entity/Textbook.java index 89ff1ff..2c201d5 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Textbook.java +++ b/src/main/java/ditda/backend/domain/commission/category/textbook/entity/Textbook.java @@ -1,5 +1,6 @@ -package ditda.backend.domain.commission.core.entity; +package ditda.backend.domain.commission.category.textbook.entity; +import ditda.backend.domain.commission.core.entity.Commission; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/TextbookPage.java b/src/main/java/ditda/backend/domain/commission/category/textbook/entity/TextbookPage.java similarity index 93% rename from src/main/java/ditda/backend/domain/commission/core/entity/TextbookPage.java rename to src/main/java/ditda/backend/domain/commission/category/textbook/entity/TextbookPage.java index 8d75625..fd39611 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/TextbookPage.java +++ b/src/main/java/ditda/backend/domain/commission/category/textbook/entity/TextbookPage.java @@ -1,5 +1,6 @@ -package ditda.backend.domain.commission.core.entity; +package ditda.backend.domain.commission.category.textbook.entity; +import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.entity.enums.PageType; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/ditda/backend/domain/commission/core/repository/TextbookPageRepository.java b/src/main/java/ditda/backend/domain/commission/category/textbook/repository/TextbookPageRepository.java similarity index 50% rename from src/main/java/ditda/backend/domain/commission/core/repository/TextbookPageRepository.java rename to src/main/java/ditda/backend/domain/commission/category/textbook/repository/TextbookPageRepository.java index 9215c17..79b76a6 100644 --- a/src/main/java/ditda/backend/domain/commission/core/repository/TextbookPageRepository.java +++ b/src/main/java/ditda/backend/domain/commission/category/textbook/repository/TextbookPageRepository.java @@ -1,8 +1,8 @@ -package ditda.backend.domain.commission.core.repository; +package ditda.backend.domain.commission.category.textbook.repository; import org.springframework.data.jpa.repository.JpaRepository; -import ditda.backend.domain.commission.core.entity.TextbookPage; +import ditda.backend.domain.commission.category.textbook.entity.TextbookPage; public interface TextbookPageRepository extends JpaRepository { } diff --git a/src/main/java/ditda/backend/domain/commission/category/textbook/repository/TextbookRepository.java b/src/main/java/ditda/backend/domain/commission/category/textbook/repository/TextbookRepository.java new file mode 100644 index 0000000..51221fd --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/category/textbook/repository/TextbookRepository.java @@ -0,0 +1,8 @@ +package ditda.backend.domain.commission.category.textbook.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import ditda.backend.domain.commission.category.textbook.entity.Textbook; + +public interface TextbookRepository extends JpaRepository { +} diff --git a/src/main/java/ditda/backend/domain/commission/core/repository/TextbookRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/TextbookRepository.java deleted file mode 100644 index f81eb5c..0000000 --- a/src/main/java/ditda/backend/domain/commission/core/repository/TextbookRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package ditda.backend.domain.commission.core.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import ditda.backend.domain.commission.core.entity.Textbook; - -public interface TextbookRepository extends JpaRepository { -} From 7c6c5f842b4d2b4f4d153e54b6ef2e3759d876b5 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 3 Jun 2026 01:52:10 +0900 Subject: [PATCH 029/102] =?UTF-8?q?refactor(s3):=20content-type=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=EC=9D=84=20S3ContentType=20enum=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/SignupPortfolioService.java | 16 ++++++------ .../global/s3/enums/S3ContentType.java | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ditda/backend/global/s3/enums/S3ContentType.java diff --git a/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java b/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java index ae493e6..7c90ff2 100644 --- a/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java +++ b/src/main/java/ditda/backend/domain/auth/service/SignupPortfolioService.java @@ -1,7 +1,8 @@ package ditda.backend.domain.auth.service; +import java.util.EnumSet; import java.util.List; -import java.util.Map; +import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,6 +17,7 @@ import ditda.backend.global.s3.S3Properties; import ditda.backend.global.s3.S3UploadManager; import ditda.backend.global.s3.enums.BucketType; +import ditda.backend.global.s3.enums.S3ContentType; import lombok.RequiredArgsConstructor; @Service @@ -25,9 +27,9 @@ public class SignupPortfolioService { private static final int MAX_PORTFOLIO_COUNT = 3; private static final String DIR = "portfolio"; private static final BucketType BUCKET = BucketType.PRIVATE; - private static final Map ALLOWED_CONTENT_TYPES = Map.of( - "application/pdf", ".pdf", - "image/png", ".png" + private static final Set ALLOWED = EnumSet.of( + S3ContentType.PNG, + S3ContentType.PDF ); private final S3UploadManager s3UploadManager; @@ -37,12 +39,12 @@ public class SignupPortfolioService { public PortfolioPresignResponse generatePresignedUpload(String contentType) { // 파일 타입 검증 - String extension = ALLOWED_CONTENT_TYPES.get(contentType); - if (extension == null) { + S3ContentType type = S3ContentType.from(contentType); + if (type == null || !ALLOWED.contains(type)) { throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); } - PresignedUpload upload = s3UploadManager.issueTempUpload(BUCKET, DIR, extension, contentType); + PresignedUpload upload = s3UploadManager.issueTempUpload(BUCKET, DIR, type.getExtension(), contentType); return new PortfolioPresignResponse(upload.key(), upload.presignedUrl()); } diff --git a/src/main/java/ditda/backend/global/s3/enums/S3ContentType.java b/src/main/java/ditda/backend/global/s3/enums/S3ContentType.java new file mode 100644 index 0000000..09b0d59 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/enums/S3ContentType.java @@ -0,0 +1,25 @@ +package ditda.backend.global.s3.enums; + +import java.util.Arrays; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum S3ContentType { + + PNG("image/png", ".png"), + JPEG("image/jpeg", ".jpeg"), + PDF("application/pdf", ".pdf"); + + private final String contentType; + private final String extension; + + public static S3ContentType from(String contentType) { + return Arrays.stream(values()) + .filter(t -> t.contentType.equals(contentType)) + .findFirst() + .orElse(null); + } +} From 559d569a06def6f777a0b8a040a35916774a1e6a Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 3 Jun 2026 01:52:26 +0900 Subject: [PATCH 030/102] =?UTF-8?q?feat(commission):=20=EC=83=88=20?= =?UTF-8?q?=EC=99=B8=EC=A3=BC=20=EC=B2=A8=EB=B6=80=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/entity/enums/FileKind.java | 11 +- .../core/exception/CommissionErrorCode.java | 25 +++++ .../service/CommissionCreateFileService.java | 106 ++++++++++++++++++ 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java index ebb820d..a1cff54 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java @@ -1,6 +1,13 @@ package ditda.backend.domain.commission.core.entity.enums; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor public enum FileKind { - MATERIAL, // 자료 첨부 - REFERENCE // 레퍼런스 + MATERIAL(3), // 자료 첨부 + REFERENCE(3); // 레퍼런스 + + private final int maxCount; } diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java new file mode 100644 index 0000000..cff4638 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -0,0 +1,25 @@ +package ditda.backend.domain.commission.core.exception; + +import org.springframework.http.HttpStatus; + +import ditda.backend.global.apipayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum CommissionErrorCode implements BaseErrorCode { + + // 새 외주 작성 파일 업로드 + COMMISSION_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "COMMISSION_400_01", "첨부 파일 개수가 허용 범위를 초과했습니다."), + INVALID_COMMISSION_FILE(HttpStatus.BAD_REQUEST, "COMMISSION_400_02", "지원하지 않는 파일 형식입니다."), + COMMISSION_FILE_SIZE_EXCEEDED(HttpStatus.CONTENT_TOO_LARGE, "COMMISSION_413_01", "첨부 파일 크기가 제한을 초과했습니다."), + PORTFOLIO_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COMMISSION_500_01", "파일 업로드에 실패했습니다."), + + COLORS_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_03", "직접 색상 선택 시 색상은 필수입니다."), + SETTLEMENT_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "결제 약관 동의는 필수입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java new file mode 100644 index 0000000..8aa6123 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java @@ -0,0 +1,106 @@ +package ditda.backend.domain.commission.core.service; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.CommissionFile; +import ditda.backend.domain.commission.core.entity.enums.FileKind; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; +import ditda.backend.domain.commission.core.repository.CommissionFileRepository; +import ditda.backend.global.apipayload.exception.GeneralException; +import ditda.backend.global.s3.PresignedUpload; +import ditda.backend.global.s3.S3Properties; +import ditda.backend.global.s3.S3UploadManager; +import ditda.backend.global.s3.enums.BucketType; +import ditda.backend.global.s3.enums.S3ContentType; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CommissionCreateFileService { + + private static final String DIR = "commission"; + private static final BucketType BUCKET = BucketType.PRIVATE; + private static final Set ALLOWED = EnumSet.of(S3ContentType.PNG); + + private final S3UploadManager s3UploadManager; + private final CommissionFileRepository commissionFileRepository; + private final S3Properties s3Properties; + + public CommissionFilePresignResponse generatePresignedUpload(FileKind fileKind, String contentType) { + + S3ContentType type = S3ContentType.from(contentType); + if (type == null || !ALLOWED.contains(type)) { + throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); + } + + PresignedUpload upload = s3UploadManager.issueTempUpload(BUCKET, directoryOf(fileKind), type.getExtension(), contentType); + + return new CommissionFilePresignResponse(upload.key(), upload.presignedUrl()); + } + + public void validateKeys(FileKind fileKind, List keys) { + + if (keys.size() > fileKind.getMaxCount()) { + throw new GeneralException( + CommissionErrorCode.COMMISSION_FILE_LIMIT_EXCEEDED, + "%s 파일은 최대 %d개까지 업로드 가능합니다.".formatted(fileKind, fileKind.getMaxCount()) + ); + } + + if (keys.size() != keys.stream().distinct().count()) { + throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); + } + + String dir = directoryOf(fileKind); + for (String key : keys) { + if (!s3UploadManager.isTempKey(key, dir)) { + throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); + } + + Long size = s3UploadManager.getObjectSize(BUCKET, key); + if (size == null) { + throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); + } + if (size > s3Properties.getMaxFileSize().toBytes()) { + throw new GeneralException(CommissionErrorCode.COMMISSION_FILE_SIZE_EXCEEDED); + } + } + } + + @Transactional + public void saveCommissionFiles( + Commission commission, + FileKind fileKind, + List keys, + String description + ) { + + if (keys == null || keys.isEmpty()) { + return; + } + + List commissionFiles = keys.stream() + .map(key -> CommissionFile.create(commission, fileKind, key, description)) + .toList(); + commissionFileRepository.saveAll(commissionFiles); + } + + public List promote(List tempKeys) { + return s3UploadManager.promote(BUCKET, tempKeys); + } + + public void deleteFiles(List keys) { + s3UploadManager.deleteAll(BUCKET, keys); + } + + private String directoryOf(FileKind fileKind) { + return DIR + "/" + fileKind.name().toLowerCase(); + } +} From 63ddafa635ce4db16e28048b5644cba028639dde Mon Sep 17 00:00:00 2001 From: fervovita Date: Wed, 3 Jun 2026 01:56:02 +0900 Subject: [PATCH 031/102] =?UTF-8?q?Refactor(designer):=20PortfolioErrorCod?= =?UTF-8?q?e=EB=A5=BC=20DesignerErrorCode=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tfolioErrorCode.java => DesignerErrorCode.java} | 2 +- .../domain/designer/service/PortfolioService.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/main/java/ditda/backend/domain/designer/exception/{PortfolioErrorCode.java => DesignerErrorCode.java} (93%) diff --git a/src/main/java/ditda/backend/domain/designer/exception/PortfolioErrorCode.java b/src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java similarity index 93% rename from src/main/java/ditda/backend/domain/designer/exception/PortfolioErrorCode.java rename to src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java index 306c513..da57c87 100644 --- a/src/main/java/ditda/backend/domain/designer/exception/PortfolioErrorCode.java +++ b/src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor -public enum PortfolioErrorCode implements BaseErrorCode { +public enum DesignerErrorCode implements BaseErrorCode { PORTFOLIO_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "DESIGNER_400_01", "포트폴리오 파일은 최대 3개까지 업로드 가능합니다."), INVALID_PORTFOLIO_FILE(HttpStatus.BAD_REQUEST, "DESIGNER_400_02", "지원하지 않는 파일 형식입니다."), diff --git a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java index 095f218..8be7ac2 100644 --- a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java +++ b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java @@ -8,7 +8,7 @@ import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.designer.entity.Portfolio; -import ditda.backend.domain.designer.exception.PortfolioErrorCode; +import ditda.backend.domain.designer.exception.DesignerErrorCode; import ditda.backend.domain.designer.repository.PortfolioRepository; import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.PresignedUpload; @@ -38,7 +38,7 @@ public PresignedUpload generatePresignedUpload(String contentType) { // 파일 타입 검증 String extension = ALLOWED_CONTENT_TYPES.get(contentType); if (extension == null) { - throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); + throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } return s3UploadManager.issueTempUpload(BUCKET, DIR, extension, contentType); @@ -48,27 +48,27 @@ public void validateKeys(List keys) { // 파일 개수 검증 if (keys.size() > MAX_PORTFOLIO_COUNT) { - throw new GeneralException(PortfolioErrorCode.PORTFOLIO_FILE_LIMIT_EXCEEDED); + throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_LIMIT_EXCEEDED); } // distinct key 검증 if (keys.size() != keys.stream().distinct().count()) { - throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); + throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } for (String key : keys) { // 파일 key 형식 검증 if (!s3UploadManager.isTempKey(key, DIR)) { - throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); + throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } // 파일 크기 검증 Long size = s3UploadManager.getObjectSize(BUCKET, key); if (size == null) { - throw new GeneralException(PortfolioErrorCode.INVALID_PORTFOLIO_FILE); // 미업로드 key + throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); // 미업로드 key } if (size > s3Properties.getMaxFileSize().toBytes()) { - throw new GeneralException(PortfolioErrorCode.PORTFOLIO_FILE_SIZE_EXCEEDED); + throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_SIZE_EXCEEDED); } } } From bb7d3185e3ea9a062f9ab06e1d0df84fa2c36e96 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 13:27:02 +0900 Subject: [PATCH 032/102] =?UTF-8?q?Chore:=20=ED=8C=8C=EC=9D=BC=20=EB=81=9D?= =?UTF-8?q?=EC=97=90=20=EC=A4=84=EB=B0=94=EA=BF=88=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20import=20=EB=AC=B8=20=EC=88=9C=EC=84=9C=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/dto/response/CommissionCreateResponse.java | 2 +- .../core/dto/response/CommissionFilePresignResponse.java | 2 +- .../backend/domain/commission/core/entity/Commission.java | 2 +- .../core/service/CommissionCreateFileService.java | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java index 33ec0f6..8b7a110 100644 --- a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java +++ b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionCreateResponse.java @@ -54,4 +54,4 @@ public static CommissionCreateResponse from(Commission commission) { commission.getCreatedAt() ); } -} \ No newline at end of file +} diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java index 3c42471..9fc4501 100644 --- a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java +++ b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java @@ -11,4 +11,4 @@ public record CommissionFilePresignResponse( @Schema(description = "S3 직접 업로드용 presigned PUT URL") String presignedUrl ) { -} \ No newline at end of file +} diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index 88187bf..aff4ac1 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -5,8 +5,8 @@ import ditda.backend.domain.commission.core.entity.enums.CategoryType; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; -import ditda.backend.domain.commission.core.entity.enums.PlanCode; import ditda.backend.domain.commission.core.entity.enums.PageSize; +import ditda.backend.domain.commission.core.entity.enums.PlanCode; import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.global.entity.BaseEntity; diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java index 8aa6123..f99da26 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java @@ -40,7 +40,12 @@ public CommissionFilePresignResponse generatePresignedUpload(FileKind fileKind, throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); } - PresignedUpload upload = s3UploadManager.issueTempUpload(BUCKET, directoryOf(fileKind), type.getExtension(), contentType); + PresignedUpload upload = s3UploadManager.issueTempUpload( + BUCKET, + directoryOf(fileKind), + type.getExtension(), + contentType + ); return new CommissionFilePresignResponse(upload.key(), upload.presignedUrl()); } From c0890121fbaa8f81582e76696136c6c0df9dd7fd Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 17:33:40 +0900 Subject: [PATCH 033/102] =?UTF-8?q?Refactor(designer):=20=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=ED=8F=B4=EB=A6=AC=EC=98=A4=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=83=80=EC=9E=85=EC=9D=84=20S3ContentTyp?= =?UTF-8?q?e=20enum=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/designer/service/PortfolioService.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java index 8be7ac2..f0f34d8 100644 --- a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java +++ b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java @@ -1,7 +1,8 @@ package ditda.backend.domain.designer.service; +import java.util.EnumSet; import java.util.List; -import java.util.Map; +import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +16,7 @@ import ditda.backend.global.s3.S3Properties; import ditda.backend.global.s3.S3UploadManager; import ditda.backend.global.s3.enums.BucketType; +import ditda.backend.global.s3.enums.S3ContentType; import lombok.RequiredArgsConstructor; @Service @@ -24,10 +26,7 @@ public class PortfolioService { private static final int MAX_PORTFOLIO_COUNT = 3; private static final String DIR = "portfolio"; private static final BucketType BUCKET = BucketType.PRIVATE; - private static final Map ALLOWED_CONTENT_TYPES = Map.of( - "application/pdf", ".pdf", - "image/png", ".png" - ); + private static final Set ALLOWED = EnumSet.of(S3ContentType.PNG, S3ContentType.PDF); private final S3UploadManager s3UploadManager; private final PortfolioRepository portfolioRepository; @@ -36,12 +35,12 @@ public class PortfolioService { public PresignedUpload generatePresignedUpload(String contentType) { // 파일 타입 검증 - String extension = ALLOWED_CONTENT_TYPES.get(contentType); - if (extension == null) { + S3ContentType type = S3ContentType.from(contentType); + if (type == null || !ALLOWED.contains(type)) { throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); } - return s3UploadManager.issueTempUpload(BUCKET, DIR, extension, contentType); + return s3UploadManager.issueTempUpload(BUCKET, DIR, type.getExtension(), contentType); } public void validateKeys(List keys) { From b4216655e331c2501de524c87c6478f6148e6ae9 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 17:34:31 +0900 Subject: [PATCH 034/102] =?UTF-8?q?Feat(payment):=20=EC=99=B8=EC=A3=BC=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EB=8C=80=EA=B8=B0(PENDING)=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/payment/entity/Payment.java | 3 +- .../payment/service/PaymentService.java | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ditda/backend/domain/payment/service/PaymentService.java diff --git a/src/main/java/ditda/backend/domain/payment/entity/Payment.java b/src/main/java/ditda/backend/domain/payment/entity/Payment.java index 827667f..a2cc1a7 100644 --- a/src/main/java/ditda/backend/domain/payment/entity/Payment.java +++ b/src/main/java/ditda/backend/domain/payment/entity/Payment.java @@ -52,11 +52,12 @@ public class Payment extends BaseEntity { @Column(name = "paid_at") private LocalDateTime paidAt; - public static Payment create(Commission commission) { + public static Payment create(Commission commission, String depositorName) { return Payment.builder() .commission(commission) .amount(commission.getPlanCode().getPrice()) + .depositorName(depositorName) .status(PaymentStatus.PENDING) .build(); } diff --git a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java new file mode 100644 index 0000000..89d114c --- /dev/null +++ b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java @@ -0,0 +1,42 @@ +package ditda.backend.domain.payment.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.payment.entity.Payment; +import ditda.backend.domain.payment.repository.PaymentRepository; +import ditda.backend.domain.term.entity.PaymentTerm; +import ditda.backend.domain.term.repository.PaymentTermRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class PaymentService { + + private final PaymentRepository paymentRepository; + private final PaymentTermRepository paymentTermRepository; + + // 강사 새 외주 작성시 결제 대기 상태 생성 + @Transactional + public void createPendingPayment( + Commission commission, + String depositorName, + String termVersion, + boolean isTermAgreed + ) { + + Payment payment = Payment.create( + commission, + depositorName + ); + paymentRepository.save(payment); + + PaymentTerm paymentTerm = PaymentTerm.create( + payment, + termVersion, + isTermAgreed + ); + paymentTermRepository.save(paymentTerm); + } +} From 8a0dfa79a983671b96533c05ac881b53ac53c899 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 17:35:07 +0900 Subject: [PATCH 035/102] =?UTF-8?q?Feat(commission):=20=EC=99=B8=EC=A3=BC?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=9A=94=EC=B2=AD=20DTO=C2=B7=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=BD=94=EB=93=9C=C2=B7=EC=B2=A8=EB=B6=80=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=95=EC=B1=85=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/CommissionCreateRequest.java | 56 +++++++++++-------- .../core/entity/enums/FileKind.java | 3 +- .../core/exception/CommissionErrorCode.java | 17 +++++- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java b/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java index 100ed93..097fac6 100644 --- a/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java +++ b/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionCreateRequest.java @@ -33,24 +33,10 @@ public record CommissionCreateRequest( @Valid DesignInfo designInfo, - @Schema(description = "교재명", example = "수학의 정석") - @NotBlank(message = "교재명은 필수입니다.") - @Size(max = 50, message = "교재명은 50자 이하여야 합니다.") - String textbookName, - - @Schema(description = "강사명", example = "홍길동") - @NotBlank(message = "강사명은 필수입니다.") - @Size(max = 50, message = "강사명은 50자 이하여야 합니다.") - String instructorName, - - @Schema(description = "과목", example = "수학") - @NotBlank(message = "과목은 필수입니다.") - @Size(max = 50, message = "과목은 50자 이하여야 합니다.") - String subject, - - @NotEmpty(message = "외주 요청 페이지는 최소 1개 이상입니다.") + // 교재 내지/외지 카테고리 전용 (카테고리에 따라 비어있을 수 있어 @NotNull 제외 -> Handler에서 검증) + @Schema(description = "교재 내지/외지 카테고리 상세") @Valid - List<@NotNull @Valid RequiredPage> requiredPages, + TextbookDetail textbook, @Schema(description = "첨부 파일 목록 (presign 업로드 후 받은 key)") @Valid @@ -77,7 +63,8 @@ public record DesignInfo( PageSize pageSize, @Schema(description = "컨셉 태그", example = "[\"CUTE\", \"ELEGANT\"]") - @NotEmpty(message = "컨셉은 최소 1개 이상입니다.") + @NotNull(message = "컨셉은 필수입니다.") + @Size(min = 1, max = 2, message = "컨셉은 1개 이상 2개 이하여야 합니다.") List<@NotNull ConceptTag> concepts, @Schema(description = "추가 컨셉 설명", example = "초등 저학년 대상이라 너무 무겁지 않게") @@ -94,6 +81,31 @@ public record DesignInfo( ) { } + // 추후 카테고리 추가될 시, 다형 역직렬화(@JsonTypeInfo)를 통해 분리 + @Schema(description = "교재 외지/내지 카테고리 상세") + public record TextbookDetail( + + @Schema(description = "교재명", example = "수학의 정석") + @NotBlank(message = "교재명은 필수입니다.") + @Size(max = 50, message = "교재명은 50자 이하여야 합니다.") + String textbookName, + + @Schema(description = "강사명", example = "홍길동") + @NotBlank(message = "강사명은 필수입니다.") + @Size(max = 50, message = "강사명은 50자 이하여야 합니다.") + String instructorName, + + @Schema(description = "과목", example = "수학") + @NotBlank(message = "과목은 필수입니다.") + @Size(max = 50, message = "과목은 50자 이하여야 합니다.") + String subject, + + @NotEmpty(message = "외주 요청 페이지는 최소 1개 이상입니다.") + @Valid + List<@NotNull @Valid RequiredPage> requiredPages + ) { + } + @Schema(description = "색상 정보") public record ColorInfo( @@ -128,13 +140,13 @@ public record FileInfo( @NotNull(message = "파일 종류는 필수입니다.") FileKind fileKind, - @Schema(description = "presigned URL로 업로드한 첨부파일 key 목록 (최대 3개)", + @Schema(description = "presigned URL로 업로드한 첨부파일 key 목록", example = "[\"commission/tmp/3f2b....png\", \"commission/tmp/9a1c....jpg\"]") - @Size(max = 3, message = "파일은 최대 3개까지 업로드 가능합니다.") + @NotEmpty(message = "파일 key는 최소 1개 이상입니다.") List<@NotBlank(message = "파일 key는 비어 있을 수 없습니다.") String> keys, - // 필수값이라고 생각하는데, 디자인에서는 선택 @Schema(description = "파일 설명", example = "img.04는 강사 프로필 이미지") + @NotBlank(message = "파일 설명은 필수입니다.") @Size(max = 300, message = "파일 설명은 300자 이하여야 합니다.") String description ) { @@ -170,4 +182,4 @@ public record TermRequest( Boolean isAgreed ) { } -} \ No newline at end of file +} diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java b/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java index a1cff54..b74b713 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/enums/FileKind.java @@ -6,8 +6,9 @@ @Getter @AllArgsConstructor public enum FileKind { - MATERIAL(3), // 자료 첨부 + MATERIAL(5), // 자료 첨부 REFERENCE(3); // 레퍼런스 + // 새 외주 작성시 카테고리별 첨부파일 최대 개수 private final int maxCount; } diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java index cff4638..f111b09 100644 --- a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -14,10 +14,23 @@ public enum CommissionErrorCode implements BaseErrorCode { COMMISSION_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "COMMISSION_400_01", "첨부 파일 개수가 허용 범위를 초과했습니다."), INVALID_COMMISSION_FILE(HttpStatus.BAD_REQUEST, "COMMISSION_400_02", "지원하지 않는 파일 형식입니다."), COMMISSION_FILE_SIZE_EXCEEDED(HttpStatus.CONTENT_TOO_LARGE, "COMMISSION_413_01", "첨부 파일 크기가 제한을 초과했습니다."), - PORTFOLIO_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COMMISSION_500_01", "파일 업로드에 실패했습니다."), + COMMISSION_FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COMMISSION_500_01", "파일 업로드에 실패했습니다."), + // 색상 선택 (직접 색상 지정) COLORS_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_03", "직접 색상 선택 시 색상은 필수입니다."), - SETTLEMENT_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "결제 약관 동의는 필수입니다."); + + // 카테고리별 + UNSUPPORTED_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "지원하지 않는 카테고리입니다."), + TEXTBOOK_DETAIL_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "교재 정보는 필수입니다."), + INVALID_PAGE_TYPE_FOR_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_06", "해당 카테고리에서 지원하지 않는 페이지 종류입니다."), + + // 마감 기한 + INVALID_DEADLINE_ORDER(HttpStatus.BAD_REQUEST, "COMMISSION_400_07", "1차 시안 마감일은 최종 마감일보다 빨라야 합니다."), + FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_08", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), + FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), + + // 결제 약관 + SETTLEMENT_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "결제 약관 동의는 필수입니다."); private final HttpStatus httpStatus; private final String code; From 5885424a4188db1e1114f42048a2caf2a16236a3 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 17:35:24 +0900 Subject: [PATCH 036/102] =?UTF-8?q?Feat(commission):=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=EB=B3=84=20=EC=B2=98=EB=A6=AC=20=EC=A0=84?= =?UTF-8?q?=EB=9E=B5(Handler)=20=EA=B5=AC=EC=A1=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/TextbookCategoryHandler.java | 70 +++++++++++++++++++ .../handler/CommissionCategoryHandler.java | 20 ++++++ .../CommissionCategoryHandlerResolver.java | 38 ++++++++++ 3 files changed, 128 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/commission/category/textbook/handler/TextbookCategoryHandler.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandler.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandlerResolver.java diff --git a/src/main/java/ditda/backend/domain/commission/category/textbook/handler/TextbookCategoryHandler.java b/src/main/java/ditda/backend/domain/commission/category/textbook/handler/TextbookCategoryHandler.java new file mode 100644 index 0000000..36fa90e --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/category/textbook/handler/TextbookCategoryHandler.java @@ -0,0 +1,70 @@ +package ditda.backend.domain.commission.category.textbook.handler; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import ditda.backend.domain.commission.category.textbook.entity.Textbook; +import ditda.backend.domain.commission.category.textbook.entity.TextbookPage; +import ditda.backend.domain.commission.category.textbook.repository.TextbookPageRepository; +import ditda.backend.domain.commission.category.textbook.repository.TextbookRepository; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TextbookDetail; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CategoryType; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; +import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; +import ditda.backend.global.apipayload.exception.GeneralException; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class TextbookCategoryHandler implements CommissionCategoryHandler { + + private final TextbookRepository textbookRepository; + private final TextbookPageRepository textbookPageRepository; + + @Override + public CategoryType category() { + return CategoryType.FLYER_TEXTBOOK_COVER_INNER; + } + + @Override + public void validate(CommissionCreateRequest request) { + + TextbookDetail detail = request.textbook(); + if (detail == null) { + throw new GeneralException(CommissionErrorCode.TEXTBOOK_DETAIL_REQUIRED); + } + } + + @Override + public String buildTitle(CommissionCreateRequest request) { + + TextbookDetail detail = request.textbook(); + return detail.textbookName() + " - " + detail.instructorName(); + } + + @Override + public void saveDetail(Commission commission, CommissionCreateRequest request) { + + TextbookDetail detail = request.textbook(); + + Textbook textbook = Textbook.create( + commission, + detail.textbookName(), + detail.instructorName(), + detail.subject() + ); + textbookRepository.save(textbook); + + List pages = detail.requiredPages().stream() + .map(p -> TextbookPage.create( + commission, + p.pageType(), + p.description() + )) + .toList(); + textbookPageRepository.saveAll(pages); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandler.java b/src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandler.java new file mode 100644 index 0000000..2db2877 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandler.java @@ -0,0 +1,20 @@ +package ditda.backend.domain.commission.core.handler; + +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CategoryType; + +public interface CommissionCategoryHandler { + + // 담당 카테고리 + CategoryType category(); + + // 카테고리별 전용 검증 + void validate(CommissionCreateRequest request); + + // 카테고리별 전용 제목 생성 + String buildTitle(CommissionCreateRequest request); + + // 카테고리별 전용 저장 + void saveDetail(Commission commission, CommissionCreateRequest request); +} diff --git a/src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandlerResolver.java b/src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandlerResolver.java new file mode 100644 index 0000000..6f4aad6 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/handler/CommissionCategoryHandlerResolver.java @@ -0,0 +1,38 @@ +package ditda.backend.domain.commission.core.handler; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import ditda.backend.domain.commission.core.entity.enums.CategoryType; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; +import ditda.backend.global.apipayload.exception.GeneralException; + +/** + * 카테고리(CategoryType)에 맞는 CommissionCategoryHandler를 찾아준다. + * 새 카테고리 추가 = 핸들러 구현체(@Component) 하나 추가 → 자동 등록 + */ +@Component +public class CommissionCategoryHandlerResolver { + + private final Map handlers; + + // CommissionCategoryHandler 구현체 전부 주입 -> 카테고리별 Map으로 변환 + public CommissionCategoryHandlerResolver(List handlers) { + this.handlers = handlers.stream() + .collect(Collectors.toMap(CommissionCategoryHandler::category, handler -> handler)); + } + + // 카테고리로 Handler 조회 (미지원 카테고리면 예외처리) + public CommissionCategoryHandler resolve(CategoryType categoryType) { + + CommissionCategoryHandler handler = handlers.get(categoryType); + if (handler == null) { + throw new GeneralException(CommissionErrorCode.UNSUPPORTED_CATEGORY); + } + return handler; + } + +} From cff553f759f7a44a1af05ba4c6286f0dac53b9f9 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 17:35:41 +0900 Subject: [PATCH 037/102] =?UTF-8?q?Feat(commission):=20=EC=99=B8=EC=A3=BC?= =?UTF-8?q?=20=EC=B2=A8=EB=B6=80=ED=8C=8C=EC=9D=BC=20presign=C2=B7?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=C2=B7=EC=A0=80=EC=9E=A5=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CommissionCreateFileService.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java index f99da26..355a96a 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java @@ -7,7 +7,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.entity.CommissionFile; import ditda.backend.domain.commission.core.entity.enums.FileKind; @@ -33,25 +32,20 @@ public class CommissionCreateFileService { private final CommissionFileRepository commissionFileRepository; private final S3Properties s3Properties; - public CommissionFilePresignResponse generatePresignedUpload(FileKind fileKind, String contentType) { + public PresignedUpload generatePresignedUpload(FileKind fileKind, String contentType) { + // 파일 타입 검증 S3ContentType type = S3ContentType.from(contentType); if (type == null || !ALLOWED.contains(type)) { throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); } - PresignedUpload upload = s3UploadManager.issueTempUpload( - BUCKET, - directoryOf(fileKind), - type.getExtension(), - contentType - ); - - return new CommissionFilePresignResponse(upload.key(), upload.presignedUrl()); + return s3UploadManager.issueTempUpload(BUCKET, directoryOf(fileKind), type.getExtension(), contentType); } public void validateKeys(FileKind fileKind, List keys) { + // 파일 개수 검증 if (keys.size() > fileKind.getMaxCount()) { throw new GeneralException( CommissionErrorCode.COMMISSION_FILE_LIMIT_EXCEEDED, @@ -59,16 +53,19 @@ public void validateKeys(FileKind fileKind, List keys) { ); } + // distinct key 검증 if (keys.size() != keys.stream().distinct().count()) { throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); } String dir = directoryOf(fileKind); for (String key : keys) { + // 파일 key 형식 검증 if (!s3UploadManager.isTempKey(key, dir)) { throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); } + // 파일 크기 검증 Long size = s3UploadManager.getObjectSize(BUCKET, key); if (size == null) { throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); @@ -91,6 +88,7 @@ public void saveCommissionFiles( return; } + // 파일 DB에 일괄 저장 List commissionFiles = keys.stream() .map(key -> CommissionFile.create(commission, fileKind, key, description)) .toList(); From 1b043424bf1ac3dbbb4bf4892d6e2248ae58ffb9 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 17:35:53 +0900 Subject: [PATCH 038/102] =?UTF-8?q?Feat(commission):=20=EC=83=88=20?= =?UTF-8?q?=EC=99=B8=EC=A3=BC=20=EC=83=9D=EC=84=B1=20API=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controller/CommissionController.java | 44 ++++- .../core/facade/CommissionFacade.java | 74 ++++++++ .../core/service/CommissionService.java | 176 ++++++++++++++++++ .../core/vo/CommissionFileToSave.java | 12 ++ 4 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java create mode 100644 src/main/java/ditda/backend/domain/commission/core/vo/CommissionFileToSave.java diff --git a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java index 3340dee..cae596a 100644 --- a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java +++ b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java @@ -1,14 +1,22 @@ package ditda.backend.domain.commission.core.controller; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.dto.request.CommissionFilePresignRequest; +import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; +import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; -import ditda.backend.domain.commission.core.service.CommissionService; +import ditda.backend.domain.commission.core.facade.CommissionFacade; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RestController @@ -17,11 +25,41 @@ @Tag(name = "Instructor Commission", description = "강사 새 외주 작성 API") public class CommissionController { - private final CommissionService commissionService; + private final CommissionFacade commissionFacade; @Operation(summary = "플랜 조회", description = "**[새 외주 작성]** 플랜 정보를 조회합니다.") @GetMapping("/plans") public ApiResponse getPlans() { - return ApiResponse.onSuccess("플랜 조회 성공", commissionService.getPlans()); + + PlanListResponse response = commissionFacade.getPlans(); + + return ApiResponse.onSuccess("플랜 조회 성공", response); + } + + @Operation( + summary = "새 외주 작성 파일 업로드 URL 발급", + description = "**[새 외주 작성]** 새 외주 작성시 자료 첨부 및 레퍼런스 파일을 S3에 올릴 presigned PUT URL을 발급합니다. " + + "PUT URL로 S3에 직접 업로드한 뒤, 응답의 key를 외주 생성 요청의 keys에 담아 전송합니다." + ) + @PostMapping("/files/presigned-url") + public ApiResponse issueFilePresignedUrls( + @Valid @RequestBody CommissionFilePresignRequest request + ) { + + CommissionFilePresignResponse response = commissionFacade.issueFilePresignedUrls(request); + + return ApiResponse.onSuccess("새 외주 작성 파일 업로드 URL 발급 성공", response); + } + + @Operation(summary = "새 외주 생성", description = "**[새 외주 작성]** 새로운 외주를 생성합니다.") + @PostMapping + public ApiResponse createCommission( + @AuthenticationPrincipal Long instructorId, + @Valid @RequestBody CommissionCreateRequest request + ) { + + CommissionCreateResponse response = commissionFacade.createCommission(instructorId, request); + + return ApiResponse.onSuccess("외주 생성 성공", response); } } diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java new file mode 100644 index 0000000..1f3a484 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java @@ -0,0 +1,74 @@ +package ditda.backend.domain.commission.core.facade; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.FileInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionFilePresignRequest; +import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; +import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; +import ditda.backend.domain.commission.core.dto.response.PlanListResponse; +import ditda.backend.domain.commission.core.service.CommissionCreateFileService; +import ditda.backend.domain.commission.core.service.CommissionService; +import ditda.backend.domain.commission.core.vo.CommissionFileToSave; +import ditda.backend.global.s3.PresignedUpload; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CommissionFacade { + + private final CommissionService commissionService; + private final CommissionCreateFileService commissionCreateFileService; + + public PlanListResponse getPlans() { + return commissionService.getPlans(); + } + + public CommissionFilePresignResponse issueFilePresignedUrls(CommissionFilePresignRequest request) { + + PresignedUpload presignedUpload = commissionCreateFileService.generatePresignedUpload( + request.fileKind(), + request.contentType() + ); + + return new CommissionFilePresignResponse(presignedUpload.key(), presignedUpload.presignedUrl()); + } + + public CommissionCreateResponse createCommission( + Long instructorId, + CommissionCreateRequest request + ) { + + List files = request.files() == null + ? List.of() + : request.files(); + + files.forEach(file -> commissionCreateFileService.validateKeys( + file.fileKind(), + file.keys() + )); + + List commissionFiles = new ArrayList<>(); + List promotedKeys = new ArrayList<>(); + try { + for (FileInfo file : files) { + List permanent = commissionCreateFileService.promote(file.keys()); + commissionFiles.add(new CommissionFileToSave( + file.fileKind(), + permanent, + file.description() + )); + promotedKeys.addAll(permanent); + } + + return commissionService.createCommission(instructorId, request, commissionFiles); + } catch (Exception exception) { + commissionCreateFileService.deleteFiles(promotedKeys); + throw exception; + } + } +} diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index f1d4079..b4a8164 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -1,18 +1,194 @@ package ditda.backend.domain.commission.core.service; +import java.time.LocalDate; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.ColorInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DateInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TermRequest; +import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.CommissionColor; +import ditda.backend.domain.commission.core.entity.CommissionConcept; +import ditda.backend.domain.commission.core.entity.enums.ColorRole; +import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; +import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; +import ditda.backend.domain.commission.core.handler.CommissionCategoryHandlerResolver; +import ditda.backend.domain.commission.core.repository.CommissionColorRepository; +import ditda.backend.domain.commission.core.repository.CommissionConceptRepository; +import ditda.backend.domain.commission.core.repository.CommissionRepository; +import ditda.backend.domain.commission.core.vo.CommissionFileToSave; +import ditda.backend.domain.instructor.entity.Instructor; +import ditda.backend.domain.instructor.repository.InstructorRepository; +import ditda.backend.domain.payment.service.PaymentService; +import ditda.backend.domain.term.entity.enums.TermType; +import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class CommissionService { + private static final int MIN_FIRST_DRAFT_LEAD_DAYS = 10; + private static final int MIN_DRAFT_TO_FINAL_DAYS = 14; + + private final CommissionRepository commissionRepository; + private final CommissionConceptRepository commissionConceptRepository; + private final CommissionColorRepository commissionColorRepository; + private final InstructorRepository instructorRepository; + private final CommissionCreateFileService commissionCreateFileService; + private final CommissionCategoryHandlerResolver commissionCategoryHandlerResolver; + private final PaymentService paymentService; + // 외주 플랜 정보 조회 @Transactional(readOnly = true) public PlanListResponse getPlans() { return PlanListResponse.from(); } + + // 새 외주 생성 + @Transactional + public CommissionCreateResponse createCommission( + Long instructorId, + CommissionCreateRequest request, + List commissionFiles + ) { + + // 1. 공통 검증 + validateColors(request); + validateTerm(request); + validateDeadlines(request); + + // 2. 카테고리 핸들러 + 전용 검증 + CommissionCategoryHandler handler = commissionCategoryHandlerResolver.resolve(request.category()); + handler.validate(request); + + // 3. Commission 저장 + Instructor instructor = instructorRepository.getReferenceById(instructorId); + String title = handler.buildTitle(request); + Commission commission = Commission.create( + instructor, + request.plan(), + title, + request.category(), + request.designInfo().pageSize(), + request.designInfo().additionalConcept(), + request.designInfo().colorSelectionMode(), + request.date().firstDraftDeadline(), + request.date().finalDeadline() + ); + commissionRepository.save(commission); + + // 4. 공통 섹션 저장 + saveConcepts(commission, request); + saveColors(commission, request); + saveFiles(commission, commissionFiles); + + // 5. 카테고리별 전용 섹션 저장 + handler.saveDetail(commission, request); + + // 6. 결제(입금 대기 [PENDING]) + 결제 약관 저장 + String depositorName = instructor.getUser().getName(); + paymentService.createPendingPayment( + commission, + depositorName, + request.term().version(), + request.term().isAgreed() + ); + + return CommissionCreateResponse.from(commission); + } + + // 색상 검증 (직접 색상 지정시 MAIN, SUB1, SUB2 필수) + private void validateColors(CommissionCreateRequest request) { + + if (request.designInfo().colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { + return; + } + + List colors = request.designInfo().colors(); + if (colors == null || colors.isEmpty()) { + throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); + } + + Set roles = colors.stream() + .map(ColorInfo::role) + .collect(Collectors.toSet()); + + if (roles.size() != colors.size() || !roles.equals(EnumSet.allOf(ColorRole.class))) { + throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); + } + } + + // 결제 약관 검증 + private void validateTerm(CommissionCreateRequest request) { + + TermRequest term = request.term(); + if (term.type() != TermType.SETTLEMENT || !Boolean.TRUE.equals(term.isAgreed())) { + throw new GeneralException(CommissionErrorCode.SETTLEMENT_TERM_NOT_AGREED); + } + } + + // 마감 기한 검증 + private void validateDeadlines(CommissionCreateRequest request) { + + DateInfo date = request.date(); + LocalDate today = LocalDate.now(); + + if (!date.firstDraftDeadline().isBefore(date.finalDeadline())) { + throw new GeneralException(CommissionErrorCode.INVALID_DEADLINE_ORDER); + } + + if (date.firstDraftDeadline().minusDays(MIN_FIRST_DRAFT_LEAD_DAYS).isBefore(today)) { + throw new GeneralException(CommissionErrorCode.FIRST_DRAFT_DEADLINE_TOO_SOON); + } + + if (date.finalDeadline().isBefore(date.firstDraftDeadline().plusDays(MIN_DRAFT_TO_FINAL_DAYS))) { + throw new GeneralException(CommissionErrorCode.FINAL_DEADLINE_TOO_SOON); + } + } + + // 컨셉 저장 + private void saveConcepts(Commission commission, CommissionCreateRequest request) { + + List concepts = request.designInfo().concepts().stream() + .map(tag -> CommissionConcept.create(commission, tag)) + .toList(); + commissionConceptRepository.saveAll(concepts); + } + + // 색상 저장 (강사 직접 선택시) + private void saveColors(Commission commission, CommissionCreateRequest request) { + + if (request.designInfo().colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { + return; + } + + List colors = request.designInfo().colors().stream() + .map(color -> CommissionColor.create(commission, color.role(), color.colorCode())) + .toList(); + commissionColorRepository.saveAll(colors); + } + + // 첨부 파일 저장 (자료첨부, 레퍼런스) + private void saveFiles(Commission commission, List commissionFiles) { + + for (CommissionFileToSave file : commissionFiles) { + commissionCreateFileService.saveCommissionFiles( + commission, + file.fileKind(), + file.keys(), + file.description() + ); + } + } } diff --git a/src/main/java/ditda/backend/domain/commission/core/vo/CommissionFileToSave.java b/src/main/java/ditda/backend/domain/commission/core/vo/CommissionFileToSave.java new file mode 100644 index 0000000..f7ccfa3 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/vo/CommissionFileToSave.java @@ -0,0 +1,12 @@ +package ditda.backend.domain.commission.core.vo; + +import java.util.List; + +import ditda.backend.domain.commission.core.entity.enums.FileKind; + +public record CommissionFileToSave( + FileKind fileKind, + List keys, + String description +) { +} From 7fa6d9c1af61739db4351e2aee9711e55ae71122 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 18:00:08 +0900 Subject: [PATCH 039/102] =?UTF-8?q?Refactor(commission):=20=EC=99=B8?= =?UTF-8?q?=EC=A3=BC=20=EC=83=9D=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=B2=A8=EB=B6=80?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=95=A9=EC=82=B0=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/facade/CommissionFacade.java | 16 ++-- .../service/CommissionCreateFileService.java | 74 +++++++++------ .../core/service/CommissionService.java | 83 ++--------------- .../validator/CommissionCreateValidator.java | 89 +++++++++++++++++++ 4 files changed, 149 insertions(+), 113 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java index 1f3a484..d2d8e69 100644 --- a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java +++ b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java @@ -11,8 +11,10 @@ import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; +import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; import ditda.backend.domain.commission.core.service.CommissionCreateFileService; import ditda.backend.domain.commission.core.service.CommissionService; +import ditda.backend.domain.commission.core.validator.CommissionCreateValidator; import ditda.backend.domain.commission.core.vo.CommissionFileToSave; import ditda.backend.global.s3.PresignedUpload; import lombok.RequiredArgsConstructor; @@ -23,6 +25,7 @@ public class CommissionFacade { private final CommissionService commissionService; private final CommissionCreateFileService commissionCreateFileService; + private final CommissionCreateValidator commissionCreateValidator; public PlanListResponse getPlans() { return commissionService.getPlans(); @@ -43,15 +46,16 @@ public CommissionCreateResponse createCommission( CommissionCreateRequest request ) { + // 1. 순수 검증 + 카테고리 핸들러 확보 + CommissionCategoryHandler handler = commissionCreateValidator.validate(request); + + // 2. 파일 검증 List files = request.files() == null ? List.of() : request.files(); + commissionCreateFileService.validateFiles(files); - files.forEach(file -> commissionCreateFileService.validateKeys( - file.fileKind(), - file.keys() - )); - + // 3. promote 후 영속화 List commissionFiles = new ArrayList<>(); List promotedKeys = new ArrayList<>(); try { @@ -65,7 +69,7 @@ public CommissionCreateResponse createCommission( promotedKeys.addAll(permanent); } - return commissionService.createCommission(instructorId, request, commissionFiles); + return commissionService.createCommission(instructorId, request, handler, commissionFiles); } catch (Exception exception) { commissionCreateFileService.deleteFiles(promotedKeys); throw exception; diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java index 355a96a..73677b3 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java @@ -2,11 +2,14 @@ import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.FileInfo; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.entity.CommissionFile; import ditda.backend.domain.commission.core.entity.enums.FileKind; @@ -43,37 +46,17 @@ public PresignedUpload generatePresignedUpload(FileKind fileKind, String content return s3UploadManager.issueTempUpload(BUCKET, directoryOf(fileKind), type.getExtension(), contentType); } - public void validateKeys(FileKind fileKind, List keys) { + // 첨부 파일의 종류별로 keys 합산하여 검증 + // 클라이언트가 같은 종류 여러번 보내도 종류별 총량을 기준으로 검사 + public void validateFiles(List files) { - // 파일 개수 검증 - if (keys.size() > fileKind.getMaxCount()) { - throw new GeneralException( - CommissionErrorCode.COMMISSION_FILE_LIMIT_EXCEEDED, - "%s 파일은 최대 %d개까지 업로드 가능합니다.".formatted(fileKind, fileKind.getMaxCount()) - ); - } - - // distinct key 검증 - if (keys.size() != keys.stream().distinct().count()) { - throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); - } - - String dir = directoryOf(fileKind); - for (String key : keys) { - // 파일 key 형식 검증 - if (!s3UploadManager.isTempKey(key, dir)) { - throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); - } + Map> keysByKind = files.stream() + .collect(Collectors.groupingBy( + FileInfo::fileKind, + Collectors.flatMapping(file -> file.keys().stream(), Collectors.toList()) + )); - // 파일 크기 검증 - Long size = s3UploadManager.getObjectSize(BUCKET, key); - if (size == null) { - throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); - } - if (size > s3Properties.getMaxFileSize().toBytes()) { - throw new GeneralException(CommissionErrorCode.COMMISSION_FILE_SIZE_EXCEEDED); - } - } + keysByKind.forEach(this::validateKeys); } @Transactional @@ -106,4 +89,37 @@ public void deleteFiles(List keys) { private String directoryOf(FileKind fileKind) { return DIR + "/" + fileKind.name().toLowerCase(); } + + private void validateKeys(FileKind fileKind, List keys) { + + // 파일 개수 검증 + if (keys.size() > fileKind.getMaxCount()) { + throw new GeneralException( + CommissionErrorCode.COMMISSION_FILE_LIMIT_EXCEEDED, + "%s 파일은 최대 %d개까지 업로드 가능합니다.".formatted(fileKind, fileKind.getMaxCount()) + ); + } + + // distinct key 검증 + if (keys.size() != keys.stream().distinct().count()) { + throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); + } + + String dir = directoryOf(fileKind); + for (String key : keys) { + // 파일 key 형식 검증 + if (!s3UploadManager.isTempKey(key, dir)) { + throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); + } + + // 파일 크기 검증 + Long size = s3UploadManager.getObjectSize(BUCKET, key); + if (size == null) { + throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); + } + if (size > s3Properties.getMaxFileSize().toBytes()) { + throw new GeneralException(CommissionErrorCode.COMMISSION_FILE_SIZE_EXCEEDED); + } + } + } } diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index b4a8164..a899de6 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -1,28 +1,18 @@ package ditda.backend.domain.commission.core.service; -import java.time.LocalDate; -import java.util.EnumSet; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; -import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.ColorInfo; -import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DateInfo; -import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TermRequest; import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.entity.CommissionColor; import ditda.backend.domain.commission.core.entity.CommissionConcept; -import ditda.backend.domain.commission.core.entity.enums.ColorRole; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; -import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; -import ditda.backend.domain.commission.core.handler.CommissionCategoryHandlerResolver; import ditda.backend.domain.commission.core.repository.CommissionColorRepository; import ditda.backend.domain.commission.core.repository.CommissionConceptRepository; import ditda.backend.domain.commission.core.repository.CommissionRepository; @@ -30,23 +20,17 @@ import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.domain.instructor.repository.InstructorRepository; import ditda.backend.domain.payment.service.PaymentService; -import ditda.backend.domain.term.entity.enums.TermType; -import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class CommissionService { - private static final int MIN_FIRST_DRAFT_LEAD_DAYS = 10; - private static final int MIN_DRAFT_TO_FINAL_DAYS = 14; - private final CommissionRepository commissionRepository; private final CommissionConceptRepository commissionConceptRepository; private final CommissionColorRepository commissionColorRepository; private final InstructorRepository instructorRepository; private final CommissionCreateFileService commissionCreateFileService; - private final CommissionCategoryHandlerResolver commissionCategoryHandlerResolver; private final PaymentService paymentService; // 외주 플랜 정보 조회 @@ -60,19 +44,11 @@ public PlanListResponse getPlans() { public CommissionCreateResponse createCommission( Long instructorId, CommissionCreateRequest request, + CommissionCategoryHandler handler, List commissionFiles ) { - // 1. 공통 검증 - validateColors(request); - validateTerm(request); - validateDeadlines(request); - - // 2. 카테고리 핸들러 + 전용 검증 - CommissionCategoryHandler handler = commissionCategoryHandlerResolver.resolve(request.category()); - handler.validate(request); - - // 3. Commission 저장 + // 1. Commission 저장 Instructor instructor = instructorRepository.getReferenceById(instructorId); String title = handler.buildTitle(request); Commission commission = Commission.create( @@ -88,15 +64,15 @@ public CommissionCreateResponse createCommission( ); commissionRepository.save(commission); - // 4. 공통 섹션 저장 + // 2. 공통 섹션 저장 saveConcepts(commission, request); saveColors(commission, request); saveFiles(commission, commissionFiles); - // 5. 카테고리별 전용 섹션 저장 + // 3. 카테고리별 전용 섹션 저장 handler.saveDetail(commission, request); - // 6. 결제(입금 대기 [PENDING]) + 결제 약관 저장 + // 4. 결제(입금 대기 [PENDING]) + 결제 약관 저장 String depositorName = instructor.getUser().getName(); paymentService.createPendingPayment( commission, @@ -108,55 +84,6 @@ public CommissionCreateResponse createCommission( return CommissionCreateResponse.from(commission); } - // 색상 검증 (직접 색상 지정시 MAIN, SUB1, SUB2 필수) - private void validateColors(CommissionCreateRequest request) { - - if (request.designInfo().colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { - return; - } - - List colors = request.designInfo().colors(); - if (colors == null || colors.isEmpty()) { - throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); - } - - Set roles = colors.stream() - .map(ColorInfo::role) - .collect(Collectors.toSet()); - - if (roles.size() != colors.size() || !roles.equals(EnumSet.allOf(ColorRole.class))) { - throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); - } - } - - // 결제 약관 검증 - private void validateTerm(CommissionCreateRequest request) { - - TermRequest term = request.term(); - if (term.type() != TermType.SETTLEMENT || !Boolean.TRUE.equals(term.isAgreed())) { - throw new GeneralException(CommissionErrorCode.SETTLEMENT_TERM_NOT_AGREED); - } - } - - // 마감 기한 검증 - private void validateDeadlines(CommissionCreateRequest request) { - - DateInfo date = request.date(); - LocalDate today = LocalDate.now(); - - if (!date.firstDraftDeadline().isBefore(date.finalDeadline())) { - throw new GeneralException(CommissionErrorCode.INVALID_DEADLINE_ORDER); - } - - if (date.firstDraftDeadline().minusDays(MIN_FIRST_DRAFT_LEAD_DAYS).isBefore(today)) { - throw new GeneralException(CommissionErrorCode.FIRST_DRAFT_DEADLINE_TOO_SOON); - } - - if (date.finalDeadline().isBefore(date.firstDraftDeadline().plusDays(MIN_DRAFT_TO_FINAL_DAYS))) { - throw new GeneralException(CommissionErrorCode.FINAL_DEADLINE_TOO_SOON); - } - } - // 컨셉 저장 private void saveConcepts(Commission commission, CommissionCreateRequest request) { diff --git a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java new file mode 100644 index 0000000..05f0115 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java @@ -0,0 +1,89 @@ +package ditda.backend.domain.commission.core.validator; + +import java.time.LocalDate; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.entity.enums.ColorRole; +import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; +import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; +import ditda.backend.domain.commission.core.handler.CommissionCategoryHandlerResolver; +import ditda.backend.domain.term.entity.enums.TermType; +import ditda.backend.global.apipayload.exception.GeneralException; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CommissionCreateValidator { + + private static final int MIN_FIRST_DRAFT_LEAD_DAYS = 10; + private static final int MIN_DRAFT_TO_FINAL_DAYS = 14; + + private final CommissionCategoryHandlerResolver commissionCategoryHandlerResolver; + + public CommissionCategoryHandler validate(CommissionCreateRequest request) { + + validateColors(request); + validateTerm(request); + validateDeadlines(request); + + CommissionCategoryHandler handler = commissionCategoryHandlerResolver.resolve(request.category()); + handler.validate(request); + return handler; + } + + // 색상 검증 (직접 색상 지정시 MAIN, SUB1, SUB2 필수) + private void validateColors(CommissionCreateRequest request) { + + if (request.designInfo().colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { + return; + } + + List colors = request.designInfo().colors(); + if (colors == null || colors.isEmpty()) { + throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); + } + + Set roles = colors.stream() + .map(CommissionCreateRequest.ColorInfo::role) + .collect(Collectors.toSet()); + + if (roles.size() != colors.size() || !roles.equals(EnumSet.allOf(ColorRole.class))) { + throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); + } + } + + // 결제 약관 검증 + private void validateTerm(CommissionCreateRequest request) { + + CommissionCreateRequest.TermRequest term = request.term(); + if (term.type() != TermType.SETTLEMENT || !Boolean.TRUE.equals(term.isAgreed())) { + throw new GeneralException(CommissionErrorCode.SETTLEMENT_TERM_NOT_AGREED); + } + } + + // 마감 기한 검증 + private void validateDeadlines(CommissionCreateRequest request) { + + CommissionCreateRequest.DateInfo date = request.date(); + LocalDate today = LocalDate.now(); + + if (!date.firstDraftDeadline().isBefore(date.finalDeadline())) { + throw new GeneralException(CommissionErrorCode.INVALID_DEADLINE_ORDER); + } + + if (date.firstDraftDeadline().minusDays(MIN_FIRST_DRAFT_LEAD_DAYS).isBefore(today)) { + throw new GeneralException(CommissionErrorCode.FIRST_DRAFT_DEADLINE_TOO_SOON); + } + + if (date.finalDeadline().isBefore(date.firstDraftDeadline().plusDays(MIN_DRAFT_TO_FINAL_DAYS))) { + throw new GeneralException(CommissionErrorCode.FINAL_DEADLINE_TOO_SOON); + } + } +} From b889fd075aa38637ea11030587950eeff7787d32 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 18:12:49 +0900 Subject: [PATCH 040/102] =?UTF-8?q?Refactor(commission):=20=EC=99=B8?= =?UTF-8?q?=EC=A3=BC=20=EC=83=9D=EC=84=B1=20train-wreck=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/service/CommissionService.java | 37 +++++++++++-------- .../validator/CommissionCreateValidator.java | 24 ++++++------ .../domain/instructor/entity/Instructor.java | 4 ++ 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index a899de6..25c1158 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -6,6 +6,9 @@ import org.springframework.transaction.annotation.Transactional; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DateInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DesignInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TermRequest; import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; import ditda.backend.domain.commission.core.entity.Commission; @@ -48,6 +51,10 @@ public CommissionCreateResponse createCommission( List commissionFiles ) { + DesignInfo design = request.designInfo(); + DateInfo date = request.date(); + TermRequest term = request.term(); + // 1. Commission 저장 Instructor instructor = instructorRepository.getReferenceById(instructorId); String title = handler.buildTitle(request); @@ -56,51 +63,51 @@ public CommissionCreateResponse createCommission( request.plan(), title, request.category(), - request.designInfo().pageSize(), - request.designInfo().additionalConcept(), - request.designInfo().colorSelectionMode(), - request.date().firstDraftDeadline(), - request.date().finalDeadline() + design.pageSize(), + design.additionalConcept(), + design.colorSelectionMode(), + date.firstDraftDeadline(), + date.finalDeadline() ); commissionRepository.save(commission); // 2. 공통 섹션 저장 - saveConcepts(commission, request); - saveColors(commission, request); + saveConcepts(commission, design); + saveColors(commission, design); saveFiles(commission, commissionFiles); // 3. 카테고리별 전용 섹션 저장 handler.saveDetail(commission, request); // 4. 결제(입금 대기 [PENDING]) + 결제 약관 저장 - String depositorName = instructor.getUser().getName(); + String depositorName = instructor.getName(); paymentService.createPendingPayment( commission, depositorName, - request.term().version(), - request.term().isAgreed() + term.version(), + term.isAgreed() ); return CommissionCreateResponse.from(commission); } // 컨셉 저장 - private void saveConcepts(Commission commission, CommissionCreateRequest request) { + private void saveConcepts(Commission commission, DesignInfo design) { - List concepts = request.designInfo().concepts().stream() + List concepts = design.concepts().stream() .map(tag -> CommissionConcept.create(commission, tag)) .toList(); commissionConceptRepository.saveAll(concepts); } // 색상 저장 (강사 직접 선택시) - private void saveColors(Commission commission, CommissionCreateRequest request) { + private void saveColors(Commission commission, DesignInfo design) { - if (request.designInfo().colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { + if (design.colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { return; } - List colors = request.designInfo().colors().stream() + List colors = design.colors().stream() .map(color -> CommissionColor.create(commission, color.role(), color.colorCode())) .toList(); commissionColorRepository.saveAll(colors); diff --git a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java index 05f0115..1aae4b2 100644 --- a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java +++ b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java @@ -9,6 +9,10 @@ import org.springframework.stereotype.Component; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.ColorInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DateInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TermRequest; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DesignInfo; import ditda.backend.domain.commission.core.entity.enums.ColorRole; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; import ditda.backend.domain.commission.core.exception.CommissionErrorCode; @@ -29,9 +33,9 @@ public class CommissionCreateValidator { public CommissionCategoryHandler validate(CommissionCreateRequest request) { - validateColors(request); - validateTerm(request); - validateDeadlines(request); + validateColors(request.designInfo()); + validateTerm(request.term()); + validateDeadlines(request.date()); CommissionCategoryHandler handler = commissionCategoryHandlerResolver.resolve(request.category()); handler.validate(request); @@ -39,19 +43,19 @@ public CommissionCategoryHandler validate(CommissionCreateRequest request) { } // 색상 검증 (직접 색상 지정시 MAIN, SUB1, SUB2 필수) - private void validateColors(CommissionCreateRequest request) { + private void validateColors(DesignInfo design) { - if (request.designInfo().colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { + if (design.colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { return; } - List colors = request.designInfo().colors(); + List colors = design.colors(); if (colors == null || colors.isEmpty()) { throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); } Set roles = colors.stream() - .map(CommissionCreateRequest.ColorInfo::role) + .map(ColorInfo::role) .collect(Collectors.toSet()); if (roles.size() != colors.size() || !roles.equals(EnumSet.allOf(ColorRole.class))) { @@ -60,18 +64,16 @@ private void validateColors(CommissionCreateRequest request) { } // 결제 약관 검증 - private void validateTerm(CommissionCreateRequest request) { + private void validateTerm(TermRequest term) { - CommissionCreateRequest.TermRequest term = request.term(); if (term.type() != TermType.SETTLEMENT || !Boolean.TRUE.equals(term.isAgreed())) { throw new GeneralException(CommissionErrorCode.SETTLEMENT_TERM_NOT_AGREED); } } // 마감 기한 검증 - private void validateDeadlines(CommissionCreateRequest request) { + private void validateDeadlines(DateInfo date) { - CommissionCreateRequest.DateInfo date = request.date(); LocalDate today = LocalDate.now(); if (!date.firstDraftDeadline().isBefore(date.finalDeadline())) { diff --git a/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java b/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java index 8578e45..befa2e8 100644 --- a/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java +++ b/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java @@ -38,4 +38,8 @@ public static Instructor createInstructor(User user) { .user(user) .build(); } + + public String getName() { + return user.getName(); + } } From 8c2ea244f565abe156185ab2b25bfb766433cd8d Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 18:18:28 +0900 Subject: [PATCH 041/102] =?UTF-8?q?Refactor(commission):=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EA=B5=AC=EC=84=B1=20=EC=98=A4=EB=A5=98=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=BD=94=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/exception/CommissionErrorCode.java | 15 ++++++++------- .../core/validator/CommissionCreateValidator.java | 4 +++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java index f111b09..98bfc3c 100644 --- a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -18,19 +18,20 @@ public enum CommissionErrorCode implements BaseErrorCode { // 색상 선택 (직접 색상 지정) COLORS_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_03", "직접 색상 선택 시 색상은 필수입니다."), + INVALID_COLOR_COMPOSITION(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "색상 구성이 올바르지 않습니다. MAIN, SUB1, SUB2를 각 1개씩 지정해야 합니다."), // 카테고리별 - UNSUPPORTED_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "지원하지 않는 카테고리입니다."), - TEXTBOOK_DETAIL_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "교재 정보는 필수입니다."), - INVALID_PAGE_TYPE_FOR_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_06", "해당 카테고리에서 지원하지 않는 페이지 종류입니다."), + UNSUPPORTED_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "지원하지 않는 카테고리입니다."), + TEXTBOOK_DETAIL_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_06", "교재 정보는 필수입니다."), + INVALID_PAGE_TYPE_FOR_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_07", "해당 카테고리에서 지원하지 않는 페이지 종류입니다."), // 마감 기한 - INVALID_DEADLINE_ORDER(HttpStatus.BAD_REQUEST, "COMMISSION_400_07", "1차 시안 마감일은 최종 마감일보다 빨라야 합니다."), - FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_08", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), - FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), + INVALID_DEADLINE_ORDER(HttpStatus.BAD_REQUEST, "COMMISSION_400_08", "1차 시안 마감일은 최종 마감일보다 빨라야 합니다."), + FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), + FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), // 결제 약관 - SETTLEMENT_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "결제 약관 동의는 필수입니다."); + SETTLEMENT_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "COMMISSION_400_11", "결제 약관 동의는 필수입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java index 1aae4b2..7d645a4 100644 --- a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java +++ b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java @@ -50,6 +50,7 @@ private void validateColors(DesignInfo design) { } List colors = design.colors(); + // 색상이 아예 없을 경우 if (colors == null || colors.isEmpty()) { throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); } @@ -58,8 +59,9 @@ private void validateColors(DesignInfo design) { .map(ColorInfo::role) .collect(Collectors.toSet()); + // MAIN, SUB1, SUB2 중 부분 누락 or 중복 if (roles.size() != colors.size() || !roles.equals(EnumSet.allOf(ColorRole.class))) { - throw new GeneralException(CommissionErrorCode.COLORS_REQUIRED); + throw new GeneralException(CommissionErrorCode.INVALID_COLOR_COMPOSITION); } } From 40dfd690e8d83d6afdccbde62968d321f47e00eb Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 18:20:25 +0900 Subject: [PATCH 042/102] =?UTF-8?q?Refactor(commission):=20=ED=94=8C?= =?UTF-8?q?=EB=9E=9C=20=EC=A1=B0=ED=9A=8C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20API=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/commission/core/controller/CommissionController.java | 2 +- .../domain/commission/core/service/CommissionService.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java index cae596a..154f6d3 100644 --- a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java +++ b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java @@ -20,7 +20,7 @@ import lombok.RequiredArgsConstructor; @RestController -@RequestMapping("/api/v1/instructors") +@RequestMapping("/api/v1/instructors/commissions") @RequiredArgsConstructor @Tag(name = "Instructor Commission", description = "강사 새 외주 작성 API") public class CommissionController { diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index 25c1158..3d1fec6 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -37,7 +37,6 @@ public class CommissionService { private final PaymentService paymentService; // 외주 플랜 정보 조회 - @Transactional(readOnly = true) public PlanListResponse getPlans() { return PlanListResponse.from(); } From ca57db2536c1fb9d748f33619a4d908199538d05 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 18:22:09 +0900 Subject: [PATCH 043/102] =?UTF-8?q?Chore(Commission):=20=EC=A4=84=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EB=B0=8F=20import=20=EB=AC=B8=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/core/exception/CommissionErrorCode.java | 6 +++++- .../core/validator/CommissionCreateValidator.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java index 98bfc3c..73e9a7c 100644 --- a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -18,7 +18,11 @@ public enum CommissionErrorCode implements BaseErrorCode { // 색상 선택 (직접 색상 지정) COLORS_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_03", "직접 색상 선택 시 색상은 필수입니다."), - INVALID_COLOR_COMPOSITION(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "색상 구성이 올바르지 않습니다. MAIN, SUB1, SUB2를 각 1개씩 지정해야 합니다."), + INVALID_COLOR_COMPOSITION( + HttpStatus.BAD_REQUEST, + "COMMISSION_400_04", + "색상 구성이 올바르지 않습니다. MAIN, SUB1, SUB2를 각 1개씩 지정해야 합니다." + ), // 카테고리별 UNSUPPORTED_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "지원하지 않는 카테고리입니다."), diff --git a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java index 7d645a4..15eb36e 100644 --- a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java +++ b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java @@ -11,8 +11,8 @@ import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.ColorInfo; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DateInfo; -import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TermRequest; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DesignInfo; +import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TermRequest; import ditda.backend.domain.commission.core.entity.enums.ColorRole; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; import ditda.backend.domain.commission.core.exception.CommissionErrorCode; From 11f457bd1b0e58c1e880b39db825ca544faee258 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 20:12:41 +0900 Subject: [PATCH 044/102] =?UTF-8?q?Refactor:=20=EA=B0=95=EC=82=AC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/service/CommissionService.java | 6 +++++- .../exception/InstructorErrorCode.java | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ditda/backend/domain/instructor/exception/InstructorErrorCode.java diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index 3d1fec6..663f0a3 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -21,8 +21,10 @@ import ditda.backend.domain.commission.core.repository.CommissionRepository; import ditda.backend.domain.commission.core.vo.CommissionFileToSave; import ditda.backend.domain.instructor.entity.Instructor; +import ditda.backend.domain.instructor.exception.InstructorErrorCode; import ditda.backend.domain.instructor.repository.InstructorRepository; import ditda.backend.domain.payment.service.PaymentService; +import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @Service @@ -55,7 +57,9 @@ public CommissionCreateResponse createCommission( TermRequest term = request.term(); // 1. Commission 저장 - Instructor instructor = instructorRepository.getReferenceById(instructorId); + Instructor instructor = instructorRepository.findById(instructorId) + .orElseThrow(() -> new GeneralException(InstructorErrorCode.INSTRUCTOR_NOT_FOUND)); + String title = handler.buildTitle(request); Commission commission = Commission.create( instructor, diff --git a/src/main/java/ditda/backend/domain/instructor/exception/InstructorErrorCode.java b/src/main/java/ditda/backend/domain/instructor/exception/InstructorErrorCode.java new file mode 100644 index 0000000..5e8df51 --- /dev/null +++ b/src/main/java/ditda/backend/domain/instructor/exception/InstructorErrorCode.java @@ -0,0 +1,18 @@ +package ditda.backend.domain.instructor.exception; + +import org.springframework.http.HttpStatus; + +import ditda.backend.global.apipayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum InstructorErrorCode implements BaseErrorCode { + + INSTRUCTOR_NOT_FOUND(HttpStatus.NOT_FOUND, "INSTRUCTOR_404_01", "강사를 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} From 812189667fbfca8b6c4f24a0db6c2f26fb603b45 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 21:51:12 +0900 Subject: [PATCH 045/102] =?UTF-8?q?Fix(auth):=20@MapsId=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/DesignerAuthService.java | 3 ++- .../domain/auth/service/InstructorAuthService.java | 3 ++- .../ditda/backend/domain/designer/entity/Designer.java | 9 ++++++++- .../backend/domain/instructor/entity/Instructor.java | 10 +++++++++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java index 1e72e52..8c79e4a 100644 --- a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java @@ -65,7 +65,8 @@ public AuthResult signup(DesignerSignupRequest request, List portfolioKe request.bankAccount().accountNumber(), request.bankAccount().accountHolder() ); - designerRepository.save(designer); + // 왜인지 모르겠지만 save로 했을때는 user만 저장. Designer는 저장 X + designerRepository.saveAndFlush(designer); // 포트폴리오 S3 key를 DB에 일괄 저장 portfolioService.savePortfolios(designer, portfolioKeys); diff --git a/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java index 65b53fd..5eec766 100644 --- a/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java @@ -50,7 +50,8 @@ public AuthResult signup(InstructorSignupRequest request) { termService.saveTerms(user, toAgreements(request.terms())); - instructorRepository.save(Instructor.createInstructor(user)); + // 왜인지 모르겠지만 save로 했을때는 user만 저장. Instructor는 저장 X + instructorRepository.saveAndFlush(Instructor.createInstructor(user)); TokenResult tokens = authService.issueTokens(user.getId()); diff --git a/src/main/java/ditda/backend/domain/designer/entity/Designer.java b/src/main/java/ditda/backend/domain/designer/entity/Designer.java index bc425fd..6149c4f 100644 --- a/src/main/java/ditda/backend/domain/designer/entity/Designer.java +++ b/src/main/java/ditda/backend/domain/designer/entity/Designer.java @@ -1,5 +1,7 @@ package ditda.backend.domain.designer.entity; +import org.springframework.data.domain.Persistable; + import ditda.backend.domain.designer.entity.enums.BankName; import ditda.backend.domain.user.entity.User; import ditda.backend.global.encryption.AesEncryptConverter; @@ -27,7 +29,7 @@ @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Designer extends BaseEntity { +public class Designer extends BaseEntity implements Persistable { @Id @Column(name = "designer_id") @@ -57,6 +59,11 @@ public class Designer extends BaseEntity { @Column(name = "account_holder", length = 50, nullable = false) private String accountHolder; + @Override + public boolean isNew() { + return getCreatedAt() == null; + } + public static Designer createDesigner(User user, BankName bankName, String accountNumber, String accountHolder) { return Designer.builder() .user(user) diff --git a/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java b/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java index befa2e8..c703c42 100644 --- a/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java +++ b/src/main/java/ditda/backend/domain/instructor/entity/Instructor.java @@ -1,5 +1,7 @@ package ditda.backend.domain.instructor.entity; +import org.springframework.data.domain.Persistable; + import ditda.backend.domain.user.entity.User; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; @@ -22,7 +24,7 @@ @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Instructor extends BaseEntity { +public class Instructor extends BaseEntity implements Persistable { @Id @Column(name = "instructor_id") @@ -33,6 +35,11 @@ public class Instructor extends BaseEntity { @JoinColumn(name = "instructor_id") private User user; + @Override + public boolean isNew() { + return getCreatedAt() == null; + } + public static Instructor createInstructor(User user) { return Instructor.builder() .user(user) @@ -42,4 +49,5 @@ public static Instructor createInstructor(User user) { public String getName() { return user.getName(); } + } From 7fc7c39efe96c72313e515d5b25eae267a1d504c Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 21:51:59 +0900 Subject: [PATCH 046/102] =?UTF-8?q?Feat:=20=EC=9E=85=EA=B8=88=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controller/CommissionController.java | 13 +++++ .../core/facade/CommissionFacade.java | 8 +++ .../dto/response/DepositNotifyResponse.java | 31 +++++++++++ .../domain/payment/entity/Payment.java | 8 ++- .../payment/event/DepositNotifiedEvent.java | 13 +++++ .../payment/exception/PaymentException.java | 20 ++++++++ .../payment/notification/DepositMailer.java | 38 ++++++++++++++ .../payment/notification/DepositNotifier.java | 37 ++++++++++++++ .../payment/repository/PaymentRepository.java | 4 ++ .../payment/service/PaymentService.java | 41 +++++++++++++++ .../templates/email/deposit-notification.html | 51 +++++++++++++++++++ 11 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ditda/backend/domain/payment/dto/response/DepositNotifyResponse.java create mode 100644 src/main/java/ditda/backend/domain/payment/event/DepositNotifiedEvent.java create mode 100644 src/main/java/ditda/backend/domain/payment/exception/PaymentException.java create mode 100644 src/main/java/ditda/backend/domain/payment/notification/DepositMailer.java create mode 100644 src/main/java/ditda/backend/domain/payment/notification/DepositNotifier.java create mode 100644 src/main/resources/templates/email/deposit-notification.html diff --git a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java index 154f6d3..d22e94b 100644 --- a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java +++ b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java @@ -2,6 +2,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,6 +14,7 @@ import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; import ditda.backend.domain.commission.core.facade.CommissionFacade; +import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -62,4 +64,15 @@ public ApiResponse createCommission( return ApiResponse.onSuccess("외주 생성 성공", response); } + + @Operation(summary = "입금 통보", description = "**[새 외주 결제]** 강사가 결제(입금) 완료를 통보하고 관리자에게 확인 메일을 발송합니다.") + @PostMapping("/{commissionId}/notify-deposit") + public ApiResponse notifyDeposit( + @AuthenticationPrincipal Long instructorId, + @PathVariable Long commissionId + ) { + DepositNotifyResponse response = commissionFacade.notifyDeposit(instructorId, commissionId); + + return ApiResponse.onSuccess("입금이 접수되었습니다. 관리자 확인 후 진행됩니다.", response); + } } diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java index d2d8e69..b65f726 100644 --- a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java +++ b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java @@ -16,6 +16,8 @@ import ditda.backend.domain.commission.core.service.CommissionService; import ditda.backend.domain.commission.core.validator.CommissionCreateValidator; import ditda.backend.domain.commission.core.vo.CommissionFileToSave; +import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; +import ditda.backend.domain.payment.service.PaymentService; import ditda.backend.global.s3.PresignedUpload; import lombok.RequiredArgsConstructor; @@ -26,6 +28,7 @@ public class CommissionFacade { private final CommissionService commissionService; private final CommissionCreateFileService commissionCreateFileService; private final CommissionCreateValidator commissionCreateValidator; + private final PaymentService paymentService; public PlanListResponse getPlans() { return commissionService.getPlans(); @@ -75,4 +78,9 @@ public CommissionCreateResponse createCommission( throw exception; } } + + public DepositNotifyResponse notifyDeposit(Long instructorId, Long commissionId) { + + return paymentService.notifyDeposit(instructorId, commissionId); + } } diff --git a/src/main/java/ditda/backend/domain/payment/dto/response/DepositNotifyResponse.java b/src/main/java/ditda/backend/domain/payment/dto/response/DepositNotifyResponse.java new file mode 100644 index 0000000..ef018dc --- /dev/null +++ b/src/main/java/ditda/backend/domain/payment/dto/response/DepositNotifyResponse.java @@ -0,0 +1,31 @@ +package ditda.backend.domain.payment.dto.response; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import ditda.backend.domain.payment.entity.enums.PaymentStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "입금 통보 응답") +public record DepositNotifyResponse( + + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "결제 상태", example = "PENDING") + PaymentStatus status, + + @Schema(description = "입금 통보 시각", example = "2026-05-26 15:30:00") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime depositNotifiedAt +) { + + public static DepositNotifyResponse of( + Long commissionId, + PaymentStatus status, + LocalDateTime depositNotifiedAt + ) { + return new DepositNotifyResponse(commissionId, status, depositNotifiedAt); + } +} diff --git a/src/main/java/ditda/backend/domain/payment/entity/Payment.java b/src/main/java/ditda/backend/domain/payment/entity/Payment.java index a2cc1a7..531f723 100644 --- a/src/main/java/ditda/backend/domain/payment/entity/Payment.java +++ b/src/main/java/ditda/backend/domain/payment/entity/Payment.java @@ -52,8 +52,10 @@ public class Payment extends BaseEntity { @Column(name = "paid_at") private LocalDateTime paidAt; - public static Payment create(Commission commission, String depositorName) { + @Column(name = "deposit_notified_at") + private LocalDateTime depositNotifiedAt; + public static Payment create(Commission commission, String depositorName) { return Payment.builder() .commission(commission) .amount(commission.getPlanCode().getPrice()) @@ -61,4 +63,8 @@ public static Payment create(Commission commission, String depositorName) { .status(PaymentStatus.PENDING) .build(); } + + public void markDepositNotified() { + this.depositNotifiedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/ditda/backend/domain/payment/event/DepositNotifiedEvent.java b/src/main/java/ditda/backend/domain/payment/event/DepositNotifiedEvent.java new file mode 100644 index 0000000..47622ff --- /dev/null +++ b/src/main/java/ditda/backend/domain/payment/event/DepositNotifiedEvent.java @@ -0,0 +1,13 @@ +package ditda.backend.domain.payment.event; + +import java.time.LocalDateTime; + +public record DepositNotifiedEvent( + Long commissionId, + String commissionTitle, + String instructorName, + String depositorName, + int amount, + LocalDateTime notifiedAt +) { +} diff --git a/src/main/java/ditda/backend/domain/payment/exception/PaymentException.java b/src/main/java/ditda/backend/domain/payment/exception/PaymentException.java new file mode 100644 index 0000000..be0c740 --- /dev/null +++ b/src/main/java/ditda/backend/domain/payment/exception/PaymentException.java @@ -0,0 +1,20 @@ +package ditda.backend.domain.payment.exception; + +import org.springframework.http.HttpStatus; + +import ditda.backend.global.apipayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PaymentException implements BaseErrorCode { + + PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT_404_01", "결제 정보를 찾을 수 없습니다."), + PAYMENT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PAYMENT_403_01", "해당 외주에 대한 권한이 없습니다."), + DEPOSIT_NOTIFY_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "PAYMENT_400_01", "입금 확인 대기 상태에서만 입금 통보가 가능합니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/ditda/backend/domain/payment/notification/DepositMailer.java b/src/main/java/ditda/backend/domain/payment/notification/DepositMailer.java new file mode 100644 index 0000000..d2c1d61 --- /dev/null +++ b/src/main/java/ditda/backend/domain/payment/notification/DepositMailer.java @@ -0,0 +1,38 @@ +package ditda.backend.domain.payment.notification; + +import java.time.LocalDateTime; +import java.util.Map; + +import org.springframework.stereotype.Component; + +import ditda.backend.global.email.EmailSender; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DepositMailer { + + private static final String SUBJECT = "[DITDA] 외주 입금 확인 요청"; + private static final String TEMPLATE = "email/deposit-notification"; + + private final EmailSender emailSender; + + public void sendAdminNotification( + String adminEmail, + Long commissionId, + String commissionTitle, + String instructorName, + String depositorName, + int amount, + LocalDateTime notifiedAt + ) { + emailSender.send(adminEmail, SUBJECT, TEMPLATE, Map.of( + "commissionId", commissionId, + "title", commissionTitle, + "instructorName", instructorName, + "depositorName", depositorName, + "amount", amount, + "notifiedAt", notifiedAt + )); + } +} diff --git a/src/main/java/ditda/backend/domain/payment/notification/DepositNotifier.java b/src/main/java/ditda/backend/domain/payment/notification/DepositNotifier.java new file mode 100644 index 0000000..76c788f --- /dev/null +++ b/src/main/java/ditda/backend/domain/payment/notification/DepositNotifier.java @@ -0,0 +1,37 @@ +package ditda.backend.domain.payment.notification; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import ditda.backend.domain.payment.event.DepositNotifiedEvent; +import ditda.backend.global.config.AdminProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DepositNotifier { + + private final DepositMailer depositMailer; + private final AdminProperties adminProperties; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onDepositNotifier(DepositNotifiedEvent event) { + + try { + depositMailer.sendAdminNotification( + adminProperties.getNotificationEmail(), + event.commissionId(), + event.commissionTitle(), + event.instructorName(), + event.depositorName(), + event.amount(), + event.notifiedAt()); + } catch (Exception exception) { + log.error("Failed to dispatch deposit notification. commissionId={}", event.commissionId(), exception); + // TODO : 디스코드 웹훅 + } + } +} diff --git a/src/main/java/ditda/backend/domain/payment/repository/PaymentRepository.java b/src/main/java/ditda/backend/domain/payment/repository/PaymentRepository.java index 242eafb..b173b59 100644 --- a/src/main/java/ditda/backend/domain/payment/repository/PaymentRepository.java +++ b/src/main/java/ditda/backend/domain/payment/repository/PaymentRepository.java @@ -1,8 +1,12 @@ package ditda.backend.domain.payment.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import ditda.backend.domain.payment.entity.Payment; public interface PaymentRepository extends JpaRepository { + + Optional findByCommissionId(Long commissionId); } diff --git a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java index 89d114c..f8582c6 100644 --- a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java +++ b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java @@ -1,13 +1,19 @@ package ditda.backend.domain.payment.service; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; import ditda.backend.domain.payment.entity.Payment; +import ditda.backend.domain.payment.entity.enums.PaymentStatus; +import ditda.backend.domain.payment.event.DepositNotifiedEvent; +import ditda.backend.domain.payment.exception.PaymentException; import ditda.backend.domain.payment.repository.PaymentRepository; import ditda.backend.domain.term.entity.PaymentTerm; import ditda.backend.domain.term.repository.PaymentTermRepository; +import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @Service @@ -16,6 +22,7 @@ public class PaymentService { private final PaymentRepository paymentRepository; private final PaymentTermRepository paymentTermRepository; + private final ApplicationEventPublisher eventPublisher; // 강사 새 외주 작성시 결제 대기 상태 생성 @Transactional @@ -39,4 +46,38 @@ public void createPendingPayment( ); paymentTermRepository.save(paymentTerm); } + + @Transactional + public DepositNotifyResponse notifyDeposit(Long instructorId, Long commissionId) { + + Payment payment = paymentRepository.findByCommissionId(commissionId) + .orElseThrow(() -> new GeneralException(PaymentException.PAYMENT_NOT_FOUND)); + + Commission commission = payment.getCommission(); + + if (!commission.getInstructor().getId().equals(instructorId)) { + throw new GeneralException(PaymentException.PAYMENT_ACCESS_DENIED); + } + + if (payment.getStatus() != PaymentStatus.PENDING) { + throw new GeneralException(PaymentException.DEPOSIT_NOTIFY_NOT_ALLOWED); + } + + payment.markDepositNotified(); + + eventPublisher.publishEvent(new DepositNotifiedEvent( + commission.getId(), + commission.getTitle(), + commission.getInstructor().getName(), + payment.getDepositorName(), + payment.getAmount(), + payment.getDepositNotifiedAt() + )); + + return DepositNotifyResponse.of( + commission.getId(), + payment.getStatus(), + payment.getDepositNotifiedAt() + ); + } } diff --git a/src/main/resources/templates/email/deposit-notification.html b/src/main/resources/templates/email/deposit-notification.html new file mode 100644 index 0000000..ac11ff9 --- /dev/null +++ b/src/main/resources/templates/email/deposit-notification.html @@ -0,0 +1,51 @@ + + + + + + +
+ +
+ ditda +
+ +
+

외주 입금 확인 요청

+

강사가 입금 완료를 통보했습니다.
입금 내역 확인 후 진행 처리 부탁드립니다.

+
+ +
+
+ 외주 ID + +
+
+ 외주명 + +
+
+ 강사 + +
+
+ 입금자명 + +
+
+ 금액 + +
+
+ 통보 시각 + +
+
+ +

+ 본 메일은 자동 발송된 알림입니다. +

+
+ + \ No newline at end of file From 10632c9d4685b6531d2eac2abc44e31643d4f458 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 21:54:09 +0900 Subject: [PATCH 047/102] =?UTF-8?q?Fix(payment):=20NPE=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ditda/backend/domain/payment/service/PaymentService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java index f8582c6..3ed3c24 100644 --- a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java +++ b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java @@ -1,5 +1,7 @@ package ditda.backend.domain.payment.service; +import java.util.Objects; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -55,7 +57,7 @@ public DepositNotifyResponse notifyDeposit(Long instructorId, Long commissionId) Commission commission = payment.getCommission(); - if (!commission.getInstructor().getId().equals(instructorId)) { + if (!Objects.equals(commission.getInstructor().getId(), instructorId)) { throw new GeneralException(PaymentException.PAYMENT_ACCESS_DENIED); } From 5de7a9c15899570175547c8b07cc6c033f45ac1e Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 22:44:45 +0900 Subject: [PATCH 048/102] =?UTF-8?q?Fix:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EC=9E=85=EA=B8=88=20=EC=95=8C=EB=A6=BC=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/auth/facade/DesignerAuthFacade.java | 10 +++++++--- .../commission/core/facade/CommissionFacade.java | 10 +++++++--- .../backend/domain/payment/service/PaymentService.java | 4 ++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java index fe95545..090f1b3 100644 --- a/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java +++ b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java @@ -54,9 +54,13 @@ public AuthResult signup(DesignerSignupRequest request) { AuthResult result; try { result = designerAuthService.signup(request, portfolioKeys); - } catch (Exception e) { - portfolioService.deleteFiles(portfolioKeys); - throw e; + } catch (Exception original) { + try { + portfolioService.deleteFiles(portfolioKeys); + } catch (Exception cleanupEx) { + original.addSuppressed(cleanupEx); + } + throw original; } // 이메일 인증 마크 정리 diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java index b65f726..4c0baea 100644 --- a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java +++ b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java @@ -73,9 +73,13 @@ public CommissionCreateResponse createCommission( } return commissionService.createCommission(instructorId, request, handler, commissionFiles); - } catch (Exception exception) { - commissionCreateFileService.deleteFiles(promotedKeys); - throw exception; + } catch (Exception original) { + try { + commissionCreateFileService.deleteFiles(promotedKeys); + } catch (Exception cleanupEx) { + original.addSuppressed(cleanupEx); + } + throw original; } } diff --git a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java index 3ed3c24..beb7d61 100644 --- a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java +++ b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java @@ -65,6 +65,10 @@ public DepositNotifyResponse notifyDeposit(Long instructorId, Long commissionId) throw new GeneralException(PaymentException.DEPOSIT_NOTIFY_NOT_ALLOWED); } + if (payment.getDepositNotifiedAt() != null) { + throw new GeneralException(PaymentException.DEPOSIT_NOTIFY_NOT_ALLOWED); + } + payment.markDepositNotified(); eventPublisher.publishEvent(new DepositNotifiedEvent( From b6a04b74c8e940d23f168ea0332e3d411b3de666 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 4 Jun 2026 22:45:25 +0900 Subject: [PATCH 049/102] =?UTF-8?q?Chore(Payment):=20=EB=93=A4=EC=97=AC?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ditda/backend/domain/payment/service/PaymentService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java index beb7d61..88bdedc 100644 --- a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java +++ b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java @@ -66,7 +66,7 @@ public DepositNotifyResponse notifyDeposit(Long instructorId, Long commissionId) } if (payment.getDepositNotifiedAt() != null) { - throw new GeneralException(PaymentException.DEPOSIT_NOTIFY_NOT_ALLOWED); + throw new GeneralException(PaymentException.DEPOSIT_NOTIFY_NOT_ALLOWED); } payment.markDepositNotified(); From 514a8aa53b23e999307149ca0bf34d2d798c78da Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sun, 7 Jun 2026 00:48:49 +0900 Subject: [PATCH 050/102] =?UTF-8?q?Refactor(term):=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=95=BD=EA=B4=80=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20Ter?= =?UTF-8?q?mErrorCode=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/core/exception/CommissionErrorCode.java | 5 +---- .../commission/core/validator/CommissionCreateValidator.java | 3 ++- .../ditda/backend/domain/term/exception/TermErrorCode.java | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java index 73e9a7c..b768fc2 100644 --- a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -32,10 +32,7 @@ public enum CommissionErrorCode implements BaseErrorCode { // 마감 기한 INVALID_DEADLINE_ORDER(HttpStatus.BAD_REQUEST, "COMMISSION_400_08", "1차 시안 마감일은 최종 마감일보다 빨라야 합니다."), FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), - FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), - - // 결제 약관 - SETTLEMENT_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "COMMISSION_400_11", "결제 약관 동의는 필수입니다."); + FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java index 15eb36e..1400e92 100644 --- a/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java +++ b/src/main/java/ditda/backend/domain/commission/core/validator/CommissionCreateValidator.java @@ -19,6 +19,7 @@ import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; import ditda.backend.domain.commission.core.handler.CommissionCategoryHandlerResolver; import ditda.backend.domain.term.entity.enums.TermType; +import ditda.backend.domain.term.exception.TermErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -69,7 +70,7 @@ private void validateColors(DesignInfo design) { private void validateTerm(TermRequest term) { if (term.type() != TermType.SETTLEMENT || !Boolean.TRUE.equals(term.isAgreed())) { - throw new GeneralException(CommissionErrorCode.SETTLEMENT_TERM_NOT_AGREED); + throw new GeneralException(TermErrorCode.SETTLEMENT_TERM_NOT_AGREED); } } diff --git a/src/main/java/ditda/backend/domain/term/exception/TermErrorCode.java b/src/main/java/ditda/backend/domain/term/exception/TermErrorCode.java index 187eba8..1c79ac9 100644 --- a/src/main/java/ditda/backend/domain/term/exception/TermErrorCode.java +++ b/src/main/java/ditda/backend/domain/term/exception/TermErrorCode.java @@ -10,7 +10,8 @@ @AllArgsConstructor public enum TermErrorCode implements BaseErrorCode { - REQUIRED_TERMS_NOT_AGREED(HttpStatus.BAD_REQUEST, "TERM_400_01", "필수 약관에 동의해야 합니다."); + REQUIRED_TERMS_NOT_AGREED(HttpStatus.BAD_REQUEST, "TERM_400_01", "필수 약관에 동의해야 합니다."), + SETTLEMENT_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "TERM_400_02", "결제 약관 동의는 필수입니다."); private final HttpStatus httpStatus; private final String code; From 3607bcef715d38e43a80d88a60b5fb1a6747e197 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sun, 7 Jun 2026 00:50:01 +0900 Subject: [PATCH 051/102] =?UTF-8?q?Refactor(commission):=20=EA=B0=95?= =?UTF-8?q?=EC=82=AC=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20InstructorService?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/service/CommissionService.java | 21 +++++++--------- .../instructor/service/InstructorService.java | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/instructor/service/InstructorService.java diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index 663f0a3..56330ec 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -6,9 +6,6 @@ import org.springframework.transaction.annotation.Transactional; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; -import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DateInfo; -import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.DesignInfo; -import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.TermRequest; import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; import ditda.backend.domain.commission.core.entity.Commission; @@ -22,7 +19,7 @@ import ditda.backend.domain.commission.core.vo.CommissionFileToSave; import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.domain.instructor.exception.InstructorErrorCode; -import ditda.backend.domain.instructor.repository.InstructorRepository; +import ditda.backend.domain.instructor.service.InstructorService; import ditda.backend.domain.payment.service.PaymentService; import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -34,7 +31,7 @@ public class CommissionService { private final CommissionRepository commissionRepository; private final CommissionConceptRepository commissionConceptRepository; private final CommissionColorRepository commissionColorRepository; - private final InstructorRepository instructorRepository; + private final InstructorService instructorService; private final CommissionCreateFileService commissionCreateFileService; private final PaymentService paymentService; @@ -52,14 +49,12 @@ public CommissionCreateResponse createCommission( List commissionFiles ) { - DesignInfo design = request.designInfo(); - DateInfo date = request.date(); - TermRequest term = request.term(); + CommissionCreateRequest.DesignInfo design = request.designInfo(); + CommissionCreateRequest.DateInfo date = request.date(); + CommissionCreateRequest.TermRequest term = request.term(); // 1. Commission 저장 - Instructor instructor = instructorRepository.findById(instructorId) - .orElseThrow(() -> new GeneralException(InstructorErrorCode.INSTRUCTOR_NOT_FOUND)); - + Instructor instructor = instructorService.getById(instructorId); String title = handler.buildTitle(request); Commission commission = Commission.create( instructor, @@ -95,7 +90,7 @@ public CommissionCreateResponse createCommission( } // 컨셉 저장 - private void saveConcepts(Commission commission, DesignInfo design) { + private void saveConcepts(Commission commission, CommissionCreateRequest.DesignInfo design) { List concepts = design.concepts().stream() .map(tag -> CommissionConcept.create(commission, tag)) @@ -104,7 +99,7 @@ private void saveConcepts(Commission commission, DesignInfo design) { } // 색상 저장 (강사 직접 선택시) - private void saveColors(Commission commission, DesignInfo design) { + private void saveColors(Commission commission, CommissionCreateRequest.DesignInfo design) { if (design.colorSelectionMode() != ColorSelectionMode.USER_SELECTED) { return; diff --git a/src/main/java/ditda/backend/domain/instructor/service/InstructorService.java b/src/main/java/ditda/backend/domain/instructor/service/InstructorService.java new file mode 100644 index 0000000..d4bd54c --- /dev/null +++ b/src/main/java/ditda/backend/domain/instructor/service/InstructorService.java @@ -0,0 +1,24 @@ +package ditda.backend.domain.instructor.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.instructor.entity.Instructor; +import ditda.backend.domain.instructor.exception.InstructorErrorCode; +import ditda.backend.domain.instructor.repository.InstructorRepository; +import ditda.backend.global.apipayload.exception.GeneralException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class InstructorService { + + private final InstructorRepository instructorRepository; + + @Transactional(readOnly = true) + public Instructor getById(Long instructorId) { + + return instructorRepository.findById(instructorId) + .orElseThrow(() -> new GeneralException(InstructorErrorCode.INSTRUCTOR_NOT_FOUND)); + } +} From 76bdc74f6f954a9bc8c9075667617e22730a195c Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sun, 7 Jun 2026 00:50:17 +0900 Subject: [PATCH 052/102] =?UTF-8?q?Refactor(payment):=20=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=20=EC=95=BD=EA=B4=80=20=EC=A0=80=EC=9E=A5=EC=9D=84=20TermServi?= =?UTF-8?q?ce=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...entException.java => PaymenErrorCode.java} | 2 +- .../payment/service/PaymentService.java | 22 +++++++------------ .../domain/term/service/TermService.java | 15 +++++++++++++ 3 files changed, 24 insertions(+), 15 deletions(-) rename src/main/java/ditda/backend/domain/payment/exception/{PaymentException.java => PaymenErrorCode.java} (92%) diff --git a/src/main/java/ditda/backend/domain/payment/exception/PaymentException.java b/src/main/java/ditda/backend/domain/payment/exception/PaymenErrorCode.java similarity index 92% rename from src/main/java/ditda/backend/domain/payment/exception/PaymentException.java rename to src/main/java/ditda/backend/domain/payment/exception/PaymenErrorCode.java index be0c740..a6b8d02 100644 --- a/src/main/java/ditda/backend/domain/payment/exception/PaymentException.java +++ b/src/main/java/ditda/backend/domain/payment/exception/PaymenErrorCode.java @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor -public enum PaymentException implements BaseErrorCode { +public enum PaymenErrorCode implements BaseErrorCode { PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT_404_01", "결제 정보를 찾을 수 없습니다."), PAYMENT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PAYMENT_403_01", "해당 외주에 대한 권한이 없습니다."), diff --git a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java index 88bdedc..5940c9d 100644 --- a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java +++ b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java @@ -11,10 +11,9 @@ import ditda.backend.domain.payment.entity.Payment; import ditda.backend.domain.payment.entity.enums.PaymentStatus; import ditda.backend.domain.payment.event.DepositNotifiedEvent; -import ditda.backend.domain.payment.exception.PaymentException; +import ditda.backend.domain.payment.exception.PaymenErrorCode; import ditda.backend.domain.payment.repository.PaymentRepository; -import ditda.backend.domain.term.entity.PaymentTerm; -import ditda.backend.domain.term.repository.PaymentTermRepository; +import ditda.backend.domain.term.service.TermService; import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -23,8 +22,8 @@ public class PaymentService { private final PaymentRepository paymentRepository; - private final PaymentTermRepository paymentTermRepository; private final ApplicationEventPublisher eventPublisher; + private final TermService termService; // 강사 새 외주 작성시 결제 대기 상태 생성 @Transactional @@ -41,32 +40,27 @@ public void createPendingPayment( ); paymentRepository.save(payment); - PaymentTerm paymentTerm = PaymentTerm.create( - payment, - termVersion, - isTermAgreed - ); - paymentTermRepository.save(paymentTerm); + termService.savePaymentTerm(payment, termVersion, isTermAgreed); } @Transactional public DepositNotifyResponse notifyDeposit(Long instructorId, Long commissionId) { Payment payment = paymentRepository.findByCommissionId(commissionId) - .orElseThrow(() -> new GeneralException(PaymentException.PAYMENT_NOT_FOUND)); + .orElseThrow(() -> new GeneralException(PaymenErrorCode.PAYMENT_NOT_FOUND)); Commission commission = payment.getCommission(); if (!Objects.equals(commission.getInstructor().getId(), instructorId)) { - throw new GeneralException(PaymentException.PAYMENT_ACCESS_DENIED); + throw new GeneralException(PaymenErrorCode.PAYMENT_ACCESS_DENIED); } if (payment.getStatus() != PaymentStatus.PENDING) { - throw new GeneralException(PaymentException.DEPOSIT_NOTIFY_NOT_ALLOWED); + throw new GeneralException(PaymenErrorCode.DEPOSIT_NOTIFY_NOT_ALLOWED); } if (payment.getDepositNotifiedAt() != null) { - throw new GeneralException(PaymentException.DEPOSIT_NOTIFY_NOT_ALLOWED); + throw new GeneralException(PaymenErrorCode.DEPOSIT_NOTIFY_NOT_ALLOWED); } payment.markDepositNotified(); diff --git a/src/main/java/ditda/backend/domain/term/service/TermService.java b/src/main/java/ditda/backend/domain/term/service/TermService.java index fa6700c..7db0650 100644 --- a/src/main/java/ditda/backend/domain/term/service/TermService.java +++ b/src/main/java/ditda/backend/domain/term/service/TermService.java @@ -6,10 +6,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ditda.backend.domain.payment.entity.Payment; import ditda.backend.domain.term.dto.TermAgreement; +import ditda.backend.domain.term.entity.PaymentTerm; import ditda.backend.domain.term.entity.UserTerm; import ditda.backend.domain.term.entity.enums.TermType; import ditda.backend.domain.term.exception.TermErrorCode; +import ditda.backend.domain.term.repository.PaymentTermRepository; import ditda.backend.domain.term.repository.UserTermRepository; import ditda.backend.domain.user.entity.User; import ditda.backend.global.apipayload.exception.GeneralException; @@ -26,6 +29,18 @@ public class TermService { TermType.DISINTERMEDIATION ); private final UserTermRepository userTermRepository; + private final PaymentTermRepository paymentTermRepository; + + @Transactional + public void savePaymentTerm(Payment payment, String version, boolean isAgreed) { + + PaymentTerm paymentTerm = PaymentTerm.create( + payment, + version, + isAgreed + ); + paymentTermRepository.save(paymentTerm); + } @Transactional public void saveTerms(User user, List agreements) { From 3dee3a94f4abe282bfec3bd9cf0b6c7d79956ce4 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 8 Jun 2026 16:31:02 +0900 Subject: [PATCH 053/102] =?UTF-8?q?Rename(commission):=20CommissionFileToS?= =?UTF-8?q?ave=20vo=20->=20dto=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/core/{vo => dto}/CommissionFileToSave.java | 2 +- .../domain/commission/core/facade/CommissionFacade.java | 2 +- .../domain/commission/core/service/CommissionService.java | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) rename src/main/java/ditda/backend/domain/commission/core/{vo => dto}/CommissionFileToSave.java (79%) diff --git a/src/main/java/ditda/backend/domain/commission/core/vo/CommissionFileToSave.java b/src/main/java/ditda/backend/domain/commission/core/dto/CommissionFileToSave.java similarity index 79% rename from src/main/java/ditda/backend/domain/commission/core/vo/CommissionFileToSave.java rename to src/main/java/ditda/backend/domain/commission/core/dto/CommissionFileToSave.java index f7ccfa3..521705a 100644 --- a/src/main/java/ditda/backend/domain/commission/core/vo/CommissionFileToSave.java +++ b/src/main/java/ditda/backend/domain/commission/core/dto/CommissionFileToSave.java @@ -1,4 +1,4 @@ -package ditda.backend.domain.commission.core.vo; +package ditda.backend.domain.commission.core.dto; import java.util.List; diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java index 4c0baea..00065ce 100644 --- a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java +++ b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java @@ -15,7 +15,7 @@ import ditda.backend.domain.commission.core.service.CommissionCreateFileService; import ditda.backend.domain.commission.core.service.CommissionService; import ditda.backend.domain.commission.core.validator.CommissionCreateValidator; -import ditda.backend.domain.commission.core.vo.CommissionFileToSave; +import ditda.backend.domain.commission.core.dto.CommissionFileToSave; import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; import ditda.backend.domain.payment.service.PaymentService; import ditda.backend.global.s3.PresignedUpload; diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index 56330ec..dcc9f45 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -16,12 +16,10 @@ import ditda.backend.domain.commission.core.repository.CommissionColorRepository; import ditda.backend.domain.commission.core.repository.CommissionConceptRepository; import ditda.backend.domain.commission.core.repository.CommissionRepository; -import ditda.backend.domain.commission.core.vo.CommissionFileToSave; +import ditda.backend.domain.commission.core.dto.CommissionFileToSave; import ditda.backend.domain.instructor.entity.Instructor; -import ditda.backend.domain.instructor.exception.InstructorErrorCode; import ditda.backend.domain.instructor.service.InstructorService; import ditda.backend.domain.payment.service.PaymentService; -import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @Service From 445cae77ebcd4ad0d1525797be87f747a36947f6 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 8 Jun 2026 16:34:56 +0900 Subject: [PATCH 054/102] =?UTF-8?q?Chore(commission):=20import=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/commission/core/facade/CommissionFacade.java | 2 +- .../domain/commission/core/service/CommissionService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java index 00065ce..64b79be 100644 --- a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java +++ b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Component; +import ditda.backend.domain.commission.core.dto.CommissionFileToSave; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.FileInfo; import ditda.backend.domain.commission.core.dto.request.CommissionFilePresignRequest; @@ -15,7 +16,6 @@ import ditda.backend.domain.commission.core.service.CommissionCreateFileService; import ditda.backend.domain.commission.core.service.CommissionService; import ditda.backend.domain.commission.core.validator.CommissionCreateValidator; -import ditda.backend.domain.commission.core.dto.CommissionFileToSave; import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; import ditda.backend.domain.payment.service.PaymentService; import ditda.backend.global.s3.PresignedUpload; diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index dcc9f45..4852fbc 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ditda.backend.domain.commission.core.dto.CommissionFileToSave; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; @@ -16,7 +17,6 @@ import ditda.backend.domain.commission.core.repository.CommissionColorRepository; import ditda.backend.domain.commission.core.repository.CommissionConceptRepository; import ditda.backend.domain.commission.core.repository.CommissionRepository; -import ditda.backend.domain.commission.core.dto.CommissionFileToSave; import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.domain.instructor.service.InstructorService; import ditda.backend.domain.payment.service.PaymentService; From bbf3a4ccfcbf6b7da2f63f12ea1cac3c1e4eecbc Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sun, 14 Jun 2026 17:27:47 +0900 Subject: [PATCH 055/102] =?UTF-8?q?Feature(commission):=20=EA=B0=95?= =?UTF-8?q?=EC=82=AC=201=EC=B0=A8=20=EC=8B=9C=EC=95=88=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/exception/CommissionErrorCode.java | 6 +- .../draft/controller/DraftController.java | 34 ++++++++++ .../draft/dto/response/DraftListResponse.java | 35 ++++++++++ .../draft/exception/DraftErrorCode.java | 19 ++++++ .../CommissionDraftFileRepository.java | 9 +++ .../repository/CommissionDraftRepository.java | 10 +++ .../draft/service/DraftQueryService.java | 68 +++++++++++++++++++ 7 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java create mode 100644 src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java create mode 100644 src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java create mode 100644 src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java index b768fc2..b301f21 100644 --- a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -32,7 +32,11 @@ public enum CommissionErrorCode implements BaseErrorCode { // 마감 기한 INVALID_DEADLINE_ORDER(HttpStatus.BAD_REQUEST, "COMMISSION_400_08", "1차 시안 마감일은 최종 마감일보다 빨라야 합니다."), FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), - FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."); + FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), + + // 외주 + COMMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMISSION_404_01", "외주를 찾을 수 없습니다."), + COMMISSION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "COMMISSION_403_01", "해당 외주에 대한 권한이 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java new file mode 100644 index 0000000..da3e938 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java @@ -0,0 +1,34 @@ +package ditda.backend.domain.commission.draft.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; +import ditda.backend.domain.commission.draft.service.DraftQueryService; +import ditda.backend.global.apipayload.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/v1/instructors/commissions") +@RequiredArgsConstructor +@Tag(name = "Instructor Draft", description = "강사 시안 조회 API") +public class DraftController { + + private final DraftQueryService draftQueryService; + + @Operation(summary = "1차 시안 목록 조회", description = "**[시안 제출 조회]** 외주명 클릭 시 디자이너들이 제출한 1차 시안 목록을 조회합니다.") + @GetMapping("/{commissionId}/drafts") + public ApiResponse getFirstRoundDrafts( + @AuthenticationPrincipal Long instructorId, + @PathVariable Long commissionId + ) { + + DraftListResponse response = draftQueryService.getFirstRoundDrafts(instructorId, commissionId); + return ApiResponse.onSuccess("1차 시안 목록 조회 성공", response); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java new file mode 100644 index 0000000..f3fcecd --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java @@ -0,0 +1,35 @@ +package ditda.backend.domain.commission.draft.dto.response; + +import java.util.List; + +import ditda.backend.domain.commission.core.entity.Commission; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "1차 시안 목록 조회 응답") +public record DraftListResponse( + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "외주명", example = "YBM 영어 교재 - 홍길동") + String title, + + @Schema(description = "1차 시안 목록") + List drafts +) { + + public static DraftListResponse of(Commission commission, List drafts) { + return new DraftListResponse(commission.getId(), commission.getTitle(), drafts); + } + + // 상세 조회에서 재사용할 수 있으면 빼놓고 import로 가져오는 방법 기억해두기. + @Schema(description = "시안 정보") + public record DraftResponse( + + @Schema(description = "시안 ID", example = "1") + Long draftId, + + @Schema(description = "썸네일 URL") + String thumbnailUrl + ) { + } +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java b/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java new file mode 100644 index 0000000..fd794cf --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java @@ -0,0 +1,19 @@ +package ditda.backend.domain.commission.draft.exception; + +import org.springframework.http.HttpStatus; + +import ditda.backend.global.apipayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum DraftErrorCode implements BaseErrorCode { + + DRAFTS_NOT_READY(HttpStatus.CONFLICT, "DRAFT_409_01", "아직 모든 디자이너가 시안을 제출하지 않았습니다."), + DRAFT_NOT_FOUND(HttpStatus.NOT_FOUND, "DRAFT_404_01", "시안을 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java index 8c6786e..3263406 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java @@ -1,8 +1,17 @@ package ditda.backend.domain.commission.draft.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; public interface CommissionDraftFileRepository extends JpaRepository { + + @Query("select f from CommissionDraftFile f " + + "where f.commissionDraft.id in :draftIds " + + "and f.fileOrder = 0 ") + List findThumbnails(@Param("draftIds") List draftIds); } diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java index a7b5a96..a1a5297 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java @@ -1,8 +1,18 @@ package ditda.backend.domain.commission.draft.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.draft.entity.CommissionDraft; public interface CommissionDraftRepository extends JpaRepository { + + @Query("select d from CommissionDraft d " + + "where d.commissionApplication.commission.id = :commissionId " + + "and d.round = 0 " + + "order by d.id asc") + List findFirstRoundDrafts(@Param("commissionId") Long commissionId); } diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java new file mode 100644 index 0000000..6d0a36c --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java @@ -0,0 +1,68 @@ +package ditda.backend.domain.commission.draft.service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; +import ditda.backend.domain.commission.core.repository.CommissionRepository; +import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; +import ditda.backend.domain.commission.draft.entity.CommissionDraft; +import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; +import ditda.backend.domain.commission.draft.exception.DraftErrorCode; +import ditda.backend.domain.commission.draft.repository.CommissionDraftFileRepository; +import ditda.backend.domain.commission.draft.repository.CommissionDraftRepository; +import ditda.backend.global.apipayload.exception.GeneralException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DraftQueryService { + + private final CommissionRepository commissionRepository; + private final CommissionDraftRepository commissionDraftRepository; + private final CommissionDraftFileRepository commissionDraftFileRepository; + + // 1차 시안 목록 조회 + @Transactional(readOnly = true) + public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionId) { + + // 1. 외주 조회 + 강사 확인 + Commission commission = commissionRepository.findById(commissionId) + .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); + + if (!Objects.equals(commission.getInstructor().getId(), instructorId)) { + throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); + } + + // 2. 1차 시안 (round = 0) 목록 확인 + List drafts = commissionDraftRepository.findFirstRoundDrafts(commissionId); + boolean isFirstDeadlinePassed = LocalDate.now().isAfter(commission.getFirstDraftDeadline()); + int required = commission.getPlanCode().getDesignerCount(); + + // 마감 전에는 요구 인원이 다 차야 조회 가능, 마감 후에는 덜 차도 조회 가능 + if (!isFirstDeadlinePassed && drafts.size() < required) { + throw new GeneralException(DraftErrorCode.DRAFTS_NOT_READY); + } + + // 3. 시안별 썸네일 (fileOrder = 0) 목록 + List draftIds = drafts.stream().map(CommissionDraft::getId).toList(); + Map thumbnailByDraftId = commissionDraftFileRepository.findThumbnails(draftIds).stream() + .collect(Collectors.toMap( + f -> f.getCommissionDraft().getId(), + CommissionDraftFile::getFileUrl // TODO : 워터마크 도입 후 getWaterMarkedFileUrl()로 교체 + )); + + List responses = drafts.stream() + .map(d -> new DraftListResponse.DraftResponse(d.getId(), thumbnailByDraftId.get(d.getId()))) + .toList(); + + return DraftListResponse.of(commission, responses); + } +} From 6711a0513d04b2cd8c44ee1a941be7ba0070b402 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 13:20:07 +0900 Subject: [PATCH 056/102] =?UTF-8?q?Feature(commission):=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../draft/controller/DraftController.java | 13 ++++++ .../dto/response/DraftDetailResponse.java | 23 +++++++++++ .../CommissionDraftFileRepository.java | 2 + .../repository/CommissionDraftRepository.java | 2 + .../draft/service/DraftQueryService.java | 40 ++++++++++++++++--- 5 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java diff --git a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java index da3e938..6500da0 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java +++ b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.service.DraftQueryService; import ditda.backend.global.apipayload.response.ApiResponse; @@ -31,4 +32,16 @@ public ApiResponse getFirstRoundDrafts( DraftListResponse response = draftQueryService.getFirstRoundDrafts(instructorId, commissionId); return ApiResponse.onSuccess("1차 시안 목록 조회 성공", response); } + + @Operation(summary = "시안 상세 조회", description = "**[시안 상세 조회]** 시안 클릭 시 시안의 상세 정보를 반환합니다.") + @GetMapping("/{commissionId}/drafts/{draftId}") + public ApiResponse getDraftDetail( + @AuthenticationPrincipal Long instructorId, + @PathVariable Long commissionId, + @PathVariable Long draftId + ) { + + DraftDetailResponse response = draftQueryService.getDraftDetail(instructorId, commissionId, draftId); + return ApiResponse.onSuccess("시안 상세 조회 성공", response); + } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java new file mode 100644 index 0000000..6e702a4 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java @@ -0,0 +1,23 @@ +package ditda.backend.domain.commission.draft.dto.response; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "시안 상세 조회 응답") +public record DraftDetailResponse( + + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "시안 ID", example = "1") + Long draftId, + + @Schema(description = "시안 파일 URL 목록") + List fileUrls +) { + + public static DraftDetailResponse of(Long commissionId, Long draftId, List fileUrls) { + return new DraftDetailResponse(commissionId, draftId, fileUrls); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java index 3263406..74e6b97 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftFileRepository.java @@ -14,4 +14,6 @@ public interface CommissionDraftFileRepository extends JpaRepository findThumbnails(@Param("draftIds") List draftIds); + + List findByCommissionDraftIdOrderByFileOrderAsc(Long draftId); } diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java index a1a5297..0b0c5dc 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java @@ -15,4 +15,6 @@ public interface CommissionDraftRepository extends JpaRepository findFirstRoundDrafts(@Param("commissionId") Long commissionId); + + boolean existsByIdAndCommissionApplication_Commission_Id(Long draftId, Long commissionId); } diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java index 6d0a36c..34baa0c 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java @@ -12,6 +12,7 @@ import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.repository.CommissionRepository; +import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; @@ -34,12 +35,7 @@ public class DraftQueryService { public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionId) { // 1. 외주 조회 + 강사 확인 - Commission commission = commissionRepository.findById(commissionId) - .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); - - if (!Objects.equals(commission.getInstructor().getId(), instructorId)) { - throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); - } + Commission commission = getOwnedCommission(commissionId, instructorId); // 2. 1차 시안 (round = 0) 목록 확인 List drafts = commissionDraftRepository.findFirstRoundDrafts(commissionId); @@ -65,4 +61,36 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI return DraftListResponse.of(commission, responses); } + + // 시안 상세 조회 + @Transactional(readOnly = true) + public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, Long draftId) { + + // 1. 외주 조회 + 강사 확인 + getOwnedCommission(commissionId, instructorId); + + // 2. 시안 조회 + if (!commissionDraftRepository.existsByIdAndCommissionApplication_Commission_Id(draftId, commissionId)) { + throw new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND); + } + + // 3. 시안 파일 URL 목록 + List fileUrls = commissionDraftFileRepository + .findByCommissionDraftIdOrderByFileOrderAsc(draftId).stream() + .map(CommissionDraftFile::getFileUrl) + .toList(); + + return DraftDetailResponse.of(commissionId, draftId, fileUrls); + } + + private Commission getOwnedCommission(Long commissionId, Long instructorId) { + Commission commission = commissionRepository.findById(commissionId) + .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); + + if (!Objects.equals(commission.getInstructor().getId(), instructorId)) { + throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); + } + + return commission; + } } From d8ea73770d500fb24dedee2b5f9a12b45733840a Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 13:48:14 +0900 Subject: [PATCH 057/102] =?UTF-8?q?Feature(s3):=20S3=20URL=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=ED=95=B8=EB=93=A4=EB=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apipayload/code/GeneralErrorCode.java | 5 +++- .../global/s3/S3PresignedUrlGenerator.java | 27 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java index 9b035b0..24d7be6 100644 --- a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java +++ b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java @@ -37,7 +37,10 @@ public enum GeneralErrorCode implements BaseErrorCode { // 서버 내부 에러 INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SERVER_500_01", "서버 내부 오류입니다."), SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "SERVER_503_01", "서버가 일시적으로 불안정합니다."), - EXTERNAL_SERVICE_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT, "SERVER_504_01", "외부 서비스 응답 지연"); + EXTERNAL_SERVICE_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT, "SERVER_504_01", "외부 서비스 응답 지연"), + + // S3 파일 에러 + FILE_URL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_500_01", "파일 URL 생성에 실패했습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java index cb652d7..4614d94 100644 --- a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java +++ b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java @@ -4,8 +4,11 @@ import org.springframework.stereotype.Component; +import ditda.backend.global.apipayload.code.GeneralErrorCode; +import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.enums.BucketType; import lombok.RequiredArgsConstructor; +import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; @@ -26,18 +29,22 @@ public String generatePrivateGetUrl(String key) { } public String generatePrivateGetUrl(String key, Duration ttl) { - GetObjectRequest getObjectRequest = GetObjectRequest.builder() - .bucket(s3Properties.getPrivateBucket()) - .key(key) - .build(); + try { + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(s3Properties.getPrivateBucket()) + .key(key) + .build(); - GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() - .signatureDuration(ttl) - .getObjectRequest(getObjectRequest) - .build(); + GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() + .signatureDuration(ttl) + .getObjectRequest(getObjectRequest) + .build(); - PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest); - return presignedRequest.url().toString(); + PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest); + return presignedRequest.url().toString(); + } catch (SdkException exception) { + throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); + } } public String generatePutUrl(BucketType bucketType, String key, String contentType) { From 78e99dc05db7f174e97df7822feb622ebcadd759 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 13:48:47 +0900 Subject: [PATCH 058/102] =?UTF-8?q?Fix(commission):=20=EC=8B=9C=EC=95=88?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20key=20->=20URL=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/draft/service/DraftQueryService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java index 34baa0c..4277906 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java @@ -15,11 +15,11 @@ import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; -import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; import ditda.backend.domain.commission.draft.exception.DraftErrorCode; import ditda.backend.domain.commission.draft.repository.CommissionDraftFileRepository; import ditda.backend.domain.commission.draft.repository.CommissionDraftRepository; import ditda.backend.global.apipayload.exception.GeneralException; +import ditda.backend.global.s3.S3PresignedUrlGenerator; import lombok.RequiredArgsConstructor; @Service @@ -29,6 +29,7 @@ public class DraftQueryService { private final CommissionRepository commissionRepository; private final CommissionDraftRepository commissionDraftRepository; private final CommissionDraftFileRepository commissionDraftFileRepository; + private final S3PresignedUrlGenerator s3PresignedUrlGenerator; // 1차 시안 목록 조회 @Transactional(readOnly = true) @@ -47,12 +48,12 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI throw new GeneralException(DraftErrorCode.DRAFTS_NOT_READY); } - // 3. 시안별 썸네일 (fileOrder = 0) 목록 + // 3. 시안별 썸네일(워터마크 O) (fileOrder = 0) 목록 List draftIds = drafts.stream().map(CommissionDraft::getId).toList(); Map thumbnailByDraftId = commissionDraftFileRepository.findThumbnails(draftIds).stream() .collect(Collectors.toMap( f -> f.getCommissionDraft().getId(), - CommissionDraftFile::getFileUrl // TODO : 워터마크 도입 후 getWaterMarkedFileUrl()로 교체 + f -> s3PresignedUrlGenerator.generatePrivateGetUrl(f.getWatermarkedFileUrl()) )); List responses = drafts.stream() @@ -74,16 +75,17 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, throw new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND); } - // 3. 시안 파일 URL 목록 + // 3. 시안 파일(워터마크 O) URL 목록 List fileUrls = commissionDraftFileRepository .findByCommissionDraftIdOrderByFileOrderAsc(draftId).stream() - .map(CommissionDraftFile::getFileUrl) + .map(f -> s3PresignedUrlGenerator.generatePrivateGetUrl(f.getWatermarkedFileUrl())) .toList(); return DraftDetailResponse.of(commissionId, draftId, fileUrls); } private Commission getOwnedCommission(Long commissionId, Long instructorId) { + Commission commission = commissionRepository.findById(commissionId) .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); From 2a4c96dfd6b5c8b7685d7dad228e1f4e964c3118 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 15:35:26 +0900 Subject: [PATCH 059/102] =?UTF-8?q?Feature(commission):=201=EC=B0=A8=20?= =?UTF-8?q?=EC=8B=9C=EC=95=88=20=EC=9B=8C=ED=84=B0=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9D=91=EB=8B=B5=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/core/entity/Commission.java | 16 ++++++ .../dto/response/DraftDetailResponse.java | 19 +++++-- .../draft/dto/response/DraftListResponse.java | 14 +++-- .../repository/CommissionDraftRepository.java | 3 +- .../draft/service/DraftQueryService.java | 54 +++++++++++-------- 5 files changed, 72 insertions(+), 34 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index aff4ac1..85fb559 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -1,14 +1,17 @@ package ditda.backend.domain.commission.core.entity; import java.time.LocalDate; +import java.util.Objects; import ditda.backend.domain.commission.core.entity.enums.CategoryType; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.commission.core.entity.enums.PageSize; import ditda.backend.domain.commission.core.entity.enums.PlanCode; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.instructor.entity.Instructor; +import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -116,4 +119,17 @@ public static Commission create( .maxRevision(planCode.getBaseRevision()) .build(); } + + public boolean isDraftListViewable(int currentDraftCount, LocalDate currentDate) { + + boolean isFirstDeadlinePassed = currentDate.isAfter(firstDraftDeadline); + return isFirstDeadlinePassed || currentDraftCount >= planCode.getDesignerCount(); + } + + public void validateOwner(Long instructorId) { + + if (!Objects.equals(this.instructor.getId(), instructorId)) { + throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); + } + } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java index 6e702a4..b1c5ae4 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java +++ b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftDetailResponse.java @@ -2,6 +2,7 @@ import java.util.List; +import ditda.backend.domain.commission.draft.entity.enums.WatermarkStatus; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "시안 상세 조회 응답") @@ -13,11 +14,21 @@ public record DraftDetailResponse( @Schema(description = "시안 ID", example = "1") Long draftId, - @Schema(description = "시안 파일 URL 목록") - List fileUrls + @Schema(description = "시안 파일 목록") + List files ) { - public static DraftDetailResponse of(Long commissionId, Long draftId, List fileUrls) { - return new DraftDetailResponse(commissionId, draftId, fileUrls); + @Schema(description = "시안 파일 정보") + public record FileResponse( + + @Schema(description = "페이지 순서", example = "0") + int fileOrder, + + @Schema(description = "파일 URL (워터마크 미완료 시 null)", nullable = true) + String url, + + @Schema(description = "워터마크 상태", example = "COMPLETED") + WatermarkStatus watermarkStatus + ) { } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java index f3fcecd..932b503 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java +++ b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java @@ -2,7 +2,7 @@ import java.util.List; -import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.draft.entity.enums.WatermarkStatus; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "1차 시안 목록 조회 응답") @@ -17,19 +17,17 @@ public record DraftListResponse( List drafts ) { - public static DraftListResponse of(Commission commission, List drafts) { - return new DraftListResponse(commission.getId(), commission.getTitle(), drafts); - } - - // 상세 조회에서 재사용할 수 있으면 빼놓고 import로 가져오는 방법 기억해두기. @Schema(description = "시안 정보") public record DraftResponse( @Schema(description = "시안 ID", example = "1") Long draftId, - @Schema(description = "썸네일 URL") - String thumbnailUrl + @Schema(description = "썸네일 URL (워터마크 미완료 시 null)", nullable = true) + String thumbnailUrl, + + @Schema(description = "썸네일 워터마크 상태", example = "COMPLETED") + WatermarkStatus watermarkStatus ) { } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java index 0b0c5dc..8456f11 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java @@ -11,7 +11,8 @@ public interface CommissionDraftRepository extends JpaRepository { @Query("select d from CommissionDraft d " - + "where d.commissionApplication.commission.id = :commissionId " + + "join d.commissionApplication ca " + + "where ca.commission.id = :commissionId " + "and d.round = 0 " + "order by d.id asc") List findFirstRoundDrafts(@Param("commissionId") Long commissionId); diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java index 4277906..46adbb8 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java @@ -3,7 +3,6 @@ import java.time.LocalDate; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -15,6 +14,8 @@ import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; +import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; +import ditda.backend.domain.commission.draft.entity.enums.WatermarkStatus; import ditda.backend.domain.commission.draft.exception.DraftErrorCode; import ditda.backend.domain.commission.draft.repository.CommissionDraftFileRepository; import ditda.backend.domain.commission.draft.repository.CommissionDraftRepository; @@ -40,30 +41,33 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI // 2. 1차 시안 (round = 0) 목록 확인 List drafts = commissionDraftRepository.findFirstRoundDrafts(commissionId); - boolean isFirstDeadlinePassed = LocalDate.now().isAfter(commission.getFirstDraftDeadline()); - int required = commission.getPlanCode().getDesignerCount(); // 마감 전에는 요구 인원이 다 차야 조회 가능, 마감 후에는 덜 차도 조회 가능 - if (!isFirstDeadlinePassed && drafts.size() < required) { + if (!commission.isDraftListViewable(drafts.size(), LocalDate.now())) { throw new GeneralException(DraftErrorCode.DRAFTS_NOT_READY); } - // 3. 시안별 썸네일(워터마크 O) (fileOrder = 0) 목록 + // 3. 시안별 썸네일(워터마크) (fileOrder = 0) 목록 List draftIds = drafts.stream().map(CommissionDraft::getId).toList(); - Map thumbnailByDraftId = commissionDraftFileRepository.findThumbnails(draftIds).stream() - .collect(Collectors.toMap( - f -> f.getCommissionDraft().getId(), - f -> s3PresignedUrlGenerator.generatePrivateGetUrl(f.getWatermarkedFileUrl()) - )); + Map thumbnailByDraftId = commissionDraftFileRepository.findThumbnails(draftIds) + .stream() + .collect(Collectors.toMap(f -> f.getCommissionDraft().getId(), f -> f)); List responses = drafts.stream() - .map(d -> new DraftListResponse.DraftResponse(d.getId(), thumbnailByDraftId.get(d.getId()))) + .map(d -> { + CommissionDraftFile thumbnail = thumbnailByDraftId.get(d.getId()); + return new DraftListResponse.DraftResponse( + d.getId(), + thumbnail == null ? null : resolveUrl(thumbnail), + thumbnail == null ? null : thumbnail.getWatermarkStatus() + ); + }) .toList(); - return DraftListResponse.of(commission, responses); + return new DraftListResponse(commission.getId(), commission.getTitle(), responses); } - // 시안 상세 조회 + // 1차 시안 상세 조회 @Transactional(readOnly = true) public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, Long draftId) { @@ -75,13 +79,16 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, throw new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND); } - // 3. 시안 파일(워터마크 O) URL 목록 - List fileUrls = commissionDraftFileRepository + // 3. 1차 시안 상세 파일들(워터마크) + List files = commissionDraftFileRepository .findByCommissionDraftIdOrderByFileOrderAsc(draftId).stream() - .map(f -> s3PresignedUrlGenerator.generatePrivateGetUrl(f.getWatermarkedFileUrl())) + .map(f -> new DraftDetailResponse.FileResponse( + f.getFileOrder(), + resolveUrl(f), + f.getWatermarkStatus())) .toList(); - return DraftDetailResponse.of(commissionId, draftId, fileUrls); + return new DraftDetailResponse(commissionId, draftId, files); } private Commission getOwnedCommission(Long commissionId, Long instructorId) { @@ -89,10 +96,15 @@ private Commission getOwnedCommission(Long commissionId, Long instructorId) { Commission commission = commissionRepository.findById(commissionId) .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); - if (!Objects.equals(commission.getInstructor().getId(), instructorId)) { - throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); - } - + commission.validateOwner(instructorId); return commission; } + + private String resolveUrl(CommissionDraftFile file) { + + if (file.getWatermarkStatus() != WatermarkStatus.COMPLETED) { + return null; + } + return s3PresignedUrlGenerator.generatePrivateGetUrl(file.getWatermarkedFileUrl()); + } } From 46045a5217a4b78bd953df2c914d7921b1b80680 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 16:31:51 +0900 Subject: [PATCH 060/102] =?UTF-8?q?Chore(commission):=20=EB=B9=88=EC=A4=84?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/commission/draft/dto/response/DraftListResponse.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java index 932b503..598e81f 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java +++ b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftListResponse.java @@ -7,6 +7,7 @@ @Schema(description = "1차 시안 목록 조회 응답") public record DraftListResponse( + @Schema(description = "외주 ID", example = "1") Long commissionId, From 2a0aef694ce4bdc7d18a71adf49892d889432956 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 16:38:33 +0900 Subject: [PATCH 061/102] =?UTF-8?q?Refactor(commission):=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EC=A1=B0=ED=9A=8C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=9C=84=EC=9E=84=20=EB=B0=8F=20URL=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/core/entity/Commission.java | 9 ++------- .../draft/entity/CommissionDraftFile.java | 4 ++++ .../draft/service/DraftQueryService.java | 20 +++++++++++++------ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index 85fb559..0d0d369 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -8,10 +8,8 @@ import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.commission.core.entity.enums.PageSize; import ditda.backend.domain.commission.core.entity.enums.PlanCode; -import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.instructor.entity.Instructor; -import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -126,10 +124,7 @@ public boolean isDraftListViewable(int currentDraftCount, LocalDate currentDate) return isFirstDeadlinePassed || currentDraftCount >= planCode.getDesignerCount(); } - public void validateOwner(Long instructorId) { - - if (!Objects.equals(this.instructor.getId(), instructorId)) { - throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); - } + public boolean isOwnedBy(Long instructorId) { + return Objects.equals(instructor.getId(), instructorId); } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraftFile.java b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraftFile.java index 4872c3f..5802d45 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraftFile.java +++ b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraftFile.java @@ -76,4 +76,8 @@ public static CommissionDraftFile create( .watermarkStatus(WatermarkStatus.PROCESSING) .build(); } + + public boolean isWatermarkCompleted() { + return this.watermarkStatus == WatermarkStatus.COMPLETED; + } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java index 46adbb8..b19c447 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.exception.CommissionErrorCode; @@ -15,10 +16,10 @@ import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; -import ditda.backend.domain.commission.draft.entity.enums.WatermarkStatus; import ditda.backend.domain.commission.draft.exception.DraftErrorCode; import ditda.backend.domain.commission.draft.repository.CommissionDraftFileRepository; import ditda.backend.domain.commission.draft.repository.CommissionDraftRepository; +import ditda.backend.global.apipayload.code.GeneralErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.S3PresignedUrlGenerator; import lombok.RequiredArgsConstructor; @@ -67,7 +68,7 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI return new DraftListResponse(commission.getId(), commission.getTitle(), responses); } - // 1차 시안 상세 조회 + // 시안 상세 조회 @Transactional(readOnly = true) public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, Long draftId) { @@ -79,7 +80,7 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, throw new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND); } - // 3. 1차 시안 상세 파일들(워터마크) + // 3. 시안 상세 파일들(워터마크) List files = commissionDraftFileRepository .findByCommissionDraftIdOrderByFileOrderAsc(draftId).stream() .map(f -> new DraftDetailResponse.FileResponse( @@ -96,15 +97,22 @@ private Commission getOwnedCommission(Long commissionId, Long instructorId) { Commission commission = commissionRepository.findById(commissionId) .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); - commission.validateOwner(instructorId); + if (!commission.isOwnedBy(instructorId)) { + throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); + } return commission; } private String resolveUrl(CommissionDraftFile file) { - if (file.getWatermarkStatus() != WatermarkStatus.COMPLETED) { + if (!file.isWatermarkCompleted()) { return null; } - return s3PresignedUrlGenerator.generatePrivateGetUrl(file.getWatermarkedFileUrl()); + + String waterMarkedFileUrl = file.getWatermarkedFileUrl(); + if (!StringUtils.hasText(waterMarkedFileUrl)) { + throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); + } + return s3PresignedUrlGenerator.generatePrivateGetUrl(waterMarkedFileUrl); } } From c27c6d13ff00f3075034db512176f25234f1cf0c Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 16:47:23 +0900 Subject: [PATCH 062/102] =?UTF-8?q?Rename(commission):=20DraftQueryService?= =?UTF-8?q?=20->=20DraftService=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/draft/controller/DraftController.java | 8 ++++---- .../service/{DraftQueryService.java => DraftService.java} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/ditda/backend/domain/commission/draft/service/{DraftQueryService.java => DraftService.java} (99%) diff --git a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java index 6500da0..f656be4 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java +++ b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java @@ -8,7 +8,7 @@ import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; -import ditda.backend.domain.commission.draft.service.DraftQueryService; +import ditda.backend.domain.commission.draft.service.DraftService; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -20,7 +20,7 @@ @Tag(name = "Instructor Draft", description = "강사 시안 조회 API") public class DraftController { - private final DraftQueryService draftQueryService; + private final DraftService draftService; @Operation(summary = "1차 시안 목록 조회", description = "**[시안 제출 조회]** 외주명 클릭 시 디자이너들이 제출한 1차 시안 목록을 조회합니다.") @GetMapping("/{commissionId}/drafts") @@ -29,7 +29,7 @@ public ApiResponse getFirstRoundDrafts( @PathVariable Long commissionId ) { - DraftListResponse response = draftQueryService.getFirstRoundDrafts(instructorId, commissionId); + DraftListResponse response = draftService.getFirstRoundDrafts(instructorId, commissionId); return ApiResponse.onSuccess("1차 시안 목록 조회 성공", response); } @@ -41,7 +41,7 @@ public ApiResponse getDraftDetail( @PathVariable Long draftId ) { - DraftDetailResponse response = draftQueryService.getDraftDetail(instructorId, commissionId, draftId); + DraftDetailResponse response = draftService.getDraftDetail(instructorId, commissionId, draftId); return ApiResponse.onSuccess("시안 상세 조회 성공", response); } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java similarity index 99% rename from src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java rename to src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index b19c447..6958959 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftQueryService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -26,7 +26,7 @@ @Service @RequiredArgsConstructor -public class DraftQueryService { +public class DraftService { private final CommissionRepository commissionRepository; private final CommissionDraftRepository commissionDraftRepository; From db25484619c0adec4a365362afd974c0fa71b862 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 18:34:39 +0900 Subject: [PATCH 063/102] =?UTF-8?q?Feature(designer):=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=EA=B2=BD=ED=97=98=EC=B9=98/=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=EC=97=85=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/designer/entity/Designer.java | 14 +++++++-- .../designer/entity/enums/DesignerLevel.java | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/designer/entity/enums/DesignerLevel.java diff --git a/src/main/java/ditda/backend/domain/designer/entity/Designer.java b/src/main/java/ditda/backend/domain/designer/entity/Designer.java index 6149c4f..38b956f 100644 --- a/src/main/java/ditda/backend/domain/designer/entity/Designer.java +++ b/src/main/java/ditda/backend/domain/designer/entity/Designer.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Persistable; import ditda.backend.domain.designer.entity.enums.BankName; +import ditda.backend.domain.designer.entity.enums.DesignerLevel; import ditda.backend.domain.user.entity.User; import ditda.backend.global.encryption.AesEncryptConverter; import ditda.backend.global.entity.BaseEntity; @@ -40,9 +41,10 @@ public class Designer extends BaseEntity implements Persistable { @JoinColumn(name = "designer_id") private User user; + @Enumerated(EnumType.STRING) @Builder.Default - @Column(name = "level", nullable = false) - private int level = 1; + @Column(name = "level", length = 20, nullable = false) + private DesignerLevel level = DesignerLevel.LEVEL_1; @Builder.Default @Column(name = "exp", nullable = false) @@ -72,4 +74,12 @@ public static Designer createDesigner(User user, BankName bankName, String accou .accountHolder(accountHolder) .build(); } + + public void gainExp(int amount) { + this.exp += amount; + if (level.canLevelUp(this.exp)) { + this.level = level.next(); + this.exp = 0; + } + } } diff --git a/src/main/java/ditda/backend/domain/designer/entity/enums/DesignerLevel.java b/src/main/java/ditda/backend/domain/designer/entity/enums/DesignerLevel.java new file mode 100644 index 0000000..71d4f09 --- /dev/null +++ b/src/main/java/ditda/backend/domain/designer/entity/enums/DesignerLevel.java @@ -0,0 +1,31 @@ +package ditda.backend.domain.designer.entity.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum DesignerLevel { + + LEVEL_1(500), // LEVEL_2 까지 500 + LEVEL_2(1000), // LEVEL_3 까지 1000 + LEVEL_3(0); // 만렙 + + private final int requiredExp; + + public boolean isMax() { + return this == LEVEL_3; + } + + public boolean canLevelUp(int exp) { + return !isMax() && exp >= requiredExp; + } + + public DesignerLevel next() { + return switch (this) { + case LEVEL_1 -> LEVEL_2; + case LEVEL_2 -> LEVEL_3; + case LEVEL_3 -> LEVEL_3; + }; + } +} From 02af743096a0c9ed176327dae0ab9542f4de2b55 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 18:34:48 +0900 Subject: [PATCH 064/102] =?UTF-8?q?Feature(commission):=201=EC=B0=A8=20?= =?UTF-8?q?=EC=8B=9C=EC=95=88=20=EC=84=A0=ED=83=9D=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/CommissionApplication.java | 8 +++ .../CommissionApplicationRepository.java | 4 ++ .../commission/core/entity/Commission.java | 24 ++++++++ .../core/exception/CommissionErrorCode.java | 6 +- .../draft/controller/DraftController.java | 17 +++++- .../dto/response/DraftSelectResponse.java | 29 ++++++++++ .../draft/entity/CommissionDraft.java | 4 ++ .../draft/exception/DraftErrorCode.java | 3 +- .../repository/CommissionDraftRepository.java | 3 + .../draft/service/DraftService.java | 57 ++++++++++++++++++- .../apipayload/code/GeneralErrorCode.java | 5 +- .../exception/handler/ExceptionAdvice.java | 17 ++++++ 12 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftSelectResponse.java diff --git a/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java index beb0d3f..71be80f 100644 --- a/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java +++ b/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java @@ -63,4 +63,12 @@ public static CommissionApplication create(Commission commission, Designer desig .status(ApplicationStatus.PENDING) .build(); } + + public void markSelected() { + this.status = ApplicationStatus.SELECTED; + } + + public void markRejected() { + this.status = ApplicationStatus.REJECTED; + } } diff --git a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java index 31a23f2..e6acbd9 100644 --- a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java +++ b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java @@ -1,8 +1,12 @@ package ditda.backend.domain.commission.application.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import ditda.backend.domain.commission.application.entity.CommissionApplication; public interface CommissionApplicationRepository extends JpaRepository { + + List findByCommission_Id(Long commissionId); } diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index 0d0d369..8b72aa0 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -1,6 +1,7 @@ package ditda.backend.domain.commission.core.entity; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Objects; import ditda.backend.domain.commission.core.entity.enums.CategoryType; @@ -22,6 +23,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.Version; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -91,6 +93,13 @@ public class Commission extends BaseEntity { @Column(name = "max_revision", nullable = false) private int maxRevision; + @Column(name = "selected_at") + private LocalDateTime selectedAt; + + @Version + @Column(name = "version", nullable = false) + private Long version; + public static Commission create( Instructor instructor, PlanCode planCode, @@ -127,4 +136,19 @@ public boolean isDraftListViewable(int currentDraftCount, LocalDate currentDate) public boolean isOwnedBy(Long instructorId) { return Objects.equals(instructor.getId(), instructorId); } + + public boolean isSelectable() { + return status == CommissionStatus.IN_PROGRESS; + } + + public boolean isDesignerSelected() { + return assignedDesigner != null; + } + + public void selectDesigner(Designer designer, LocalDateTime selectedAt) { + + this.assignedDesigner = designer; + this.status = CommissionStatus.EDITING; + this.selectedAt = selectedAt; + } } diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java index b301f21..c317405 100644 --- a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -34,9 +34,11 @@ public enum CommissionErrorCode implements BaseErrorCode { FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), - // 외주 + // 외주 조회 및 선택 COMMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMISSION_404_01", "외주를 찾을 수 없습니다."), - COMMISSION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "COMMISSION_403_01", "해당 외주에 대한 권한이 없습니다."); + COMMISSION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "COMMISSION_403_01", "해당 외주에 대한 권한이 없습니다."), + COMMISSION_STATUS_INVALID(HttpStatus.CONFLICT, "COMMISSION_409_01", "외주 상태가 올바르지 않습니다."), + DESIGNER_ALREADY_SELECTED(HttpStatus.CONFLICT, "COMMISSION_409_02", "이미 디자이너가 확정된 외주입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java index f656be4..851bc46 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java +++ b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java @@ -3,11 +3,13 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; +import ditda.backend.domain.commission.draft.dto.response.DraftSelectResponse; import ditda.backend.domain.commission.draft.service.DraftService; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -17,7 +19,7 @@ @RestController @RequestMapping("/api/v1/instructors/commissions") @RequiredArgsConstructor -@Tag(name = "Instructor Draft", description = "강사 시안 조회 API") +@Tag(name = "Instructor Draft", description = "강사 시안 조회/선택 API") public class DraftController { private final DraftService draftService; @@ -44,4 +46,17 @@ public ApiResponse getDraftDetail( DraftDetailResponse response = draftService.getDraftDetail(instructorId, commissionId, draftId); return ApiResponse.onSuccess("시안 상세 조회 성공", response); } + + @Operation(summary = "1차 시안 선택", description = "**[시안 확인]** 시안 선택 후 제출하기 클릭 시 해당 시안의 디자이너로 확정하고 외주를 수정 단계로 전환합니다.") + @PostMapping("/{commissionId}/drafts/{draftId}/select") + public ApiResponse selectDraft( + @AuthenticationPrincipal Long instructorId, + @PathVariable Long commissionId, + @PathVariable Long draftId + ) { + + DraftSelectResponse response = draftService.selectDraft(instructorId, commissionId, draftId); + return ApiResponse.onSuccess("1차 시안 선택 성공", response); + } + } diff --git a/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftSelectResponse.java b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftSelectResponse.java new file mode 100644 index 0000000..c1889fb --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/dto/response/DraftSelectResponse.java @@ -0,0 +1,29 @@ +package ditda.backend.domain.commission.draft.dto.response; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "시안 선택 응답") +public record DraftSelectResponse( + + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "선택한 시안 ID", example = "1") + Long selectedDraftId, + + @Schema(description = "선택된 시안 상태", example = "SELECTED") + ApplicationStatus status, + + @Schema(description = "남은 수정 횟수", example = "3") + int remainingRevisionCount, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime selectedAt + +) { +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java index 7f00f56..ff044e7 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java +++ b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java @@ -69,4 +69,8 @@ public static CommissionDraft createRevision( .round(round) .build(); } + + public boolean isDraftFirstRound() { + return round == 0; + } } diff --git a/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java b/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java index fd794cf..c013a70 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java @@ -11,7 +11,8 @@ public enum DraftErrorCode implements BaseErrorCode { DRAFTS_NOT_READY(HttpStatus.CONFLICT, "DRAFT_409_01", "아직 모든 디자이너가 시안을 제출하지 않았습니다."), - DRAFT_NOT_FOUND(HttpStatus.NOT_FOUND, "DRAFT_404_01", "시안을 찾을 수 없습니다."); + DRAFT_NOT_FOUND(HttpStatus.NOT_FOUND, "DRAFT_404_01", "시안을 찾을 수 없습니다."), + DRAFT_INVALID_ROUND(HttpStatus.CONFLICT, "DRAFT_409_02", "해당 시안은 1차 시안이 아닙니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java index 8456f11..37d6f39 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java @@ -1,6 +1,7 @@ package ditda.backend.domain.commission.draft.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -18,4 +19,6 @@ public interface CommissionDraftRepository extends JpaRepository findFirstRoundDrafts(@Param("commissionId") Long commissionId); boolean existsByIdAndCommissionApplication_Commission_Id(Long draftId, Long commissionId); + + Optional findByIdAndCommissionApplication_Commission_Id(Long draftId, Long commissionId); } diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index 6958959..7a1d145 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -1,6 +1,7 @@ package ditda.backend.domain.commission.draft.service; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -9,11 +10,14 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import ditda.backend.domain.commission.application.entity.CommissionApplication; +import ditda.backend.domain.commission.application.repository.CommissionApplicationRepository; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.repository.CommissionRepository; import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; +import ditda.backend.domain.commission.draft.dto.response.DraftSelectResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; import ditda.backend.domain.commission.draft.exception.DraftErrorCode; @@ -28,10 +32,13 @@ @RequiredArgsConstructor public class DraftService { + private static final int EXP_ON_SELECTION = 150; + private final CommissionRepository commissionRepository; private final CommissionDraftRepository commissionDraftRepository; private final CommissionDraftFileRepository commissionDraftFileRepository; private final S3PresignedUrlGenerator s3PresignedUrlGenerator; + private final CommissionApplicationRepository commissionApplicationRepository; // 1차 시안 목록 조회 @Transactional(readOnly = true) @@ -92,6 +99,50 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, return new DraftDetailResponse(commissionId, draftId, files); } + // 1차 시안 확정 + @Transactional + public DraftSelectResponse selectDraft(Long instructorId, Long commissionId, Long draftId) { + + // 1. 외주 조회 + 강사 확인 + Commission commission = getOwnedCommission(commissionId, instructorId); + + // 2. 외주 상태 확인 + if (commission.isDesignerSelected()) { + throw new GeneralException(CommissionErrorCode.DESIGNER_ALREADY_SELECTED); + } + + if (!commission.isSelectable()) { + throw new GeneralException(CommissionErrorCode.COMMISSION_STATUS_INVALID); + } + + // 3. 시안 조회 + 1차 시안 확인 + CommissionDraft draft = commissionDraftRepository + .findByIdAndCommissionApplication_Commission_Id(draftId, commissionId) + .orElseThrow(() -> new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND)); + if (!draft.isDraftFirstRound()) { + throw new GeneralException(DraftErrorCode.DRAFT_INVALID_ROUND); + } + + // 4. 시안 선택 + CommissionApplication selected = draft.getCommissionApplication(); + commission.selectDesigner(selected.getDesigner(), LocalDateTime.now()); + + selected.markSelected(); + selected.getDesigner().gainExp(EXP_ON_SELECTION); + + commissionApplicationRepository.findByCommission_Id(commissionId).stream() + .filter(app -> !app.getId().equals(selected.getId())) + .forEach(CommissionApplication::markRejected); + + return new DraftSelectResponse( + commissionId, + draftId, + selected.getStatus(), + commission.getMaxRevision(), + commission.getSelectedAt() + ); + } + private Commission getOwnedCommission(Long commissionId, Long instructorId) { Commission commission = commissionRepository.findById(commissionId) @@ -109,10 +160,10 @@ private String resolveUrl(CommissionDraftFile file) { return null; } - String waterMarkedFileUrl = file.getWatermarkedFileUrl(); - if (!StringUtils.hasText(waterMarkedFileUrl)) { + String watermarkedFileUrl = file.getWatermarkedFileUrl(); + if (!StringUtils.hasText(watermarkedFileUrl)) { throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); } - return s3PresignedUrlGenerator.generatePrivateGetUrl(waterMarkedFileUrl); + return s3PresignedUrlGenerator.generatePrivateGetUrl(watermarkedFileUrl); } } diff --git a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java index 24d7be6..c3832be 100644 --- a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java +++ b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java @@ -40,7 +40,10 @@ public enum GeneralErrorCode implements BaseErrorCode { EXTERNAL_SERVICE_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT, "SERVER_504_01", "외부 서비스 응답 지연"), // S3 파일 에러 - FILE_URL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_500_01", "파일 URL 생성에 실패했습니다."); + FILE_URL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_500_01", "파일 URL 생성에 실패했습니다."), + + // 동시성 에러 + CONCURRENT_REQUEST(HttpStatus.CONFLICT, "REQ_409_02", "이미 처리 중인 요청입니다. 잠시 후 다시 시도해주세요."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java b/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java index 7ed80e2..ebbd45d 100644 --- a/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java +++ b/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java @@ -4,6 +4,7 @@ import org.springframework.data.core.PropertyReferenceException; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -206,4 +207,20 @@ public ResponseEntity> handleException(Exception exception) .status(code.getHttpStatus()) .body(ApiResponse.onFailure(code, code.getMessage())); } + + /** + * [동시성 충돌 예외] 낙관적 락 충돌(동시 수정) 처리 + */ + @ExceptionHandler(ObjectOptimisticLockingFailureException.class) + public ResponseEntity> handleOptimisticLock( + ObjectOptimisticLockingFailureException exception + ) { + BaseErrorCode code = GeneralErrorCode.CONCURRENT_REQUEST; + + log.warn("OptimisticLockException: {}", exception.getMessage()); + + return ResponseEntity + .status(code.getHttpStatus()) + .body(ApiResponse.onFailure(code, code.getMessage())); + } } From d5fed79e34eb453e1b432168c77ed55fabe86cfc Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 18:42:33 +0900 Subject: [PATCH 065/102] =?UTF-8?q?Refactor(commission):=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=ED=8C=A9=ED=86=A0=EB=A6=AC=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../draft/entity/CommissionDraft.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java index ff044e7..ccfbabf 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java +++ b/src/main/java/ditda/backend/domain/commission/draft/entity/CommissionDraft.java @@ -51,25 +51,6 @@ public class CommissionDraft extends BaseEntity { @Column(name = "is_final", nullable = false) private boolean isFinal = false; - // 1차 시안 (디자이너 지원 완료 후 제출, 수정 0회) - public static CommissionDraft createInitial(CommissionApplication commissionApplication) { - return CommissionDraft.builder() - .commissionApplication(commissionApplication) - .round(0) - .build(); - } - - // 수정 시안 (강사의 수정요청에 대한 응답) - public static CommissionDraft createRevision( - CommissionApplication commissionApplication, - int round - ) { - return CommissionDraft.builder() - .commissionApplication(commissionApplication) - .round(round) - .build(); - } - public boolean isDraftFirstRound() { return round == 0; } From f66285b17aa64f532b222a786930951941f6022f Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 18:44:24 +0900 Subject: [PATCH 066/102] =?UTF-8?q?Refactor(commission):=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EC=9D=91=EB=8B=B5=20=EB=B3=80=ED=99=98=20=EB=B0=8F?= =?UTF-8?q?=20presign=EC=9D=84=20DraftResponseMapper=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../draft/mapper/DraftResponseMapper.java | 52 +++++++++++++++++++ .../draft/service/DraftService.java | 33 ++---------- 2 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java diff --git a/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java new file mode 100644 index 0000000..9660a94 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java @@ -0,0 +1,52 @@ +package ditda.backend.domain.commission.draft.mapper; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; +import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; +import ditda.backend.domain.commission.draft.entity.CommissionDraft; +import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; +import ditda.backend.global.apipayload.code.GeneralErrorCode; +import ditda.backend.global.apipayload.exception.GeneralException; +import ditda.backend.global.s3.S3PresignedUrlGenerator; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DraftResponseMapper { + + private final S3PresignedUrlGenerator s3PresignedUrlGenerator; + + public DraftListResponse.DraftResponse toDraftResponse(CommissionDraft draft, CommissionDraftFile thumbnail) { + + return new DraftListResponse.DraftResponse( + draft.getId(), + thumbnail == null ? null : resolveUrl(thumbnail), + thumbnail == null ? null : thumbnail.getWatermarkStatus() + ); + } + + public DraftDetailResponse.FileResponse toFileResponse(CommissionDraftFile file) { + + return new DraftDetailResponse.FileResponse( + file.getFileOrder(), + resolveUrl(file), + file.getWatermarkStatus() + ); + } + + private String resolveUrl(CommissionDraftFile file) { + + if (!file.isWatermarkCompleted()) { + return null; + } + + String watermarkedFileUrl = file.getWatermarkedFileUrl(); + if (!StringUtils.hasText(watermarkedFileUrl)) { + throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); + } + + return s3PresignedUrlGenerator.generatePrivateGetUrl(watermarkedFileUrl); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index 7a1d145..1b6ecf6 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import ditda.backend.domain.commission.application.entity.CommissionApplication; import ditda.backend.domain.commission.application.repository.CommissionApplicationRepository; @@ -21,11 +20,10 @@ import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; import ditda.backend.domain.commission.draft.exception.DraftErrorCode; +import ditda.backend.domain.commission.draft.mapper.DraftResponseMapper; import ditda.backend.domain.commission.draft.repository.CommissionDraftFileRepository; import ditda.backend.domain.commission.draft.repository.CommissionDraftRepository; -import ditda.backend.global.apipayload.code.GeneralErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.S3PresignedUrlGenerator; import lombok.RequiredArgsConstructor; @Service @@ -37,8 +35,8 @@ public class DraftService { private final CommissionRepository commissionRepository; private final CommissionDraftRepository commissionDraftRepository; private final CommissionDraftFileRepository commissionDraftFileRepository; - private final S3PresignedUrlGenerator s3PresignedUrlGenerator; private final CommissionApplicationRepository commissionApplicationRepository; + private final DraftResponseMapper draftResponseMapper; // 1차 시안 목록 조회 @Transactional(readOnly = true) @@ -62,14 +60,7 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI .collect(Collectors.toMap(f -> f.getCommissionDraft().getId(), f -> f)); List responses = drafts.stream() - .map(d -> { - CommissionDraftFile thumbnail = thumbnailByDraftId.get(d.getId()); - return new DraftListResponse.DraftResponse( - d.getId(), - thumbnail == null ? null : resolveUrl(thumbnail), - thumbnail == null ? null : thumbnail.getWatermarkStatus() - ); - }) + .map(d -> draftResponseMapper.toDraftResponse(d, thumbnailByDraftId.get(d.getId()))) .toList(); return new DraftListResponse(commission.getId(), commission.getTitle(), responses); @@ -90,10 +81,7 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, // 3. 시안 상세 파일들(워터마크) List files = commissionDraftFileRepository .findByCommissionDraftIdOrderByFileOrderAsc(draftId).stream() - .map(f -> new DraftDetailResponse.FileResponse( - f.getFileOrder(), - resolveUrl(f), - f.getWatermarkStatus())) + .map(draftResponseMapper::toFileResponse) .toList(); return new DraftDetailResponse(commissionId, draftId, files); @@ -153,17 +141,4 @@ private Commission getOwnedCommission(Long commissionId, Long instructorId) { } return commission; } - - private String resolveUrl(CommissionDraftFile file) { - - if (!file.isWatermarkCompleted()) { - return null; - } - - String watermarkedFileUrl = file.getWatermarkedFileUrl(); - if (!StringUtils.hasText(watermarkedFileUrl)) { - throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); - } - return s3PresignedUrlGenerator.generatePrivateGetUrl(watermarkedFileUrl); - } } From 81845e0bdc09558042c98ec563b186b33ab22033 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 18:47:56 +0900 Subject: [PATCH 067/102] =?UTF-8?q?Rename(payment):=20PaymentErrorCode=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{PaymenErrorCode.java => PaymentErrorCode.java} | 2 +- .../backend/domain/payment/service/PaymentService.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/ditda/backend/domain/payment/exception/{PaymenErrorCode.java => PaymentErrorCode.java} (92%) diff --git a/src/main/java/ditda/backend/domain/payment/exception/PaymenErrorCode.java b/src/main/java/ditda/backend/domain/payment/exception/PaymentErrorCode.java similarity index 92% rename from src/main/java/ditda/backend/domain/payment/exception/PaymenErrorCode.java rename to src/main/java/ditda/backend/domain/payment/exception/PaymentErrorCode.java index a6b8d02..a1f0037 100644 --- a/src/main/java/ditda/backend/domain/payment/exception/PaymenErrorCode.java +++ b/src/main/java/ditda/backend/domain/payment/exception/PaymentErrorCode.java @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor -public enum PaymenErrorCode implements BaseErrorCode { +public enum PaymentErrorCode implements BaseErrorCode { PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT_404_01", "결제 정보를 찾을 수 없습니다."), PAYMENT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PAYMENT_403_01", "해당 외주에 대한 권한이 없습니다."), diff --git a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java index 5940c9d..f3b8ea2 100644 --- a/src/main/java/ditda/backend/domain/payment/service/PaymentService.java +++ b/src/main/java/ditda/backend/domain/payment/service/PaymentService.java @@ -11,7 +11,7 @@ import ditda.backend.domain.payment.entity.Payment; import ditda.backend.domain.payment.entity.enums.PaymentStatus; import ditda.backend.domain.payment.event.DepositNotifiedEvent; -import ditda.backend.domain.payment.exception.PaymenErrorCode; +import ditda.backend.domain.payment.exception.PaymentErrorCode; import ditda.backend.domain.payment.repository.PaymentRepository; import ditda.backend.domain.term.service.TermService; import ditda.backend.global.apipayload.exception.GeneralException; @@ -47,20 +47,20 @@ public void createPendingPayment( public DepositNotifyResponse notifyDeposit(Long instructorId, Long commissionId) { Payment payment = paymentRepository.findByCommissionId(commissionId) - .orElseThrow(() -> new GeneralException(PaymenErrorCode.PAYMENT_NOT_FOUND)); + .orElseThrow(() -> new GeneralException(PaymentErrorCode.PAYMENT_NOT_FOUND)); Commission commission = payment.getCommission(); if (!Objects.equals(commission.getInstructor().getId(), instructorId)) { - throw new GeneralException(PaymenErrorCode.PAYMENT_ACCESS_DENIED); + throw new GeneralException(PaymentErrorCode.PAYMENT_ACCESS_DENIED); } if (payment.getStatus() != PaymentStatus.PENDING) { - throw new GeneralException(PaymenErrorCode.DEPOSIT_NOTIFY_NOT_ALLOWED); + throw new GeneralException(PaymentErrorCode.DEPOSIT_NOTIFY_NOT_ALLOWED); } if (payment.getDepositNotifiedAt() != null) { - throw new GeneralException(PaymenErrorCode.DEPOSIT_NOTIFY_NOT_ALLOWED); + throw new GeneralException(PaymentErrorCode.DEPOSIT_NOTIFY_NOT_ALLOWED); } payment.markDepositNotified(); From a17afdf0c72cde433e27c74fabc768c850f75594 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 21:52:25 +0900 Subject: [PATCH 068/102] =?UTF-8?q?Fix(commission):=20=EB=B9=88=201?= =?UTF-8?q?=EC=B0=A8=20=EC=8B=9C=EC=95=88=20=ED=95=B8=EB=93=A4=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/commission/draft/service/DraftService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index 1b6ecf6..09f1c09 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -53,6 +53,10 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI throw new GeneralException(DraftErrorCode.DRAFTS_NOT_READY); } + if (drafts.isEmpty()) { + return new DraftListResponse(commission.getId(), commission.getTitle(), List.of()); + } + // 3. 시안별 썸네일(워터마크) (fileOrder = 0) 목록 List draftIds = drafts.stream().map(CommissionDraft::getId).toList(); Map thumbnailByDraftId = commissionDraftFileRepository.findThumbnails(draftIds) From 5891fc15c55a7b0d2c50cd027af7f2693812d01f Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Mon, 15 Jun 2026 21:52:43 +0900 Subject: [PATCH 069/102] =?UTF-8?q?Refactor(s3):=20presigned=20Url=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ditda/backend/global/s3/S3PresignedUrlGenerator.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java index 4614d94..fd46fa9 100644 --- a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java +++ b/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java @@ -8,6 +8,7 @@ import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.enums.BucketType; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -17,6 +18,7 @@ import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; @Component +@Slf4j @RequiredArgsConstructor public class S3PresignedUrlGenerator { @@ -43,6 +45,7 @@ public String generatePrivateGetUrl(String key, Duration ttl) { PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest); return presignedRequest.url().toString(); } catch (SdkException exception) { + log.error("Failed to generate private presigned url. key={}", key, exception); throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); } } From 801577fec552392105804c9c860f88fa272fc402 Mon Sep 17 00:00:00 2001 From: fervovita Date: Wed, 17 Jun 2026 11:14:47 +0900 Subject: [PATCH 070/102] =?UTF-8?q?Refactor(commission):=20Commission=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=84=20=EA=B0=95=EC=82=AC=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...er.java => InstructorCommissionController.java} | 14 +++++++------- ...Facade.java => InstructorCommissionFacade.java} | 10 +++++----- ...rvice.java => InstructorCommissionService.java} | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) rename src/main/java/ditda/backend/domain/commission/core/controller/{CommissionController.java => InstructorCommissionController.java} (84%) rename src/main/java/ditda/backend/domain/commission/core/facade/{CommissionFacade.java => InstructorCommissionFacade.java} (89%) rename src/main/java/ditda/backend/domain/commission/core/service/{CommissionService.java => InstructorCommissionService.java} (99%) diff --git a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java b/src/main/java/ditda/backend/domain/commission/core/controller/InstructorCommissionController.java similarity index 84% rename from src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java rename to src/main/java/ditda/backend/domain/commission/core/controller/InstructorCommissionController.java index d22e94b..b6b5598 100644 --- a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java +++ b/src/main/java/ditda/backend/domain/commission/core/controller/InstructorCommissionController.java @@ -13,7 +13,7 @@ import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; -import ditda.backend.domain.commission.core.facade.CommissionFacade; +import ditda.backend.domain.commission.core.facade.InstructorCommissionFacade; import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -25,15 +25,15 @@ @RequestMapping("/api/v1/instructors/commissions") @RequiredArgsConstructor @Tag(name = "Instructor Commission", description = "강사 새 외주 작성 API") -public class CommissionController { +public class InstructorCommissionController { - private final CommissionFacade commissionFacade; + private final InstructorCommissionFacade instructorCommissionFacade; @Operation(summary = "플랜 조회", description = "**[새 외주 작성]** 플랜 정보를 조회합니다.") @GetMapping("/plans") public ApiResponse getPlans() { - PlanListResponse response = commissionFacade.getPlans(); + PlanListResponse response = instructorCommissionFacade.getPlans(); return ApiResponse.onSuccess("플랜 조회 성공", response); } @@ -48,7 +48,7 @@ public ApiResponse issueFilePresignedUrls( @Valid @RequestBody CommissionFilePresignRequest request ) { - CommissionFilePresignResponse response = commissionFacade.issueFilePresignedUrls(request); + CommissionFilePresignResponse response = instructorCommissionFacade.issueFilePresignedUrls(request); return ApiResponse.onSuccess("새 외주 작성 파일 업로드 URL 발급 성공", response); } @@ -60,7 +60,7 @@ public ApiResponse createCommission( @Valid @RequestBody CommissionCreateRequest request ) { - CommissionCreateResponse response = commissionFacade.createCommission(instructorId, request); + CommissionCreateResponse response = instructorCommissionFacade.createCommission(instructorId, request); return ApiResponse.onSuccess("외주 생성 성공", response); } @@ -71,7 +71,7 @@ public ApiResponse notifyDeposit( @AuthenticationPrincipal Long instructorId, @PathVariable Long commissionId ) { - DepositNotifyResponse response = commissionFacade.notifyDeposit(instructorId, commissionId); + DepositNotifyResponse response = instructorCommissionFacade.notifyDeposit(instructorId, commissionId); return ApiResponse.onSuccess("입금이 접수되었습니다. 관리자 확인 후 진행됩니다.", response); } diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/InstructorCommissionFacade.java similarity index 89% rename from src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java rename to src/main/java/ditda/backend/domain/commission/core/facade/InstructorCommissionFacade.java index 64b79be..2478b01 100644 --- a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java +++ b/src/main/java/ditda/backend/domain/commission/core/facade/InstructorCommissionFacade.java @@ -14,7 +14,7 @@ import ditda.backend.domain.commission.core.dto.response.PlanListResponse; import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; import ditda.backend.domain.commission.core.service.CommissionCreateFileService; -import ditda.backend.domain.commission.core.service.CommissionService; +import ditda.backend.domain.commission.core.service.InstructorCommissionService; import ditda.backend.domain.commission.core.validator.CommissionCreateValidator; import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; import ditda.backend.domain.payment.service.PaymentService; @@ -23,15 +23,15 @@ @Component @RequiredArgsConstructor -public class CommissionFacade { +public class InstructorCommissionFacade { - private final CommissionService commissionService; + private final InstructorCommissionService instructorCommissionService; private final CommissionCreateFileService commissionCreateFileService; private final CommissionCreateValidator commissionCreateValidator; private final PaymentService paymentService; public PlanListResponse getPlans() { - return commissionService.getPlans(); + return instructorCommissionService.getPlans(); } public CommissionFilePresignResponse issueFilePresignedUrls(CommissionFilePresignRequest request) { @@ -72,7 +72,7 @@ public CommissionCreateResponse createCommission( promotedKeys.addAll(permanent); } - return commissionService.createCommission(instructorId, request, handler, commissionFiles); + return instructorCommissionService.createCommission(instructorId, request, handler, commissionFiles); } catch (Exception original) { try { commissionCreateFileService.deleteFiles(promotedKeys); diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java similarity index 99% rename from src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java rename to src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java index 4852fbc..3869635 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java @@ -24,7 +24,7 @@ @Service @RequiredArgsConstructor -public class CommissionService { +public class InstructorCommissionService { private final CommissionRepository commissionRepository; private final CommissionConceptRepository commissionConceptRepository; From dcf1480a87db8e86abf686adfa54f4636ce4d65d Mon Sep 17 00:00:00 2001 From: fervovita Date: Wed, 17 Jun 2026 16:31:55 +0900 Subject: [PATCH 071/102] =?UTF-8?q?Feat(application):=20ApplicationStatus?= =?UTF-8?q?=20enum=20=ED=99=95=EC=9E=A5=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/CommissionApplication.java | 16 +++++++++++---- .../entity/enums/ApplicationStatus.java | 16 ++++++++++++--- .../exception/ApplicationErrorCode.java | 20 +++++++++++++++++++ .../draft/service/DraftService.java | 4 ++-- 4 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/application/exception/ApplicationErrorCode.java diff --git a/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java b/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java index 71be80f..2cfd54d 100644 --- a/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java +++ b/src/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.java @@ -1,8 +1,10 @@ package ditda.backend.domain.commission.application.entity; import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import ditda.backend.domain.commission.application.exception.ApplicationErrorCode; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.designer.entity.Designer; +import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -64,11 +66,17 @@ public static CommissionApplication create(Commission commission, Designer desig .build(); } - public void markSelected() { - this.status = ApplicationStatus.SELECTED; + public void markDraftSelected() { + if (status != ApplicationStatus.DRAFT_SUBMITTED) { + throw new GeneralException(ApplicationErrorCode.INVALID_STATUS_FOR_DRAFT_SELECTION); + } + this.status = ApplicationStatus.DRAFT_SELECTED; } - public void markRejected() { - this.status = ApplicationStatus.REJECTED; + public void markDraftRejected() { + if (status != ApplicationStatus.DRAFT_SUBMITTED) { + throw new GeneralException(ApplicationErrorCode.INVALID_STATUS_FOR_DRAFT_REJECTION); + } + this.status = ApplicationStatus.DRAFT_REJECTED; } } diff --git a/src/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.java b/src/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.java index 310b8a3..4a35057 100644 --- a/src/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.java +++ b/src/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.java @@ -1,7 +1,17 @@ package ditda.backend.domain.commission.application.entity.enums; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor public enum ApplicationStatus { - PENDING, // 지원 완료 - SELECTED, // 강사 시안 선택 - REJECTED // 미선택 + PENDING("지원 완료"), + ASSIGNED("1차 시안 대상자"), + DRAFT_SUBMITTED("1차 시안 제출 완료"), + DRAFT_SELECTED("최종 선택"), + APPLICATION_REJECTED("지원 탈락"), + DRAFT_REJECTED("최종 탈락"); + + private final String description; } diff --git a/src/main/java/ditda/backend/domain/commission/application/exception/ApplicationErrorCode.java b/src/main/java/ditda/backend/domain/commission/application/exception/ApplicationErrorCode.java new file mode 100644 index 0000000..5efea94 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/application/exception/ApplicationErrorCode.java @@ -0,0 +1,20 @@ +package ditda.backend.domain.commission.application.exception; + +import org.springframework.http.HttpStatus; + +import ditda.backend.global.apipayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ApplicationErrorCode implements BaseErrorCode { + + INVALID_STATUS_FOR_DRAFT_SELECTION(HttpStatus.BAD_REQUEST, "APP_400_01", "1차 시안 제출 완료 상태에서만 결과를 선택할 수 있습니다."), + INVALID_STATUS_FOR_DRAFT_REJECTION(HttpStatus.BAD_REQUEST, "APP_400_02", "1차 시안 제출 완료 상태에서만 탈락 처리할 수 있습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index 09f1c09..69c6d1a 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -119,12 +119,12 @@ public DraftSelectResponse selectDraft(Long instructorId, Long commissionId, Lon CommissionApplication selected = draft.getCommissionApplication(); commission.selectDesigner(selected.getDesigner(), LocalDateTime.now()); - selected.markSelected(); + selected.markDraftSelected(); selected.getDesigner().gainExp(EXP_ON_SELECTION); commissionApplicationRepository.findByCommission_Id(commissionId).stream() .filter(app -> !app.getId().equals(selected.getId())) - .forEach(CommissionApplication::markRejected); + .forEach(CommissionApplication::markDraftRejected); return new DraftSelectResponse( commissionId, From dff1f250d9e0cb7df911fc06cf788923a161fa69 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 17 Jun 2026 21:35:05 +0900 Subject: [PATCH 072/102] =?UTF-8?q?Refactor(commission):=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EC=84=A0=ED=83=9D=20facade=20=EB=B0=8F=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ApplicationService.java | 27 ++++++++ .../core/repository/CommissionRepository.java | 20 ++++++ .../core/service/CommissionService.java | 34 ++++++++++ .../draft/controller/DraftController.java | 10 +-- .../commission/draft/facade/DraftFacade.java | 62 ++++++++++++++++++ .../draft/service/DraftService.java | 64 +++---------------- .../designer/entity/enums/ExpReward.java | 16 +++++ 7 files changed, 172 insertions(+), 61 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java create mode 100644 src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java create mode 100644 src/main/java/ditda/backend/domain/designer/entity/enums/ExpReward.java diff --git a/src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java b/src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java new file mode 100644 index 0000000..c0b8d0f --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java @@ -0,0 +1,27 @@ +package ditda.backend.domain.commission.application.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.application.repository.CommissionApplicationRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ApplicationService { + + private final CommissionApplicationRepository commissionApplicationRepository; + + @Transactional + public void applySelection(Long commissionId, Long selectedApplicationId) { + + commissionApplicationRepository.findByCommission_Id(commissionId) + .forEach(app -> { + if (app.getId().equals(selectedApplicationId)) { + app.markDraftSelected(); + } else { + app.markDraftRejected(); + } + }); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java index fd75942..c433833 100644 --- a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java @@ -1,8 +1,28 @@ package ditda.backend.domain.commission.core.repository; +import java.time.LocalDateTime; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.designer.entity.Designer; public interface CommissionRepository extends JpaRepository { + + @Modifying + @Query("update Commission c " + + "set c.assignedDesigner = :designer, " + + "c.status = CommissionStatus.EDITING, " + + "c.selectedAt = :now " + + "where c.id = :commissionId " + + "and c.status = CommissionStatus.IN_PROGRESS " + + "and c.assignedDesigner is null") + int selectDesignerIfAvailable( + @Param("commissionId") Long id, + @Param("designer") Designer designer, + @Param("now") LocalDateTime now + ); } diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java index 4852fbc..1d7cd5b 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionService.java @@ -1,5 +1,6 @@ package ditda.backend.domain.commission.core.service; +import java.time.LocalDateTime; import java.util.List; import org.springframework.stereotype.Service; @@ -13,13 +14,16 @@ import ditda.backend.domain.commission.core.entity.CommissionColor; import ditda.backend.domain.commission.core.entity.CommissionConcept; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; +import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; import ditda.backend.domain.commission.core.repository.CommissionColorRepository; import ditda.backend.domain.commission.core.repository.CommissionConceptRepository; import ditda.backend.domain.commission.core.repository.CommissionRepository; +import ditda.backend.domain.designer.entity.Designer; import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.domain.instructor.service.InstructorService; import ditda.backend.domain.payment.service.PaymentService; +import ditda.backend.global.apipayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @Service @@ -87,6 +91,36 @@ public CommissionCreateResponse createCommission( return CommissionCreateResponse.from(commission); } + // 외주 조회 + 강사 확인 + @Transactional(readOnly = true) + public Commission getOwnedCommission(Long commissionId, Long instructorId) { + + Commission commission = commissionRepository.findById(commissionId) + .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); + + if (!commission.isOwnedBy(instructorId)) { + throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); + } + return commission; + } + + // 디자이너 선택 + @Transactional + public void selectDesigner(Commission commission, Designer designer, LocalDateTime now) { + + if (commission.isDesignerSelected()) { + throw new GeneralException(CommissionErrorCode.DESIGNER_ALREADY_SELECTED); + } + if (!commission.isSelectable()) { + throw new GeneralException(CommissionErrorCode.COMMISSION_STATUS_INVALID); + } + + int updated = commissionRepository.selectDesignerIfAvailable(commission.getId(), designer, now); + if (updated == 0) { + throw new GeneralException(CommissionErrorCode.DESIGNER_ALREADY_SELECTED); + } + } + // 컨셉 저장 private void saveConcepts(Commission commission, CommissionCreateRequest.DesignInfo design) { diff --git a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java index 851bc46..971042a 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java +++ b/src/main/java/ditda/backend/domain/commission/draft/controller/DraftController.java @@ -10,7 +10,7 @@ import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.dto.response.DraftSelectResponse; -import ditda.backend.domain.commission.draft.service.DraftService; +import ditda.backend.domain.commission.draft.facade.DraftFacade; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -22,7 +22,7 @@ @Tag(name = "Instructor Draft", description = "강사 시안 조회/선택 API") public class DraftController { - private final DraftService draftService; + private final DraftFacade draftFacade; @Operation(summary = "1차 시안 목록 조회", description = "**[시안 제출 조회]** 외주명 클릭 시 디자이너들이 제출한 1차 시안 목록을 조회합니다.") @GetMapping("/{commissionId}/drafts") @@ -31,7 +31,7 @@ public ApiResponse getFirstRoundDrafts( @PathVariable Long commissionId ) { - DraftListResponse response = draftService.getFirstRoundDrafts(instructorId, commissionId); + DraftListResponse response = draftFacade.getFirstRoundDrafts(instructorId, commissionId); return ApiResponse.onSuccess("1차 시안 목록 조회 성공", response); } @@ -43,7 +43,7 @@ public ApiResponse getDraftDetail( @PathVariable Long draftId ) { - DraftDetailResponse response = draftService.getDraftDetail(instructorId, commissionId, draftId); + DraftDetailResponse response = draftFacade.getDraftDetail(instructorId, commissionId, draftId); return ApiResponse.onSuccess("시안 상세 조회 성공", response); } @@ -55,7 +55,7 @@ public ApiResponse selectDraft( @PathVariable Long draftId ) { - DraftSelectResponse response = draftService.selectDraft(instructorId, commissionId, draftId); + DraftSelectResponse response = draftFacade.selectDraft(instructorId, commissionId, draftId); return ApiResponse.onSuccess("1차 시안 선택 성공", response); } diff --git a/src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java b/src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java new file mode 100644 index 0000000..781ca2d --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java @@ -0,0 +1,62 @@ +package ditda.backend.domain.commission.draft.facade; + +import java.time.LocalDateTime; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.application.entity.CommissionApplication; +import ditda.backend.domain.commission.application.service.ApplicationService; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.service.CommissionService; +import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; +import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; +import ditda.backend.domain.commission.draft.dto.response.DraftSelectResponse; +import ditda.backend.domain.commission.draft.service.DraftService; +import ditda.backend.domain.designer.entity.Designer; +import ditda.backend.domain.designer.entity.enums.ExpReward; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DraftFacade { + + private final CommissionService commissionService; + private final DraftService draftService; + private final ApplicationService applicationService; + + // 1차 시안 목록 조회 + public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionId) { + return draftService.getFirstRoundDrafts(instructorId, commissionId); + } + + // 시안 상세 조회 + public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, Long draftId) { + return draftService.getDraftDetail(instructorId, commissionId, draftId); + } + + // 1차 시안 선택 + @Transactional + public DraftSelectResponse selectDraft(Long instructorId, Long commissionId, Long draftId) { + + Commission commission = commissionService.getOwnedCommission(commissionId, instructorId); + + CommissionApplication selected = draftService.getApplicationForSelection(commissionId, draftId); + Designer designer = selected.getDesigner(); + + LocalDateTime now = LocalDateTime.now(); + commissionService.selectDesigner(commission, designer, now); + + applicationService.applySelection(commissionId, selected.getId()); + + designer.gainExp(ExpReward.DRAFT_SELECTED.getAmount()); + + return new DraftSelectResponse( + commissionId, + draftId, + selected.getStatus(), + commission.getMaxRevision(), + now + ); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index 69c6d1a..e86bc55 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -1,7 +1,6 @@ package ditda.backend.domain.commission.draft.service; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -10,13 +9,10 @@ import org.springframework.transaction.annotation.Transactional; import ditda.backend.domain.commission.application.entity.CommissionApplication; -import ditda.backend.domain.commission.application.repository.CommissionApplicationRepository; import ditda.backend.domain.commission.core.entity.Commission; -import ditda.backend.domain.commission.core.exception.CommissionErrorCode; -import ditda.backend.domain.commission.core.repository.CommissionRepository; +import ditda.backend.domain.commission.core.service.CommissionService; import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; -import ditda.backend.domain.commission.draft.dto.response.DraftSelectResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; import ditda.backend.domain.commission.draft.exception.DraftErrorCode; @@ -30,12 +26,9 @@ @RequiredArgsConstructor public class DraftService { - private static final int EXP_ON_SELECTION = 150; - - private final CommissionRepository commissionRepository; + private final CommissionService commissionService; private final CommissionDraftRepository commissionDraftRepository; private final CommissionDraftFileRepository commissionDraftFileRepository; - private final CommissionApplicationRepository commissionApplicationRepository; private final DraftResponseMapper draftResponseMapper; // 1차 시안 목록 조회 @@ -43,7 +36,7 @@ public class DraftService { public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionId) { // 1. 외주 조회 + 강사 확인 - Commission commission = getOwnedCommission(commissionId, instructorId); + Commission commission = commissionService.getOwnedCommission(commissionId, instructorId); // 2. 1차 시안 (round = 0) 목록 확인 List drafts = commissionDraftRepository.findFirstRoundDrafts(commissionId); @@ -75,7 +68,7 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, Long draftId) { // 1. 외주 조회 + 강사 확인 - getOwnedCommission(commissionId, instructorId); + commissionService.getOwnedCommission(commissionId, instructorId); // 2. 시안 조회 if (!commissionDraftRepository.existsByIdAndCommissionApplication_Commission_Id(draftId, commissionId)) { @@ -91,58 +84,17 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, return new DraftDetailResponse(commissionId, draftId, files); } - // 1차 시안 확정 - @Transactional - public DraftSelectResponse selectDraft(Long instructorId, Long commissionId, Long draftId) { - - // 1. 외주 조회 + 강사 확인 - Commission commission = getOwnedCommission(commissionId, instructorId); - - // 2. 외주 상태 확인 - if (commission.isDesignerSelected()) { - throw new GeneralException(CommissionErrorCode.DESIGNER_ALREADY_SELECTED); - } - - if (!commission.isSelectable()) { - throw new GeneralException(CommissionErrorCode.COMMISSION_STATUS_INVALID); - } + // 1차 시안 + public CommissionApplication getApplicationForSelection(Long commissionId, Long draftId) { - // 3. 시안 조회 + 1차 시안 확인 CommissionDraft draft = commissionDraftRepository .findByIdAndCommissionApplication_Commission_Id(draftId, commissionId) .orElseThrow(() -> new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND)); + if (!draft.isDraftFirstRound()) { throw new GeneralException(DraftErrorCode.DRAFT_INVALID_ROUND); } - // 4. 시안 선택 - CommissionApplication selected = draft.getCommissionApplication(); - commission.selectDesigner(selected.getDesigner(), LocalDateTime.now()); - - selected.markDraftSelected(); - selected.getDesigner().gainExp(EXP_ON_SELECTION); - - commissionApplicationRepository.findByCommission_Id(commissionId).stream() - .filter(app -> !app.getId().equals(selected.getId())) - .forEach(CommissionApplication::markDraftRejected); - - return new DraftSelectResponse( - commissionId, - draftId, - selected.getStatus(), - commission.getMaxRevision(), - commission.getSelectedAt() - ); - } - - private Commission getOwnedCommission(Long commissionId, Long instructorId) { - - Commission commission = commissionRepository.findById(commissionId) - .orElseThrow(() -> new GeneralException(CommissionErrorCode.COMMISSION_NOT_FOUND)); - - if (!commission.isOwnedBy(instructorId)) { - throw new GeneralException(CommissionErrorCode.COMMISSION_ACCESS_DENIED); - } - return commission; + return draft.getCommissionApplication(); } } diff --git a/src/main/java/ditda/backend/domain/designer/entity/enums/ExpReward.java b/src/main/java/ditda/backend/domain/designer/entity/enums/ExpReward.java new file mode 100644 index 0000000..525e8f5 --- /dev/null +++ b/src/main/java/ditda/backend/domain/designer/entity/enums/ExpReward.java @@ -0,0 +1,16 @@ +package ditda.backend.domain.designer.entity.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ExpReward { + + PORTFOLIO(50), + FIRST_DRAFT_SUBMIT(100), + DRAFT_SELECTED(150), + REVISION(200); + + private final int amount; +} From afeaf94f206864adda4114926430d9e0284af8f6 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 17 Jun 2026 21:50:17 +0900 Subject: [PATCH 073/102] =?UTF-8?q?Refactor(commission):=20DraftService=20?= =?UTF-8?q?mapper=20=EB=AA=A8=EB=93=A0=20=EC=B1=85=EC=9E=84=20=EC=9C=84?= =?UTF-8?q?=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../draft/mapper/DraftResponseMapper.java | 40 +++++++++++++++++-- .../draft/service/DraftService.java | 26 ++++-------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java index 9660a94..31181f6 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java +++ b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java @@ -1,8 +1,12 @@ package ditda.backend.domain.commission.draft.mapper; +import java.util.List; +import java.util.Map; + import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.draft.dto.response.DraftDetailResponse; import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; @@ -18,16 +22,44 @@ public class DraftResponseMapper { private final S3PresignedUrlGenerator s3PresignedUrlGenerator; - public DraftListResponse.DraftResponse toDraftResponse(CommissionDraft draft, CommissionDraftFile thumbnail) { + public DraftListResponse toDraftListResponse( + Commission commission, + List drafts, + Map thumbnailByDraftId + ) { + + List items = drafts.stream() + .map(d -> toDraftResponse(d, thumbnailByDraftId.get(d.getId()))) + .toList(); + + return new DraftListResponse(commission.getId(), commission.getTitle(), items); + } + + public DraftDetailResponse toDraftDetailResponse( + Long commissionId, + Long draftId, + List files + ) { + + List fileResponses = files.stream() + .map(this::toFileResponse) + .toList(); + + return new DraftDetailResponse(commissionId, draftId, fileResponses); + } + + private DraftListResponse.DraftResponse toDraftResponse( + CommissionDraft draft, + CommissionDraftFile thumbnail + ) { return new DraftListResponse.DraftResponse( draft.getId(), thumbnail == null ? null : resolveUrl(thumbnail), - thumbnail == null ? null : thumbnail.getWatermarkStatus() - ); + thumbnail == null ? null : thumbnail.getWatermarkStatus()); } - public DraftDetailResponse.FileResponse toFileResponse(CommissionDraftFile file) { + private DraftDetailResponse.FileResponse toFileResponse(CommissionDraftFile file) { return new DraftDetailResponse.FileResponse( file.getFileOrder(), diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index e86bc55..bd87fe8 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -46,21 +46,14 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI throw new GeneralException(DraftErrorCode.DRAFTS_NOT_READY); } - if (drafts.isEmpty()) { - return new DraftListResponse(commission.getId(), commission.getTitle(), List.of()); - } - - // 3. 시안별 썸네일(워터마크) (fileOrder = 0) 목록 - List draftIds = drafts.stream().map(CommissionDraft::getId).toList(); - Map thumbnailByDraftId = commissionDraftFileRepository.findThumbnails(draftIds) + Map thumbnailByDraftId = drafts.isEmpty() + ? Map.of() + : commissionDraftFileRepository + .findThumbnails(drafts.stream().map(CommissionDraft::getId).toList()) .stream() .collect(Collectors.toMap(f -> f.getCommissionDraft().getId(), f -> f)); - List responses = drafts.stream() - .map(d -> draftResponseMapper.toDraftResponse(d, thumbnailByDraftId.get(d.getId()))) - .toList(); - - return new DraftListResponse(commission.getId(), commission.getTitle(), responses); + return draftResponseMapper.toDraftListResponse(commission, drafts, thumbnailByDraftId); } // 시안 상세 조회 @@ -75,13 +68,10 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, throw new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND); } - // 3. 시안 상세 파일들(워터마크) - List files = commissionDraftFileRepository - .findByCommissionDraftIdOrderByFileOrderAsc(draftId).stream() - .map(draftResponseMapper::toFileResponse) - .toList(); + List files = commissionDraftFileRepository + .findByCommissionDraftIdOrderByFileOrderAsc(draftId); - return new DraftDetailResponse(commissionId, draftId, files); + return draftResponseMapper.toDraftDetailResponse(commissionId, draftId, files); } // 1차 시안 From 498a68a2fdff1ac6623523d6f6abec0feae96017 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 17 Jun 2026 21:56:16 +0900 Subject: [PATCH 074/102] =?UTF-8?q?Refactor:=20=EB=8F=99=EC=8B=9C=EC=84=B1?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EB=82=99=EA=B4=80=EC=A0=81?= =?UTF-8?q?=20=EB=9D=BD=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20=EB=A0=88=EB=B2=A8=EC=97=85=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/core/entity/Commission.java | 5 ----- .../domain/designer/entity/Designer.java | 4 ++-- .../apipayload/code/GeneralErrorCode.java | 5 +---- .../exception/handler/ExceptionAdvice.java | 17 ----------------- 4 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index 8b72aa0..13177a7 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -23,7 +23,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import jakarta.persistence.Version; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -96,10 +95,6 @@ public class Commission extends BaseEntity { @Column(name = "selected_at") private LocalDateTime selectedAt; - @Version - @Column(name = "version", nullable = false) - private Long version; - public static Commission create( Instructor instructor, PlanCode planCode, diff --git a/src/main/java/ditda/backend/domain/designer/entity/Designer.java b/src/main/java/ditda/backend/domain/designer/entity/Designer.java index 38b956f..d337089 100644 --- a/src/main/java/ditda/backend/domain/designer/entity/Designer.java +++ b/src/main/java/ditda/backend/domain/designer/entity/Designer.java @@ -77,9 +77,9 @@ public static Designer createDesigner(User user, BankName bankName, String accou public void gainExp(int amount) { this.exp += amount; - if (level.canLevelUp(this.exp)) { + while (level.canLevelUp(this.exp)) { + this.exp -= level.getRequiredExp(); this.level = level.next(); - this.exp = 0; } } } diff --git a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java index c3832be..24d7be6 100644 --- a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java +++ b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java @@ -40,10 +40,7 @@ public enum GeneralErrorCode implements BaseErrorCode { EXTERNAL_SERVICE_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT, "SERVER_504_01", "외부 서비스 응답 지연"), // S3 파일 에러 - FILE_URL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_500_01", "파일 URL 생성에 실패했습니다."), - - // 동시성 에러 - CONCURRENT_REQUEST(HttpStatus.CONFLICT, "REQ_409_02", "이미 처리 중인 요청입니다. 잠시 후 다시 시도해주세요."); + FILE_URL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_500_01", "파일 URL 생성에 실패했습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java b/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java index ebbd45d..7ed80e2 100644 --- a/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java +++ b/src/main/java/ditda/backend/global/apipayload/exception/handler/ExceptionAdvice.java @@ -4,7 +4,6 @@ import org.springframework.data.core.PropertyReferenceException; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -207,20 +206,4 @@ public ResponseEntity> handleException(Exception exception) .status(code.getHttpStatus()) .body(ApiResponse.onFailure(code, code.getMessage())); } - - /** - * [동시성 충돌 예외] 낙관적 락 충돌(동시 수정) 처리 - */ - @ExceptionHandler(ObjectOptimisticLockingFailureException.class) - public ResponseEntity> handleOptimisticLock( - ObjectOptimisticLockingFailureException exception - ) { - BaseErrorCode code = GeneralErrorCode.CONCURRENT_REQUEST; - - log.warn("OptimisticLockException: {}", exception.getMessage()); - - return ResponseEntity - .status(code.getHttpStatus()) - .body(ApiResponse.onFailure(code, code.getMessage())); - } } From 9ffef4ceeeb4960312dfc518e97622194dd7f3c8 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 17 Jun 2026 22:00:04 +0900 Subject: [PATCH 075/102] =?UTF-8?q?Fix(commission):=20=EC=9B=8C=ED=84=B0?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=ED=8C=8C=EC=9D=BC=20URL=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B6=88=EA=B0=80=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/commission/draft/exception/DraftErrorCode.java | 3 ++- .../domain/commission/draft/mapper/DraftResponseMapper.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java b/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java index c013a70..5c94d1b 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/draft/exception/DraftErrorCode.java @@ -12,7 +12,8 @@ public enum DraftErrorCode implements BaseErrorCode { DRAFTS_NOT_READY(HttpStatus.CONFLICT, "DRAFT_409_01", "아직 모든 디자이너가 시안을 제출하지 않았습니다."), DRAFT_NOT_FOUND(HttpStatus.NOT_FOUND, "DRAFT_404_01", "시안을 찾을 수 없습니다."), - DRAFT_INVALID_ROUND(HttpStatus.CONFLICT, "DRAFT_409_02", "해당 시안은 1차 시안이 아닙니다."); + DRAFT_INVALID_ROUND(HttpStatus.CONFLICT, "DRAFT_409_02", "해당 시안은 1차 시안이 아닙니다."), + WATERMARK_FILE_URL_MISSING(HttpStatus.INTERNAL_SERVER_ERROR, "DRAFT_500_01", "워터마크가 완료되었지만 파일 URL이 존재하지 않습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java index 31181f6..9faba95 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java +++ b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java @@ -11,7 +11,7 @@ import ditda.backend.domain.commission.draft.dto.response.DraftListResponse; import ditda.backend.domain.commission.draft.entity.CommissionDraft; import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; -import ditda.backend.global.apipayload.code.GeneralErrorCode; +import ditda.backend.domain.commission.draft.exception.DraftErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.S3PresignedUrlGenerator; import lombok.RequiredArgsConstructor; @@ -76,7 +76,7 @@ private String resolveUrl(CommissionDraftFile file) { String watermarkedFileUrl = file.getWatermarkedFileUrl(); if (!StringUtils.hasText(watermarkedFileUrl)) { - throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); + throw new GeneralException(DraftErrorCode.WATERMARK_FILE_URL_MISSING); } return s3PresignedUrlGenerator.generatePrivateGetUrl(watermarkedFileUrl); From 727d7a97f85d8e254fdb4e704da32cfb75962548 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 17 Jun 2026 22:17:40 +0900 Subject: [PATCH 076/102] =?UTF-8?q?Refactor(commission):=20=EA=B0=80?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20@Query=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ApplicationService.java | 3 ++- .../repository/CommissionDraftRepository.java | 18 ++++++++++++++++-- .../commission/draft/service/DraftService.java | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java b/src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java index c0b8d0f..ff9d3b1 100644 --- a/src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java +++ b/src/main/java/ditda/backend/domain/commission/application/service/ApplicationService.java @@ -3,6 +3,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; import ditda.backend.domain.commission.application.repository.CommissionApplicationRepository; import lombok.RequiredArgsConstructor; @@ -19,7 +20,7 @@ public void applySelection(Long commissionId, Long selectedApplicationId) { .forEach(app -> { if (app.getId().equals(selectedApplicationId)) { app.markDraftSelected(); - } else { + } else if (app.getStatus() == ApplicationStatus.DRAFT_SUBMITTED) { app.markDraftRejected(); } }); diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java index 37d6f39..6f95f72 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java @@ -18,7 +18,21 @@ public interface CommissionDraftRepository extends JpaRepository findFirstRoundDrafts(@Param("commissionId") Long commissionId); - boolean existsByIdAndCommissionApplication_Commission_Id(Long draftId, Long commissionId); + @Query("select d from CommissionDraft d " + + "join d.commissionApplication ca " + + "where d.id = :draftId " + + "and ca.commission.id = :commissionId") + Optional findDraftInCommission( + @Param("draftId") Long draftId, + @Param("commissionId") Long commissionId + ); - Optional findByIdAndCommissionApplication_Commission_Id(Long draftId, Long commissionId); + @Query("select count(d) > 0 from CommissionDraft d " + + "join d.commissionApplication ca " + + "where d.id = :draftId " + + "and ca.commission.id = :commissionId") + boolean existsDraftInCommission( + @Param("draftId") Long draftId, + @Param("commissionId") Long commissionId + ); } diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index bd87fe8..78a939d 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -64,7 +64,7 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, commissionService.getOwnedCommission(commissionId, instructorId); // 2. 시안 조회 - if (!commissionDraftRepository.existsByIdAndCommissionApplication_Commission_Id(draftId, commissionId)) { + if (!commissionDraftRepository.existsDraftInCommission(draftId, commissionId)) { throw new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND); } @@ -78,7 +78,7 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, public CommissionApplication getApplicationForSelection(Long commissionId, Long draftId) { CommissionDraft draft = commissionDraftRepository - .findByIdAndCommissionApplication_Commission_Id(draftId, commissionId) + .findDraftInCommission(draftId, commissionId) .orElseThrow(() -> new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND)); if (!draft.isDraftFirstRound()) { From 08f6ced600c279f01a3fca7443ec579f0f774520 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Wed, 17 Jun 2026 23:04:45 +0900 Subject: [PATCH 077/102] =?UTF-8?q?Refactor(commission):=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=ED=95=A8=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/commission/core/entity/Commission.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index 13177a7..0ac935b 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -139,11 +139,4 @@ public boolean isSelectable() { public boolean isDesignerSelected() { return assignedDesigner != null; } - - public void selectDesigner(Designer designer, LocalDateTime selectedAt) { - - this.assignedDesigner = designer; - this.status = CommissionStatus.EDITING; - this.selectedAt = selectedAt; - } } From a3f07d3126a6fa933e41442031c5aed4bdb6d3f4 Mon Sep 17 00:00:00 2001 From: fervovita Date: Thu, 18 Jun 2026 01:05:04 +0900 Subject: [PATCH 078/102] =?UTF-8?q?Feat(dashboard):=20=EA=B0=95=EC=82=AC?= =?UTF-8?q?=20=EC=8B=9C=EC=95=88=20=EC=A0=9C=EC=B6=9C=20=ED=98=84=ED=99=A9?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommissionApplicationRepository.java | 12 ++++ .../ApplicationSubmissionCount.java | 9 +++ .../service/CommissionApplicationService.java | 38 ++++++++++ .../commission/core/entity/Commission.java | 5 ++ .../core/repository/CommissionRepository.java | 5 ++ .../service/InstructorCommissionService.java | 6 ++ .../InstructorDashboardController.java | 31 +++++++++ .../DraftSubmissionCommissionResponse.java | 69 +++++++++++++++++++ .../service/InstructorDashboardService.java | 46 +++++++++++++ 9 files changed, 221 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java create mode 100644 src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java diff --git a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java index e6acbd9..b6d77d3 100644 --- a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java +++ b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java @@ -3,10 +3,22 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.application.entity.CommissionApplication; +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import ditda.backend.domain.commission.application.repository.projection.ApplicationSubmissionCount; public interface CommissionApplicationRepository extends JpaRepository { List findByCommission_Id(Long commissionId); + + @Query("SELECT a.commission.id AS commissionId, COUNT(a) AS count " + + "FROM CommissionApplication a " + + "WHERE a.commission.id IN :commissionIds AND a.status = :status " + + "GROUP BY a.commission.id") + List countByCommissionIdsAndStatus( + @Param("commissionIds") List commissionIds, + @Param("status") ApplicationStatus status); } diff --git a/src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java b/src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java new file mode 100644 index 0000000..8185f76 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java @@ -0,0 +1,9 @@ +package ditda.backend.domain.commission.application.repository.projection; + +public interface ApplicationSubmissionCount { + + Long getCommissionId(); + + Long getCount(); + +} diff --git a/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java b/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java new file mode 100644 index 0000000..12e2139 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java @@ -0,0 +1,38 @@ +package ditda.backend.domain.commission.application.service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import ditda.backend.domain.commission.application.repository.CommissionApplicationRepository; +import ditda.backend.domain.commission.application.repository.projection.ApplicationSubmissionCount; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CommissionApplicationService { + + private final CommissionApplicationRepository commissionApplicationRepository; + + // commission별 상태 카운트 + public Map countApplicationByStatus(List commissionIds, ApplicationStatus status) { + if (commissionIds.isEmpty()) { + return Map.of(); + } + + // 집계 결과 + Map counts = commissionApplicationRepository + .countByCommissionIdsAndStatus(commissionIds, status).stream() + .collect(Collectors.toMap( + ApplicationSubmissionCount::getCommissionId, + ApplicationSubmissionCount::getCount + )); + + // 0 보정 + return commissionIds.stream() + .collect(Collectors.toMap(id -> id, id -> counts.getOrDefault(id, 0L))); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index 8b72aa0..ed6b262 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -151,4 +151,9 @@ public void selectDesigner(Designer designer, LocalDateTime selectedAt) { this.status = CommissionStatus.EDITING; this.selectedAt = selectedAt; } + + // 디자이너 모집 정원 + public int getDesignerCount() { + return planCode.getDesignerCount(); + } } diff --git a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java index fd75942..867662d 100644 --- a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java @@ -1,8 +1,13 @@ package ditda.backend.domain.commission.core.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; public interface CommissionRepository extends JpaRepository { + + List findByInstructorIdAndStatus(Long instructorId, CommissionStatus status); } diff --git a/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java index 3869635..eb04cde 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java @@ -13,6 +13,7 @@ import ditda.backend.domain.commission.core.entity.CommissionColor; import ditda.backend.domain.commission.core.entity.CommissionConcept; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; +import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; import ditda.backend.domain.commission.core.repository.CommissionColorRepository; import ditda.backend.domain.commission.core.repository.CommissionConceptRepository; @@ -87,6 +88,11 @@ public CommissionCreateResponse createCommission( return CommissionCreateResponse.from(commission); } + // status인 commission 조회 + public List getCommissionByInstructorAndStatus(Long instructorId, CommissionStatus status) { + return commissionRepository.findByInstructorIdAndStatus(instructorId, status); + } + // 컨셉 저장 private void saveConcepts(Commission commission, CommissionCreateRequest.DesignInfo design) { diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java new file mode 100644 index 0000000..6eff14d --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java @@ -0,0 +1,31 @@ +package ditda.backend.domain.commission.dashboard.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import ditda.backend.domain.commission.dashboard.dto.response.DraftSubmissionCommissionResponse; +import ditda.backend.domain.commission.dashboard.service.InstructorDashboardService; +import ditda.backend.global.apipayload.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/v1/instructors/commissions") +@RequiredArgsConstructor +@Tag(name = "Instructor Dashboard", description = "강사 대시보드 API") +public class InstructorDashboardController { + + private final InstructorDashboardService instructorDashboardService; + + @Operation(summary = "시안 제출 현황 조회", description = "**[대시보드]** 시안 제출 현황을 조회합니다.") + @GetMapping("/draft-submissions") + public ApiResponse getDraftSubmissions( + @AuthenticationPrincipal Long instructorId + ) { + DraftSubmissionCommissionResponse response = instructorDashboardService.getDraftSubmissions(instructorId); + return ApiResponse.onSuccess("시안 제출 현황 조회 성공", response); + } +} diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java new file mode 100644 index 0000000..af44bb6 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java @@ -0,0 +1,69 @@ +package ditda.backend.domain.commission.dashboard.dto.response; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CategoryType; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "시안 제출 외주 응답") +public record DraftSubmissionCommissionResponse( + List commissions +) { + public static DraftSubmissionCommissionResponse of( + List commissions, + Map draftSubmissionCount, + LocalDate today + ) { + List items = commissions.stream() + .map(c -> CommissionItem.from(c, draftSubmissionCount.get(c.getId()), today)) + .toList(); + + return new DraftSubmissionCommissionResponse(items); + } + + public record CommissionItem( + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "제목", example = "수학의 정석 - 수학") + String title, + + @Schema(description = "카테고리", example = "FLYER_TEXTBOOK_COVER_INNER") + CategoryType category, + + @Schema(description = "1차 시안 제출 현황") + DraftSubmissionStatus draftSubmission, + + @Schema(description = "시안 확인 가능 여부", example = "true") + boolean isViewable, + + @Schema(description = "1차 시안 마감일", example = "2026-06-23") + LocalDate firstDraftDeadline + ) { + public static CommissionItem from(Commission commission, long submitted, LocalDate today) { + + return new CommissionItem( + commission.getId(), + commission.getTitle(), + commission.getCategoryType(), + new DraftSubmissionStatus((int)submitted, commission.getDesignerCount()), + commission.isDraftListViewable((int)submitted, today), + commission.getFirstDraftDeadline() + ); + } + + @Schema(description = "1차 시안 제출 현황") + public record DraftSubmissionStatus( + + @Schema(description = "제출한 디자이너 수", example = "2") + int submitted, + + @Schema(description = "총 모집 디자이너 수", example = "5") + int total + ) { + } + } +} diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java new file mode 100644 index 0000000..263cb0d --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java @@ -0,0 +1,46 @@ +package ditda.backend.domain.commission.dashboard.service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import ditda.backend.domain.commission.application.service.CommissionApplicationService; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; +import ditda.backend.domain.commission.core.service.InstructorCommissionService; +import ditda.backend.domain.commission.dashboard.dto.response.DraftSubmissionCommissionResponse; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class InstructorDashboardService { + + private final InstructorCommissionService commissionService; + private final CommissionApplicationService commissionApplicationService; + + // 시안 제출 현황 조회 + public DraftSubmissionCommissionResponse getDraftSubmissions(Long instructorId) { + + // 시안 제출 중인 외주 조회 + List commissions = commissionService.getCommissionByInstructorAndStatus( + instructorId, + CommissionStatus.IN_PROGRESS + ); + + // 외주 Id 추출 + List commissionIds = commissions.stream().map(Commission::getId).toList(); + + // 시안을 제출한 디자이너 수 조회 + Map draftSubmissionCount = commissionApplicationService.countApplicationByStatus( + commissionIds, + ApplicationStatus.DRAFT_SUBMITTED + ); + + return DraftSubmissionCommissionResponse.of(commissions, draftSubmissionCount, LocalDate.now()); + } +} From a7aa83a67e9aad192bf81af6195b22c11f08253a Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Thu, 18 Jun 2026 11:24:44 +0900 Subject: [PATCH 079/102] =?UTF-8?q?Fix(commission):=20=EC=8B=9C=EC=95=88?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=201?= =?UTF-8?q?=EC=B0=A8=20=EC=8B=9C=EC=95=88=20=EC=A7=80=EC=9B=90=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=A0=9C=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=9D=B8=EC=9B=90=20+=20=EB=A7=88=EA=B0=90?= =?UTF-8?q?=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commission/draft/facade/DraftFacade.java | 2 +- .../repository/CommissionDraftRepository.java | 6 ++++ .../draft/service/DraftService.java | 28 ++++++++++++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java b/src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java index 781ca2d..65d9ee1 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java +++ b/src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java @@ -41,7 +41,7 @@ public DraftSelectResponse selectDraft(Long instructorId, Long commissionId, Lon Commission commission = commissionService.getOwnedCommission(commissionId, instructorId); - CommissionApplication selected = draftService.getApplicationForSelection(commissionId, draftId); + CommissionApplication selected = draftService.getApplicationForSelection(commission, draftId); Designer designer = selected.getDesigner(); LocalDateTime now = LocalDateTime.now(); diff --git a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java index 6f95f72..4e72143 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java +++ b/src/main/java/ditda/backend/domain/commission/draft/repository/CommissionDraftRepository.java @@ -18,6 +18,12 @@ public interface CommissionDraftRepository extends JpaRepository findFirstRoundDrafts(@Param("commissionId") Long commissionId); + @Query("select count(d) from CommissionDraft d " + + "join d.commissionApplication ca " + + "where ca.commission.id = :commissionId " + + "and d.round = 0") + int countFirstRoundDrafts(@Param("commissionId") Long commissionId); + @Query("select d from CommissionDraft d " + "join d.commissionApplication ca " + "where d.id = :draftId " diff --git a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java index 78a939d..0179268 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java +++ b/src/main/java/ditda/backend/domain/commission/draft/service/DraftService.java @@ -56,12 +56,26 @@ public DraftListResponse getFirstRoundDrafts(Long instructorId, Long commissionI return draftResponseMapper.toDraftListResponse(commission, drafts, thumbnailByDraftId); } - // 시안 상세 조회 + // 시안 상세 조회 (1차 시안 및 수정본 포함) @Transactional(readOnly = true) public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, Long draftId) { // 1. 외주 조회 + 강사 확인 - commissionService.getOwnedCommission(commissionId, instructorId); + Commission commission = commissionService.getOwnedCommission(commissionId, instructorId); + + CommissionDraft draft = commissionDraftRepository.findById(draftId) + .orElseThrow(() -> new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND)); + + // 1차 시안의 경우에만 + if (draft.isDraftFirstRound()) { + // 마감 전에는 요구 인원이 다 차야 조회 가능, 마감 후에는 덜 차도 조회 가능 + int draftCount = commissionDraftRepository.countFirstRoundDrafts(commissionId); + if (!commission.isDraftListViewable(draftCount, LocalDate.now())) { + throw new GeneralException(DraftErrorCode.DRAFTS_NOT_READY); + } + } + + // TODO: 수정본 상세 조회 제약조건 필요 // 2. 시안 조회 if (!commissionDraftRepository.existsDraftInCommission(draftId, commissionId)) { @@ -75,10 +89,16 @@ public DraftDetailResponse getDraftDetail(Long instructorId, Long commissionId, } // 1차 시안 - public CommissionApplication getApplicationForSelection(Long commissionId, Long draftId) { + public CommissionApplication getApplicationForSelection(Commission commission, Long draftId) { + + // 마감 전에는 요구 인원이 다 차야 조회 가능, 마감 후에는 덜 차도 조회 가능 + int draftCount = commissionDraftRepository.countFirstRoundDrafts(commission.getId()); + if (!commission.isDraftListViewable(draftCount, LocalDate.now())) { + throw new GeneralException(DraftErrorCode.DRAFTS_NOT_READY); + } CommissionDraft draft = commissionDraftRepository - .findDraftInCommission(draftId, commissionId) + .findDraftInCommission(draftId, commission.getId()) .orElseThrow(() -> new GeneralException(DraftErrorCode.DRAFT_NOT_FOUND)); if (!draft.isDraftFirstRound()) { From afff17a1ddeed754346e542810f95b4ce5c4c1e2 Mon Sep 17 00:00:00 2001 From: fervovita Date: Thu, 18 Jun 2026 15:18:49 +0900 Subject: [PATCH 080/102] =?UTF-8?q?Feat(dashboard):=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=20=EC=A4=91=EC=9D=B8=20=EC=99=B8=EC=A3=BC=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommissionApplicationRepository.java | 8 +++ .../service/CommissionApplicationService.java | 27 +++++++-- .../commission/core/entity/Commission.java | 10 ++++ .../InstructorDashboardController.java | 12 ++++ .../response/MatchingCommissionResponse.java | 58 +++++++++++++++++++ .../service/InstructorDashboardService.java | 38 ++++++++++++ 6 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java diff --git a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java index b6d77d3..48d801a 100644 --- a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java +++ b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java @@ -21,4 +21,12 @@ public interface CommissionApplicationRepository extends JpaRepository countByCommissionIdsAndStatus( @Param("commissionIds") List commissionIds, @Param("status") ApplicationStatus status); + + @Query("SELECT a.commission.id AS commissionId, COUNT(DISTINCT a.designer.level) AS count " + + "FROM CommissionApplication a " + + "WHERE a.commission.id IN :commissionIds AND a.status = :status " + + "GROUP BY a.commission.id") + List countDistinctLevelByCommissionIdsAndStatus( + @Param("commissionIds") List commissionIds, + @Param("status") ApplicationStatus status); } diff --git a/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java b/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java index 12e2139..3f3f74d 100644 --- a/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java +++ b/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java @@ -24,15 +24,34 @@ public Map countApplicationByStatus(List commissionIds, Applic } // 집계 결과 - Map counts = commissionApplicationRepository - .countByCommissionIdsAndStatus(commissionIds, status).stream() + List counts = + commissionApplicationRepository.countByCommissionIdsAndStatus(commissionIds, status); + + return toCountMap(counts, commissionIds); + } + + // commission별 지원자가 존재하는 디자이너 레벨 종류 수 + public Map countDistinctLevelByStatus(List commissionIds, ApplicationStatus status) { + if (commissionIds.isEmpty()) { + return Map.of(); + } + + // 집계 결과 + List counts = + commissionApplicationRepository.countDistinctLevelByCommissionIdsAndStatus(commissionIds, status); + + return toCountMap(counts, commissionIds); + } + + // 집계 결과를 Map으로 변환 + 0 보정 + private Map toCountMap(List counts, List commissionIds) { + Map countByCommissionId = counts.stream() .collect(Collectors.toMap( ApplicationSubmissionCount::getCommissionId, ApplicationSubmissionCount::getCount )); - // 0 보정 return commissionIds.stream() - .collect(Collectors.toMap(id -> id, id -> counts.getOrDefault(id, 0L))); + .collect(Collectors.toMap(id -> id, id -> countByCommissionId.getOrDefault(id, 0L))); } } diff --git a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java index ed6b262..854fabb 100644 --- a/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java +++ b/src/main/java/ditda/backend/domain/commission/core/entity/Commission.java @@ -10,6 +10,7 @@ import ditda.backend.domain.commission.core.entity.enums.PageSize; import ditda.backend.domain.commission.core.entity.enums.PlanCode; import ditda.backend.domain.designer.entity.Designer; +import ditda.backend.domain.designer.entity.enums.DesignerLevel; import ditda.backend.domain.instructor.entity.Instructor; import ditda.backend.global.entity.BaseEntity; import jakarta.persistence.Column; @@ -156,4 +157,13 @@ public void selectDesigner(Designer designer, LocalDateTime selectedAt) { public int getDesignerCount() { return planCode.getDesignerCount(); } + + // 마감 전 매칭 확정 인원 + // 레벨별 1명 보장 슬롯은 비어있는 레벨만큼 비워두고, 잉여 인원은 선착순(designerCount - 레벨 종류 수)만 채운다 + public int matchedCount(int distinctLevels, int totalApplicants) { + + int firstComeSlots = getDesignerCount() - DesignerLevel.values().length; + int capacity = distinctLevels + firstComeSlots; + return Math.min(capacity, totalApplicants); + } } diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java index 6eff14d..0554b4d 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.RestController; import ditda.backend.domain.commission.dashboard.dto.response.DraftSubmissionCommissionResponse; +import ditda.backend.domain.commission.dashboard.dto.response.MatchingCommissionResponse; import ditda.backend.domain.commission.dashboard.service.InstructorDashboardService; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -26,6 +27,17 @@ public ApiResponse getDraftSubmissions( @AuthenticationPrincipal Long instructorId ) { DraftSubmissionCommissionResponse response = instructorDashboardService.getDraftSubmissions(instructorId); + return ApiResponse.onSuccess("시안 제출 현황 조회 성공", response); } + + @Operation(summary = "매칭 중인 외주 조회", description = "**[대시보드]** 매칭 중인 외주를 조회합니다.") + @GetMapping("/matchings") + public ApiResponse getMatchingCommissions( + @AuthenticationPrincipal Long instructorId + ) { + MatchingCommissionResponse response = instructorDashboardService.getMatchingCommissions(instructorId); + + return ApiResponse.onSuccess("매칭 중인 외주 조회 성공", response); + } } diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java new file mode 100644 index 0000000..e55e487 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java @@ -0,0 +1,58 @@ +package ditda.backend.domain.commission.dashboard.dto.response; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import ditda.backend.domain.commission.core.entity.Commission; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "매칭 중인 외주 응답") +public record MatchingCommissionResponse( + + List commissions +) { + public static MatchingCommissionResponse of(List commissions, Map matchedCount) { + List items = commissions.stream() + .map(c -> CommissionItem.from(c, matchedCount.get(c.getId()))) + .toList(); + + return new MatchingCommissionResponse(items); + } + + public record CommissionItem( + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "제목", example = "수학의 정석 - 수학") + String title, + + @Schema(description = "매칭 현황") + MatchStatusResponse matching, + + @Schema(description = "최종 마감일", example = "2026-06-23") + LocalDate finalDeadline + ) { + private static CommissionItem from(Commission commission, long matched) { + + return new CommissionItem( + commission.getId(), + commission.getTitle(), + new MatchStatusResponse((int)matched, commission.getDesignerCount()), + commission.getFinalDeadline() + ); + } + + @Schema(description = "매칭 현황") + public record MatchStatusResponse( + + @Schema(description = "매칭된 디자이너 수", example = "2") + int matched, + + @Schema(description = "총 모집 디자이너 수", example = "5") + int total + ) { + } + } +} + diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java index 263cb0d..6551e2f 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,6 +14,7 @@ import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.commission.core.service.InstructorCommissionService; import ditda.backend.domain.commission.dashboard.dto.response.DraftSubmissionCommissionResponse; +import ditda.backend.domain.commission.dashboard.dto.response.MatchingCommissionResponse; import lombok.RequiredArgsConstructor; @Service @@ -43,4 +45,40 @@ public DraftSubmissionCommissionResponse getDraftSubmissions(Long instructorId) return DraftSubmissionCommissionResponse.of(commissions, draftSubmissionCount, LocalDate.now()); } + + // 매칭 중인 외주 조회 + public MatchingCommissionResponse getMatchingCommissions(Long instructorId) { + + // 모집 중인 외주 조회 + List commissions = commissionService.getCommissionByInstructorAndStatus( + instructorId, + CommissionStatus.RECRUITING + ); + + // 외주 Id 추출 + List commissionIds = commissions.stream().map(Commission::getId).toList(); + + // 외주별 지원자가 존재하는 레벨 종류 수 + Map distinctLevelCount = commissionApplicationService.countDistinctLevelByStatus( + commissionIds, + ApplicationStatus.PENDING + ); + + // 외주별 전체 지원자 수 + Map applicantCount = commissionApplicationService.countApplicationByStatus( + commissionIds, + ApplicationStatus.PENDING + ); + + // 외주별 매칭 확정 인원 계산 + Map matchedCount = commissions.stream() + .collect(Collectors.toMap( + Commission::getId, + c -> (long)c.matchedCount( + distinctLevelCount.get(c.getId()).intValue(), + applicantCount.get(c.getId()).intValue()) + )); + + return MatchingCommissionResponse.of(commissions, matchedCount); + } } From 02cc644aac86597a8bdc45df54384b390528f6a4 Mon Sep 17 00:00:00 2001 From: fervovita Date: Thu, 18 Jun 2026 15:20:04 +0900 Subject: [PATCH 081/102] =?UTF-8?q?Test(dashboard):=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=20=EC=A4=91=EC=9D=B8=20=EC=99=B8=EC=A3=BC=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/entity/CommissionTest.java | 34 ++++++ .../InstructorDashboardServiceTest.java | 104 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java create mode 100644 src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java diff --git a/src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java b/src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java new file mode 100644 index 0000000..0c3de5a --- /dev/null +++ b/src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java @@ -0,0 +1,34 @@ +package ditda.backend.domain.commission.core.entity; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import ditda.backend.domain.commission.core.entity.enums.PlanCode; + +class CommissionTest { + + @DisplayName("플랜·레벨 분포별 매칭 확정 인원을 계산") + @ParameterizedTest(name = "[{0}] 레벨종류 {1}, 지원자 {2} → matched {3}") + @CsvSource({ + // plan, distinctLevels, totalApplicants, expected + "BASIC, 2, 4, 2", // L1:3/L2:1/L3:0 + "BASIC, 0, 0, 0", // 지원자 없음 + "PLUS, 2, 4, 3", // L1:3/L2:1/L3:0 + "PLUS, 3, 3, 3", // 각 레벨 1명씩 + "MAX, 3, 4, 4", // L1:2/L2:1/L3:1 + "MAX, 2, 4, 4", // L1:3/L2:1/L3:0 + "MAX, 1, 5, 3", // L1만 5명 + "MAX, 3, 5, 5" // 전 레벨 + 정원 + }) + void matchedCount_boundaryValues(PlanCode plan, int distinctLevels, int totalApplicants, int expected) { + + Commission commission = Commission.builder() + .planCode(plan) + .build(); + + assertThat(commission.matchedCount(distinctLevels, totalApplicants)).isEqualTo(expected); + } +} diff --git a/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java new file mode 100644 index 0000000..bdbecc7 --- /dev/null +++ b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java @@ -0,0 +1,104 @@ +package ditda.backend.domain.commission.dashboard.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import ditda.backend.domain.commission.application.service.CommissionApplicationService; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; +import ditda.backend.domain.commission.core.entity.enums.PlanCode; +import ditda.backend.domain.commission.core.service.InstructorCommissionService; +import ditda.backend.domain.commission.dashboard.dto.response.MatchingCommissionResponse; + +@ExtendWith(MockitoExtension.class) +class InstructorDashboardServiceTest { + + private static final Long INSTRUCTOR_ID = 100L; + + @Mock + private InstructorCommissionService commissionService; + + @Mock + private CommissionApplicationService commissionApplicationService; + + @InjectMocks + private InstructorDashboardService instructorDashboardService; + + @Test + @DisplayName("매칭 중인 외주별로 레벨 종류 수와 전체 지원자 수를 결합해 matched를 계산 - 성공") + void getMatchingCommissions_matchedCal() { + + // given + LocalDate deadline = LocalDate.of(2026, 6, 23); + Commission max = Commission.builder() + .id(1L) + .title("MAX 외주") + .planCode(PlanCode.MAX) + .finalDeadline(deadline) + .build(); + Commission basic = Commission.builder() + .id(2L) + .title("BASIC 외주") + .planCode(PlanCode.BASIC) + .finalDeadline(deadline) + .build(); + + List commissionIds = List.of(1L, 2L); + + given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING)) + .willReturn(List.of(max, basic)); + given(commissionApplicationService.countDistinctLevelByStatus(commissionIds, ApplicationStatus.PENDING)) + .willReturn(Map.of(1L, 3L, 2L, 2L)); // max -> 레벨 3종류 / basic -> 레벨 2종류 + given(commissionApplicationService.countApplicationByStatus(commissionIds, ApplicationStatus.PENDING)) + .willReturn(Map.of(1L, 4L, 2L, 4L)); // max -> 지원자 4명 / basic -> 지원자 4명 + + // when + MatchingCommissionResponse response = instructorDashboardService.getMatchingCommissions(INSTRUCTOR_ID); + + // then + assertThat(response.commissions()).hasSize(2); + + MatchingCommissionResponse.CommissionItem maxItem = response.commissions().getFirst(); + assertThat(maxItem.commissionId()).isEqualTo(1L); + assertThat(maxItem.title()).isEqualTo("MAX 외주"); + assertThat(maxItem.finalDeadline()).isEqualTo(deadline); + assertThat(maxItem.matching().matched()).isEqualTo(4); + assertThat(maxItem.matching().total()).isEqualTo(5); + + MatchingCommissionResponse.CommissionItem basicItem = response.commissions().get(1); + assertThat(basicItem.commissionId()).isEqualTo(2L); + assertThat(basicItem.matching().matched()).isEqualTo(2); + assertThat(basicItem.matching().total()).isEqualTo(3); + } + + @Test + @DisplayName("매칭 중인 외주가 없으면 빈 목록을 반환 - 성공") + void getMatchingCommissions_emptyList() { + + // given + given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING)) + .willReturn(List.of()); + given(commissionApplicationService.countDistinctLevelByStatus(List.of(), ApplicationStatus.PENDING)) + .willReturn(Map.of()); + given(commissionApplicationService.countApplicationByStatus(List.of(), ApplicationStatus.PENDING)) + .willReturn(Map.of()); + + // when + MatchingCommissionResponse response = instructorDashboardService.getMatchingCommissions(INSTRUCTOR_ID); + + // then + assertThat(response.commissions()).isEmpty(); + } +} From 433a8c461fc9ff5fafa6898ab06c35d9cbd18618 Mon Sep 17 00:00:00 2001 From: fervovita Date: Thu, 18 Jun 2026 15:43:59 +0900 Subject: [PATCH 082/102] =?UTF-8?q?Style:=20=ED=9B=84=ED=96=89=20=EA=B3=B5?= =?UTF-8?q?=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/commission/core/entity/CommissionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java b/src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java index 0c3de5a..e3e6c6b 100644 --- a/src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java +++ b/src/test/java/ditda/backend/domain/commission/core/entity/CommissionTest.java @@ -18,7 +18,7 @@ class CommissionTest { "BASIC, 0, 0, 0", // 지원자 없음 "PLUS, 2, 4, 3", // L1:3/L2:1/L3:0 "PLUS, 3, 3, 3", // 각 레벨 1명씩 - "MAX, 3, 4, 4", // L1:2/L2:1/L3:1 + "MAX, 3, 4, 4", // L1:2/L2:1/L3:1 "MAX, 2, 4, 4", // L1:3/L2:1/L3:0 "MAX, 1, 5, 3", // L1만 5명 "MAX, 3, 5, 5" // 전 레벨 + 정원 From dfe44c0bb3ffc94818a126dd06eb1bd9a9845348 Mon Sep 17 00:00:00 2001 From: fervovita Date: Thu, 18 Jun 2026 23:36:48 +0900 Subject: [PATCH 083/102] =?UTF-8?q?Feat(dashboard):=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=A4=91=EC=9D=B8=20=EC=99=B8=EC=A3=BC=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InstructorDashboardController.java | 11 ++++ .../response/RevisingCommissionResponse.java | 54 +++++++++++++++++++ .../service/InstructorDashboardService.java | 22 ++++++++ .../revision/dto/RevisionStatus.java | 7 +++ .../revision/entity/RevisionResponse.java | 4 ++ .../repository/RevisionRequestRepository.java | 16 ++++++ .../projection/RevisionStatusView.java | 10 ++++ .../revision/service/RevisionService.java | 43 +++++++++++++++ 8 files changed, 167 insertions(+) create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java create mode 100644 src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java create mode 100644 src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java create mode 100644 src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java index 0554b4d..5e46437 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java @@ -7,6 +7,7 @@ import ditda.backend.domain.commission.dashboard.dto.response.DraftSubmissionCommissionResponse; import ditda.backend.domain.commission.dashboard.dto.response.MatchingCommissionResponse; +import ditda.backend.domain.commission.dashboard.dto.response.RevisingCommissionResponse; import ditda.backend.domain.commission.dashboard.service.InstructorDashboardService; import ditda.backend.global.apipayload.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -40,4 +41,14 @@ public ApiResponse getMatchingCommissions( return ApiResponse.onSuccess("매칭 중인 외주 조회 성공", response); } + + @Operation(summary = "수정 중인 외주 조회", description = "**[대시보드]** 수정 중인 외주를 조회합니다.") + @GetMapping("/revisions") + public ApiResponse getRevisingCommissions( + @AuthenticationPrincipal Long instructorId + ) { + RevisingCommissionResponse response = instructorDashboardService.getRevisingCommissions(instructorId); + + return ApiResponse.onSuccess("수정 중인 외주 조회 성공", response); + } } diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java new file mode 100644 index 0000000..6f54eae --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java @@ -0,0 +1,54 @@ +package ditda.backend.domain.commission.dashboard.dto.response; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.revision.dto.RevisionStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "수정 중인 외주 응답") +public record RevisingCommissionResponse( + + List commissions +) { + public static RevisingCommissionResponse of(List commissions, Map statuses) { + List items = commissions.stream() + .map(c -> CommissionItem.from(c, statuses.get(c.getId()))) + .toList(); + + return new RevisingCommissionResponse(items); + } + + public record CommissionItem( + @Schema(description = "외주 ID", example = "1") + Long commissionId, + + @Schema(description = "제목", example = "수학의 정석 - 수학") + String title, + + @Schema(description = "수정 요청 전송 여부 (true=전송완료, false=확인하기)", example = "true") + boolean isSubmitted, + + @Schema(description = "수정사항 제출 여부", example = "true") + boolean hasUpdated, + + @Schema(description = "최종 마감일", example = "2026-06-23") + LocalDate finalDeadline + ) { + + private static CommissionItem from(Commission commission, RevisionStatus status) { + + return new CommissionItem( + commission.getId(), + commission.getTitle(), + status.submitted(), + status.hasUpdated(), + commission.getFinalDeadline() + ); + } + } +} + + diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java index 6551e2f..c587950 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java @@ -15,6 +15,9 @@ import ditda.backend.domain.commission.core.service.InstructorCommissionService; import ditda.backend.domain.commission.dashboard.dto.response.DraftSubmissionCommissionResponse; import ditda.backend.domain.commission.dashboard.dto.response.MatchingCommissionResponse; +import ditda.backend.domain.commission.dashboard.dto.response.RevisingCommissionResponse; +import ditda.backend.domain.commission.revision.dto.RevisionStatus; +import ditda.backend.domain.commission.revision.service.RevisionService; import lombok.RequiredArgsConstructor; @Service @@ -24,6 +27,7 @@ public class InstructorDashboardService { private final InstructorCommissionService commissionService; private final CommissionApplicationService commissionApplicationService; + private final RevisionService revisionService; // 시안 제출 현황 조회 public DraftSubmissionCommissionResponse getDraftSubmissions(Long instructorId) { @@ -81,4 +85,22 @@ public MatchingCommissionResponse getMatchingCommissions(Long instructorId) { return MatchingCommissionResponse.of(commissions, matchedCount); } + + // 수정 중인 외주 조회 + public RevisingCommissionResponse getRevisingCommissions(Long instructorId) { + + // 수정 중인 외주 조회 + List commissions = commissionService.getCommissionByInstructorAndStatus( + instructorId, + CommissionStatus.EDITING + ); + + // 외주 Id 추출 + List commissionIds = commissions.stream().map(Commission::getId).toList(); + + // 수정 관련 데이터 조회 (submitted/hasUpdated) + Map statuses = revisionService.getRevisionStatuses(commissionIds); + + return RevisingCommissionResponse.of(commissions, statuses); + } } diff --git a/src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java b/src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java new file mode 100644 index 0000000..4e34532 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java @@ -0,0 +1,7 @@ +package ditda.backend.domain.commission.revision.dto; + +public record RevisionStatus( + boolean submitted, + boolean hasUpdated +) { +} diff --git a/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionResponse.java b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionResponse.java index f7ac173..2c1986d 100644 --- a/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/revision/entity/RevisionResponse.java @@ -40,6 +40,10 @@ public class RevisionResponse extends BaseEntity { @Column(name = "designer_comment", length = 500, nullable = false) private String designerComment; + @Builder.Default + @Column(name = "checked", nullable = false) + private boolean checked = false; + public static RevisionResponse create( RevisionRequest revisionRequest, CommissionDraft commissionDraft, diff --git a/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java index d011fba..6d012ce 100644 --- a/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java +++ b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java @@ -1,8 +1,24 @@ package ditda.backend.domain.commission.revision.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.revision.entity.RevisionRequest; +import ditda.backend.domain.commission.revision.repository.projection.RevisionStatusView; public interface RevisionRequestRepository extends JpaRepository { + + // 수정 요청만 있고 응답이 없으면 submitted=false, 미열람 응답이 하나라도 있으면 hasUpdated=true + @Query("SELECT rr.commission.id AS commissionId," + + "CASE WHEN COUNT(CASE WHEN resp.id IS NULL THEN 1 END) > 0 THEN true ELSE false END AS submitted, " + + "CASE WHEN COUNT(CASE WHEN resp.id IS NOT NULL AND resp.checked = false THEN 1 END) > 0 " + + "THEN true ELSE false END AS hasUpdated " + + "FROM RevisionRequest rr " + + "LEFT JOIN RevisionResponse resp ON resp.revisionRequest = rr " + + "WHERE rr.commission.id IN :commissionIds " + + "GROUP BY rr.commission.id") + List findRevisionStatuses(@Param("commissionIds") List commissionIds); } diff --git a/src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java b/src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java new file mode 100644 index 0000000..abeb2cd --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java @@ -0,0 +1,10 @@ +package ditda.backend.domain.commission.revision.repository.projection; + +public interface RevisionStatusView { + + Long getCommissionId(); + + Boolean getSubmitted(); + + Boolean getHasUpdated(); +} diff --git a/src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java b/src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java new file mode 100644 index 0000000..898d72b --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java @@ -0,0 +1,43 @@ +package ditda.backend.domain.commission.revision.service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import ditda.backend.domain.commission.revision.dto.RevisionStatus; +import ditda.backend.domain.commission.revision.repository.RevisionRequestRepository; +import ditda.backend.domain.commission.revision.repository.projection.RevisionStatusView; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RevisionService { + + private final RevisionRequestRepository revisionRequestRepository; + + // 외주별 수정 상태(submitted/hasUpdated) + public Map getRevisionStatuses(List commissionIds) { + if (commissionIds.isEmpty()) { + return Map.of(); + } + + Map statuses = revisionRequestRepository.findRevisionStatuses(commissionIds).stream() + .collect(Collectors.toMap( + RevisionStatusView::getCommissionId, + this::toStatus + )); + + // 수정 요청이 없는 외주는 (false, false) + commissionIds.forEach(id -> statuses.putIfAbsent(id, new RevisionStatus(false, false))); + + return statuses; + } + + private RevisionStatus toStatus(RevisionStatusView view) { + return new RevisionStatus(Boolean.TRUE.equals(view.getSubmitted()), Boolean.TRUE.equals(view.getHasUpdated())); + } +} From 181ccc37cf2c1e9403361d8c83198d3233d8d95f Mon Sep 17 00:00:00 2001 From: fervovita Date: Thu, 18 Jun 2026 23:38:02 +0900 Subject: [PATCH 084/102] =?UTF-8?q?Refactor(dashboard):=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EC=A0=9C=EC=B6=9C=20=ED=98=84=ED=99=A9=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20DTO=EC=9D=98=20from=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20private=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/DraftSubmissionCommissionResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java index af44bb6..df05a66 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java @@ -43,7 +43,7 @@ public record CommissionItem( @Schema(description = "1차 시안 마감일", example = "2026-06-23") LocalDate firstDraftDeadline ) { - public static CommissionItem from(Commission commission, long submitted, LocalDate today) { + private static CommissionItem from(Commission commission, long submitted, LocalDate today) { return new CommissionItem( commission.getId(), From b3fa8c182989922db2f1183b9a3b88bfaea516c9 Mon Sep 17 00:00:00 2001 From: fervovita Date: Thu, 18 Jun 2026 23:46:34 +0900 Subject: [PATCH 085/102] =?UTF-8?q?Refactor(dashboard):=20isViewable=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=9D=84=20DTO=EC=97=90=EC=84=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EA=B3=84=EC=B8=B5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/DraftSubmissionCommissionResponse.java | 12 ++++++++---- .../service/InstructorDashboardService.java | 10 +++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java index df05a66..c04f902 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java @@ -15,10 +15,14 @@ public record DraftSubmissionCommissionResponse( public static DraftSubmissionCommissionResponse of( List commissions, Map draftSubmissionCount, - LocalDate today + Map viewable ) { List items = commissions.stream() - .map(c -> CommissionItem.from(c, draftSubmissionCount.get(c.getId()), today)) + .map(c -> CommissionItem.from( + c, + draftSubmissionCount.get(c.getId()), + viewable.get(c.getId()) + )) .toList(); return new DraftSubmissionCommissionResponse(items); @@ -43,14 +47,14 @@ public record CommissionItem( @Schema(description = "1차 시안 마감일", example = "2026-06-23") LocalDate firstDraftDeadline ) { - private static CommissionItem from(Commission commission, long submitted, LocalDate today) { + private static CommissionItem from(Commission commission, long submitted, boolean isViewable) { return new CommissionItem( commission.getId(), commission.getTitle(), commission.getCategoryType(), new DraftSubmissionStatus((int)submitted, commission.getDesignerCount()), - commission.isDraftListViewable((int)submitted, today), + isViewable, commission.getFirstDraftDeadline() ); } diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java index c587950..c892862 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java @@ -47,7 +47,15 @@ public DraftSubmissionCommissionResponse getDraftSubmissions(Long instructorId) ApplicationStatus.DRAFT_SUBMITTED ); - return DraftSubmissionCommissionResponse.of(commissions, draftSubmissionCount, LocalDate.now()); + // 외주별 시안 확인 가능 여부 + LocalDate today = LocalDate.now(); + Map viewable = commissions.stream() + .collect(Collectors.toMap( + Commission::getId, + c -> c.isDraftListViewable(draftSubmissionCount.get(c.getId()).intValue(), today) + )); + + return DraftSubmissionCommissionResponse.of(commissions, draftSubmissionCount, viewable); } // 매칭 중인 외주 조회 From 29c2812154b6ec434f02ff1f6c514aaf550912fc Mon Sep 17 00:00:00 2001 From: fervovita Date: Fri, 19 Jun 2026 00:09:04 +0900 Subject: [PATCH 086/102] =?UTF-8?q?Fix(dashboard):=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=A4=91=EC=9D=B8=20=EC=99=B8=EC=A3=BC=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=AA=85=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/dto/response/MatchingCommissionResponse.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java index e55e487..2757f97 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java @@ -30,8 +30,8 @@ public record CommissionItem( @Schema(description = "매칭 현황") MatchStatusResponse matching, - @Schema(description = "최종 마감일", example = "2026-06-23") - LocalDate finalDeadline + @Schema(description = "디자이너 모집 마감일", example = "2026-06-23") + LocalDate applicationsDeadline ) { private static CommissionItem from(Commission commission, long matched) { @@ -39,7 +39,7 @@ private static CommissionItem from(Commission commission, long matched) { commission.getId(), commission.getTitle(), new MatchStatusResponse((int)matched, commission.getDesignerCount()), - commission.getFinalDeadline() + commission.getApplicationDeadline() ); } From 80cd326fee89d4ed69838b0c5b8668579b5a7519 Mon Sep 17 00:00:00 2001 From: fervovita Date: Fri, 19 Jun 2026 00:13:03 +0900 Subject: [PATCH 087/102] =?UTF-8?q?Feat(dashboard):=20=EA=B0=95=EC=82=AC?= =?UTF-8?q?=20=EC=99=B8=EC=A3=BC=20=EC=A1=B0=ED=9A=8C=EC=97=90=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/repository/CommissionRepository.java | 3 ++- .../core/service/InstructorCommissionService.java | 5 +++-- .../dashboard/service/InstructorDashboardService.java | 10 +++++++--- .../service/InstructorDashboardServiceTest.java | 6 +++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java index fc19ec1..527a0ba 100644 --- a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.List; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -14,7 +15,7 @@ public interface CommissionRepository extends JpaRepository { - List findByInstructorIdAndStatus(Long instructorId, CommissionStatus status); + List findByInstructorIdAndStatus(Long instructorId, CommissionStatus status, Sort sort); @Modifying @Query("update Commission c " diff --git a/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java index eadee21..7e513c2 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.List; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -93,8 +94,8 @@ public CommissionCreateResponse createCommission( } // status인 commission 조회 - public List getCommissionByInstructorAndStatus(Long instructorId, CommissionStatus status) { - return commissionRepository.findByInstructorIdAndStatus(instructorId, status); + public List getCommissionByInstructorAndStatus(Long instructorId, CommissionStatus status, Sort sort) { + return commissionRepository.findByInstructorIdAndStatus(instructorId, status, sort); } // 외주 조회 + 강사 확인 diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java index c892862..4965185 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.stream.Collectors; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,7 +36,8 @@ public DraftSubmissionCommissionResponse getDraftSubmissions(Long instructorId) // 시안 제출 중인 외주 조회 List commissions = commissionService.getCommissionByInstructorAndStatus( instructorId, - CommissionStatus.IN_PROGRESS + CommissionStatus.IN_PROGRESS, + Sort.by("firstDraftDeadline").ascending() ); // 외주 Id 추출 @@ -64,7 +66,8 @@ public MatchingCommissionResponse getMatchingCommissions(Long instructorId) { // 모집 중인 외주 조회 List commissions = commissionService.getCommissionByInstructorAndStatus( instructorId, - CommissionStatus.RECRUITING + CommissionStatus.RECRUITING, + Sort.by("applicationDeadline").ascending() ); // 외주 Id 추출 @@ -100,7 +103,8 @@ public RevisingCommissionResponse getRevisingCommissions(Long instructorId) { // 수정 중인 외주 조회 List commissions = commissionService.getCommissionByInstructorAndStatus( instructorId, - CommissionStatus.EDITING + CommissionStatus.EDITING, + Sort.by("finalDeadline").ascending() ); // 외주 Id 추출 diff --git a/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java index bdbecc7..43ed7be 100644 --- a/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java +++ b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java @@ -57,7 +57,7 @@ void getMatchingCommissions_matchedCal() { List commissionIds = List.of(1L, 2L); - given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING)) + given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, null)) .willReturn(List.of(max, basic)); given(commissionApplicationService.countDistinctLevelByStatus(commissionIds, ApplicationStatus.PENDING)) .willReturn(Map.of(1L, 3L, 2L, 2L)); // max -> 레벨 3종류 / basic -> 레벨 2종류 @@ -73,7 +73,7 @@ void getMatchingCommissions_matchedCal() { MatchingCommissionResponse.CommissionItem maxItem = response.commissions().getFirst(); assertThat(maxItem.commissionId()).isEqualTo(1L); assertThat(maxItem.title()).isEqualTo("MAX 외주"); - assertThat(maxItem.finalDeadline()).isEqualTo(deadline); + assertThat(maxItem.applicationsDeadline()).isEqualTo(deadline); assertThat(maxItem.matching().matched()).isEqualTo(4); assertThat(maxItem.matching().total()).isEqualTo(5); @@ -88,7 +88,7 @@ void getMatchingCommissions_matchedCal() { void getMatchingCommissions_emptyList() { // given - given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING)) + given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, null)) .willReturn(List.of()); given(commissionApplicationService.countDistinctLevelByStatus(List.of(), ApplicationStatus.PENDING)) .willReturn(Map.of()); From e18f87f5fcb64b48e0fb20dfd02a306a3bad2b3b Mon Sep 17 00:00:00 2001 From: fervovita Date: Fri, 19 Jun 2026 01:06:53 +0900 Subject: [PATCH 088/102] =?UTF-8?q?Fix:=20=EA=B0=95=EC=82=AC=20=EC=99=B8?= =?UTF-8?q?=EC=A3=BC=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EC=A0=95=EB=A0=AC=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/InstructorDashboardServiceTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java index 43ed7be..8407d47 100644 --- a/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java +++ b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java @@ -13,6 +13,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; import ditda.backend.domain.commission.application.service.CommissionApplicationService; @@ -46,18 +47,21 @@ void getMatchingCommissions_matchedCal() { .id(1L) .title("MAX 외주") .planCode(PlanCode.MAX) + .applicationDeadline(deadline) .finalDeadline(deadline) .build(); Commission basic = Commission.builder() .id(2L) .title("BASIC 외주") .planCode(PlanCode.BASIC) + .applicationDeadline(deadline) .finalDeadline(deadline) .build(); List commissionIds = List.of(1L, 2L); - given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, null)) + given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, + Sort.by("applicationDeadline").ascending())) .willReturn(List.of(max, basic)); given(commissionApplicationService.countDistinctLevelByStatus(commissionIds, ApplicationStatus.PENDING)) .willReturn(Map.of(1L, 3L, 2L, 2L)); // max -> 레벨 3종류 / basic -> 레벨 2종류 @@ -88,7 +92,8 @@ void getMatchingCommissions_matchedCal() { void getMatchingCommissions_emptyList() { // given - given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, null)) + given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, + Sort.by("applicationDeadline").ascending())) .willReturn(List.of()); given(commissionApplicationService.countDistinctLevelByStatus(List.of(), ApplicationStatus.PENDING)) .willReturn(Map.of()); From 8b3df675ebbdadc362402dec8ad33edd57c6c02d Mon Sep 17 00:00:00 2001 From: fervovita Date: Fri, 19 Jun 2026 01:38:26 +0900 Subject: [PATCH 089/102] =?UTF-8?q?Fix(application):=20=EC=99=B8=EC=A3=BC?= =?UTF-8?q?=20=EC=A7=91=EA=B3=84=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20?= =?UTF-8?q?distinct=20=EB=B0=A9=EC=96=B4=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/CommissionApplicationService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java b/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java index 3f3f74d..838207e 100644 --- a/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java +++ b/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java @@ -52,6 +52,7 @@ private Map toCountMap(List counts, List )); return commissionIds.stream() + .distinct() .collect(Collectors.toMap(id -> id, id -> countByCommissionId.getOrDefault(id, 0L))); } } From 5f23b77eb27322f2feda48a3f340c0a0c16fce78 Mon Sep 17 00:00:00 2001 From: fervovita Date: Fri, 19 Jun 2026 01:52:50 +0900 Subject: [PATCH 090/102] =?UTF-8?q?Chore(dashboard):=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../revision/repository/RevisionRequestRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java index 6d012ce..66934ac 100644 --- a/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java +++ b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java @@ -11,7 +11,8 @@ public interface RevisionRequestRepository extends JpaRepository { - // 수정 요청만 있고 응답이 없으면 submitted=false, 미열람 응답이 하나라도 있으면 hasUpdated=true + // 답변이 없는 수정 요청이 하나라도 있으면 submitted=true (전송 완료) + // 미열람 답변이 하나라도 있으면 hasUpdated=true @Query("SELECT rr.commission.id AS commissionId," + "CASE WHEN COUNT(CASE WHEN resp.id IS NULL THEN 1 END) > 0 THEN true ELSE false END AS submitted, " + "CASE WHEN COUNT(CASE WHEN resp.id IS NOT NULL AND resp.checked = false THEN 1 END) > 0 " From fb106ddb4e1d8981340f3f634dd147ab49a9fd30 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Fri, 19 Jun 2026 22:29:24 +0900 Subject: [PATCH 091/102] =?UTF-8?q?Refactor:=20Commission=20presigned=20UR?= =?UTF-8?q?L=20API=EB=A5=BC=20=EA=B3=B5=EC=9A=A9=20API=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controller/CommissionController.java | 17 -------- .../request/CommissionFilePresignRequest.java | 19 --------- .../CommissionFilePresignResponse.java | 14 ------- .../core/facade/CommissionFacade.java | 13 ------- .../service/CommissionCreateFileService.java | 27 +++++-------- .../backend/global/s3/S3FileService.java | 31 +++++++++++++++ .../s3/controller/S3FileController.java | 39 +++++++++++++++++++ .../global/s3/dto/request/PresignRequest.java | 19 +++++++++ .../s3/dto/response/PresignResponse.java | 19 +++++++++ .../backend/global/s3/enums/UploadTarget.java | 19 +++++++++ 10 files changed, 136 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java delete mode 100644 src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java create mode 100644 src/main/java/ditda/backend/global/s3/S3FileService.java create mode 100644 src/main/java/ditda/backend/global/s3/controller/S3FileController.java create mode 100644 src/main/java/ditda/backend/global/s3/dto/request/PresignRequest.java create mode 100644 src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java create mode 100644 src/main/java/ditda/backend/global/s3/enums/UploadTarget.java diff --git a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java index d22e94b..759ce50 100644 --- a/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java +++ b/src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java @@ -9,9 +9,7 @@ import org.springframework.web.bind.annotation.RestController; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; -import ditda.backend.domain.commission.core.dto.request.CommissionFilePresignRequest; import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; -import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; import ditda.backend.domain.commission.core.facade.CommissionFacade; import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; @@ -38,21 +36,6 @@ public ApiResponse getPlans() { return ApiResponse.onSuccess("플랜 조회 성공", response); } - @Operation( - summary = "새 외주 작성 파일 업로드 URL 발급", - description = "**[새 외주 작성]** 새 외주 작성시 자료 첨부 및 레퍼런스 파일을 S3에 올릴 presigned PUT URL을 발급합니다. " - + "PUT URL로 S3에 직접 업로드한 뒤, 응답의 key를 외주 생성 요청의 keys에 담아 전송합니다." - ) - @PostMapping("/files/presigned-url") - public ApiResponse issueFilePresignedUrls( - @Valid @RequestBody CommissionFilePresignRequest request - ) { - - CommissionFilePresignResponse response = commissionFacade.issueFilePresignedUrls(request); - - return ApiResponse.onSuccess("새 외주 작성 파일 업로드 URL 발급 성공", response); - } - @Operation(summary = "새 외주 생성", description = "**[새 외주 작성]** 새로운 외주를 생성합니다.") @PostMapping public ApiResponse createCommission( diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java b/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java deleted file mode 100644 index a7cdf57..0000000 --- a/src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package ditda.backend.domain.commission.core.dto.request; - -import ditda.backend.domain.commission.core.entity.enums.FileKind; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "외주 첨부파일 presigned URL 발급 요청") -public record CommissionFilePresignRequest( - - @Schema(description = "파일 종류", example = "MATERIAL") - @NotNull(message = "파일 종류는 필수입니다.") - FileKind fileKind, - - @Schema(description = "파일 Content-Type", example = "image/png") - @NotBlank(message = "Content-Type은 필수입니다.") - String contentType -) { -} diff --git a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java b/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java deleted file mode 100644 index 9fc4501..0000000 --- a/src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package ditda.backend.domain.commission.core.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "외주 첨부파일 presigned URL 발급 응답") -public record CommissionFilePresignResponse( - - @Schema(description = "S3 객체 key", example = "commission/tmp/{uuid}.png") - String key, - - @Schema(description = "S3 직접 업로드용 presigned PUT URL") - String presignedUrl -) { -} diff --git a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java index 64b79be..b518260 100644 --- a/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java +++ b/src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java @@ -8,9 +8,7 @@ import ditda.backend.domain.commission.core.dto.CommissionFileToSave; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest; import ditda.backend.domain.commission.core.dto.request.CommissionCreateRequest.FileInfo; -import ditda.backend.domain.commission.core.dto.request.CommissionFilePresignRequest; import ditda.backend.domain.commission.core.dto.response.CommissionCreateResponse; -import ditda.backend.domain.commission.core.dto.response.CommissionFilePresignResponse; import ditda.backend.domain.commission.core.dto.response.PlanListResponse; import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; import ditda.backend.domain.commission.core.service.CommissionCreateFileService; @@ -18,7 +16,6 @@ import ditda.backend.domain.commission.core.validator.CommissionCreateValidator; import ditda.backend.domain.payment.dto.response.DepositNotifyResponse; import ditda.backend.domain.payment.service.PaymentService; -import ditda.backend.global.s3.PresignedUpload; import lombok.RequiredArgsConstructor; @Component @@ -34,16 +31,6 @@ public PlanListResponse getPlans() { return commissionService.getPlans(); } - public CommissionFilePresignResponse issueFilePresignedUrls(CommissionFilePresignRequest request) { - - PresignedUpload presignedUpload = commissionCreateFileService.generatePresignedUpload( - request.fileKind(), - request.contentType() - ); - - return new CommissionFilePresignResponse(presignedUpload.key(), presignedUpload.presignedUrl()); - } - public CommissionCreateResponse createCommission( Long instructorId, CommissionCreateRequest request diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java index 73677b3..9f9645c 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java @@ -1,9 +1,7 @@ package ditda.backend.domain.commission.core.service; -import java.util.EnumSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -16,36 +14,22 @@ import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.repository.CommissionFileRepository; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.PresignedUpload; import ditda.backend.global.s3.S3Properties; import ditda.backend.global.s3.S3UploadManager; import ditda.backend.global.s3.enums.BucketType; -import ditda.backend.global.s3.enums.S3ContentType; +import ditda.backend.global.s3.enums.UploadTarget; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class CommissionCreateFileService { - private static final String DIR = "commission"; private static final BucketType BUCKET = BucketType.PRIVATE; - private static final Set ALLOWED = EnumSet.of(S3ContentType.PNG); private final S3UploadManager s3UploadManager; private final CommissionFileRepository commissionFileRepository; private final S3Properties s3Properties; - public PresignedUpload generatePresignedUpload(FileKind fileKind, String contentType) { - - // 파일 타입 검증 - S3ContentType type = S3ContentType.from(contentType); - if (type == null || !ALLOWED.contains(type)) { - throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); - } - - return s3UploadManager.issueTempUpload(BUCKET, directoryOf(fileKind), type.getExtension(), contentType); - } - // 첨부 파일의 종류별로 keys 합산하여 검증 // 클라이언트가 같은 종류 여러번 보내도 종류별 총량을 기준으로 검사 public void validateFiles(List files) { @@ -87,7 +71,14 @@ public void deleteFiles(List keys) { } private String directoryOf(FileKind fileKind) { - return DIR + "/" + fileKind.name().toLowerCase(); + return targetOf(fileKind).getDir(); + } + + private static UploadTarget targetOf(FileKind fileKind) { + return switch (fileKind) { + case MATERIAL -> UploadTarget.COMMISSION_MATERIAL; + case REFERENCE -> UploadTarget.COMMISSION_REFERENCE; + }; } private void validateKeys(FileKind fileKind, List keys) { diff --git a/src/main/java/ditda/backend/global/s3/S3FileService.java b/src/main/java/ditda/backend/global/s3/S3FileService.java new file mode 100644 index 0000000..11ae092 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/S3FileService.java @@ -0,0 +1,31 @@ +package ditda.backend.global.s3; + +import org.springframework.stereotype.Service; + +import ditda.backend.global.apipayload.code.GeneralErrorCode; +import ditda.backend.global.apipayload.exception.GeneralException; +import ditda.backend.global.s3.enums.S3ContentType; +import ditda.backend.global.s3.enums.UploadTarget; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class S3FileService { + + private final S3UploadManager s3UploadManager; + + public PresignedUpload issuePresignedUpload(UploadTarget target, String contentType) { + + S3ContentType type = S3ContentType.from(contentType); + if (type == null || !target.getAllowed().contains(type)) { + throw new GeneralException(GeneralErrorCode.UNSUPPORTED_CONTENT_TYPE); + } + + return s3UploadManager.issueTempUpload( + target.getBucketType(), + target.getDir(), + type.getExtension(), + contentType + ); + } +} diff --git a/src/main/java/ditda/backend/global/s3/controller/S3FileController.java b/src/main/java/ditda/backend/global/s3/controller/S3FileController.java new file mode 100644 index 0000000..268a514 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/controller/S3FileController.java @@ -0,0 +1,39 @@ +package ditda.backend.global.s3.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import ditda.backend.global.apipayload.response.ApiResponse; +import ditda.backend.global.s3.PresignedUpload; +import ditda.backend.global.s3.S3FileService; +import ditda.backend.global.s3.dto.request.PresignRequest; +import ditda.backend.global.s3.dto.response.PresignResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/v1/files") +@RequiredArgsConstructor +@Tag(name = "File", description = "공용 파일 업로드 API") +public class S3FileController { + + private final S3FileService s3FileService; + + @Operation( + summary = "파일 업로드 URL 발급", + description = "업로드 대상에 맞는 presigned PUT URL을 발급합니다. " + + "PUT URL로 S3에 직접 업로드한 뒤, 응답의 key를 각 도메인 요청에 담아 전송합니다." + ) + @PostMapping("/presigned-url") + public ApiResponse issuePresignedUrl( + @Valid @RequestBody PresignRequest request + ) { + + PresignedUpload upload = s3FileService.issuePresignedUpload(request.target(), request.contentType()); + return ApiResponse.onSuccess("파일 업로드 URL 발급 성공", PresignResponse.from(upload)); + } +} diff --git a/src/main/java/ditda/backend/global/s3/dto/request/PresignRequest.java b/src/main/java/ditda/backend/global/s3/dto/request/PresignRequest.java new file mode 100644 index 0000000..8ed759a --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/dto/request/PresignRequest.java @@ -0,0 +1,19 @@ +package ditda.backend.global.s3.dto.request; + +import ditda.backend.global.s3.enums.UploadTarget; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "파일 업로드 presigned URL 발급 요청") +public record PresignRequest( + + @Schema(description = "업로드 대상", example = "COMMISSION_MATERIAL") + @NotNull(message = "업로드 대상은 필수입니다.") + UploadTarget target, + + @Schema(description = "파일 Content-Type", example = "image/png") + @NotBlank(message = "Content-Type은 필수입니다.") + String contentType +) { +} diff --git a/src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java b/src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java new file mode 100644 index 0000000..ddf0c5d --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java @@ -0,0 +1,19 @@ +package ditda.backend.global.s3.dto.response; + +import ditda.backend.global.s3.PresignedUpload; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "파일 업로드 presigned URL 발급 응답") +public record PresignResponse( + + @Schema(description = "S3 객체 key", example = "commission/tmp/{uuid}.png") + String key, + + @Schema(description = "S3 직접 업로드용 presigned PUT URL") + String presignedUrl +) { + + public static PresignResponse from(PresignedUpload upload) { + return new PresignResponse(upload.key(), upload.presignedUrl()); + } +} diff --git a/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java b/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java new file mode 100644 index 0000000..5b7eae1 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java @@ -0,0 +1,19 @@ +package ditda.backend.global.s3.enums; + +import java.util.EnumSet; +import java.util.Set; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UploadTarget { + + COMMISSION_MATERIAL("commission/material", BucketType.PRIVATE, EnumSet.of(S3ContentType.PNG)), + COMMISSION_REFERENCE("commission/reference", BucketType.PRIVATE, EnumSet.of(S3ContentType.PNG)); + + private final String dir; + private final BucketType bucketType; + private final Set allowed; +} From e19a6127d3a0ba81309583453edd79368cef0f16 Mon Sep 17 00:00:00 2001 From: fervovita Date: Fri, 19 Jun 2026 23:41:00 +0900 Subject: [PATCH 092/102] =?UTF-8?q?Fix(dashboard):=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=99=B8=EC=A3=BC=20=EC=9D=91=EB=8B=B5=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/dto/response/MatchingCommissionResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java index 2757f97..5c4e64a 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java @@ -31,7 +31,7 @@ public record CommissionItem( MatchStatusResponse matching, @Schema(description = "디자이너 모집 마감일", example = "2026-06-23") - LocalDate applicationsDeadline + LocalDate applicationDeadline ) { private static CommissionItem from(Commission commission, long matched) { From 006c59d41087fbc3ed59bc2294b78b3a87341a3a Mon Sep 17 00:00:00 2001 From: fervovita Date: Sat, 20 Jun 2026 00:25:04 +0900 Subject: [PATCH 093/102] =?UTF-8?q?Refactor(dashboard):=20=EA=B0=95?= =?UTF-8?q?=EC=82=AC=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20API=20base?= =?UTF-8?q?=20path=EB=A5=BC=20/dashboards=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/controller/InstructorDashboardController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java index 5e46437..6648bf4 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/controller/InstructorDashboardController.java @@ -15,7 +15,7 @@ import lombok.RequiredArgsConstructor; @RestController -@RequestMapping("/api/v1/instructors/commissions") +@RequestMapping("/api/v1/instructors/dashboards") @RequiredArgsConstructor @Tag(name = "Instructor Dashboard", description = "강사 대시보드 API") public class InstructorDashboardController { From 46666dcf8d6ea5d0384e61d5012ca1d7e1eae223 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sat, 20 Jun 2026 00:32:42 +0900 Subject: [PATCH 094/102] =?UTF-8?q?Refactor:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=82=A4=20=EA=B2=80=EC=A6=9D=EC=9D=84=20S3FileService?= =?UTF-8?q?=EB=A1=9C=20=EA=B3=B5=EC=9A=A9=ED=99=94=ED=95=98=EA=B3=A0=20com?= =?UTF-8?q?mission=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/exception/CommissionErrorCode.java | 18 +++++------ .../service/CommissionCreateFileService.java | 32 +++---------------- .../apipayload/code/GeneralErrorCode.java | 1 + .../backend/global/s3/S3FileService.java | 30 +++++++++++++++++ 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java index c317405..80430f4 100644 --- a/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java +++ b/src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java @@ -12,27 +12,25 @@ public enum CommissionErrorCode implements BaseErrorCode { // 새 외주 작성 파일 업로드 COMMISSION_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "COMMISSION_400_01", "첨부 파일 개수가 허용 범위를 초과했습니다."), - INVALID_COMMISSION_FILE(HttpStatus.BAD_REQUEST, "COMMISSION_400_02", "지원하지 않는 파일 형식입니다."), - COMMISSION_FILE_SIZE_EXCEEDED(HttpStatus.CONTENT_TOO_LARGE, "COMMISSION_413_01", "첨부 파일 크기가 제한을 초과했습니다."), COMMISSION_FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COMMISSION_500_01", "파일 업로드에 실패했습니다."), // 색상 선택 (직접 색상 지정) - COLORS_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_03", "직접 색상 선택 시 색상은 필수입니다."), + COLORS_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_02", "직접 색상 선택 시 색상은 필수입니다."), INVALID_COLOR_COMPOSITION( HttpStatus.BAD_REQUEST, - "COMMISSION_400_04", + "COMMISSION_400_03", "색상 구성이 올바르지 않습니다. MAIN, SUB1, SUB2를 각 1개씩 지정해야 합니다." ), // 카테고리별 - UNSUPPORTED_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "지원하지 않는 카테고리입니다."), - TEXTBOOK_DETAIL_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_06", "교재 정보는 필수입니다."), - INVALID_PAGE_TYPE_FOR_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_07", "해당 카테고리에서 지원하지 않는 페이지 종류입니다."), + UNSUPPORTED_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_04", "지원하지 않는 카테고리입니다."), + TEXTBOOK_DETAIL_REQUIRED(HttpStatus.BAD_REQUEST, "COMMISSION_400_05", "교재 정보는 필수입니다."), + INVALID_PAGE_TYPE_FOR_CATEGORY(HttpStatus.BAD_REQUEST, "COMMISSION_400_06", "해당 카테고리에서 지원하지 않는 페이지 종류입니다."), // 마감 기한 - INVALID_DEADLINE_ORDER(HttpStatus.BAD_REQUEST, "COMMISSION_400_08", "1차 시안 마감일은 최종 마감일보다 빨라야 합니다."), - FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), - FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_10", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), + INVALID_DEADLINE_ORDER(HttpStatus.BAD_REQUEST, "COMMISSION_400_07", "1차 시안 마감일은 최종 마감일보다 빨라야 합니다."), + FIRST_DRAFT_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_08", "1차 시안 마감일은 오늘로부터 최소 10일 이후여야 합니다."), + FINAL_DEADLINE_TOO_SOON(HttpStatus.BAD_REQUEST, "COMMISSION_400_09", "최종 마감일은 1차 시안 마감일로부터 최소 14일 이후여야 합니다."), // 외주 조회 및 선택 COMMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMISSION_404_01", "외주를 찾을 수 없습니다."), diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java index 9f9645c..becb36b 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java @@ -14,7 +14,7 @@ import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.repository.CommissionFileRepository; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.S3Properties; +import ditda.backend.global.s3.S3FileService; import ditda.backend.global.s3.S3UploadManager; import ditda.backend.global.s3.enums.BucketType; import ditda.backend.global.s3.enums.UploadTarget; @@ -26,9 +26,9 @@ public class CommissionCreateFileService { private static final BucketType BUCKET = BucketType.PRIVATE; - private final S3UploadManager s3UploadManager; private final CommissionFileRepository commissionFileRepository; - private final S3Properties s3Properties; + private final S3UploadManager s3UploadManager; + private final S3FileService s3FileService; // 첨부 파일의 종류별로 keys 합산하여 검증 // 클라이언트가 같은 종류 여러번 보내도 종류별 총량을 기준으로 검사 @@ -70,10 +70,6 @@ public void deleteFiles(List keys) { s3UploadManager.deleteAll(BUCKET, keys); } - private String directoryOf(FileKind fileKind) { - return targetOf(fileKind).getDir(); - } - private static UploadTarget targetOf(FileKind fileKind) { return switch (fileKind) { case MATERIAL -> UploadTarget.COMMISSION_MATERIAL; @@ -91,26 +87,6 @@ private void validateKeys(FileKind fileKind, List keys) { ); } - // distinct key 검증 - if (keys.size() != keys.stream().distinct().count()) { - throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); - } - - String dir = directoryOf(fileKind); - for (String key : keys) { - // 파일 key 형식 검증 - if (!s3UploadManager.isTempKey(key, dir)) { - throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); - } - - // 파일 크기 검증 - Long size = s3UploadManager.getObjectSize(BUCKET, key); - if (size == null) { - throw new GeneralException(CommissionErrorCode.INVALID_COMMISSION_FILE); - } - if (size > s3Properties.getMaxFileSize().toBytes()) { - throw new GeneralException(CommissionErrorCode.COMMISSION_FILE_SIZE_EXCEEDED); - } - } + s3FileService.validateUploadedKeys(targetOf(fileKind), keys); } } diff --git a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java index 24d7be6..2ee65dd 100644 --- a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java +++ b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java @@ -20,6 +20,7 @@ public enum GeneralErrorCode implements BaseErrorCode { MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "REQ_400_01", "필수 파라미터가 누락되었습니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "REQ_400_02", "파라미터 형식이 잘못되었습니다."), INVALID_BODY_TYPE(HttpStatus.BAD_REQUEST, "REQ_400_03", "요청 본문의 형식이 잘못되었거나, 허용되지 않은 값이 포함되어 있습니다."), + INVALID_FILE(HttpStatus.BAD_REQUEST, "REQ_400_04", "유효하지 않거나 업로드되지 않은 파일입니다."), UNSUPPORTED_CONTENT_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "REQ_415_01", "지원하지 않는 Content-Type입니다."), DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "REQ_409_01", "이미 존재하는 데이터입니다."), FILE_SIZE_EXCEEDED(HttpStatus.CONTENT_TOO_LARGE, "REQ_413_01", "파일 크기가 제한을 초과했습니다."), diff --git a/src/main/java/ditda/backend/global/s3/S3FileService.java b/src/main/java/ditda/backend/global/s3/S3FileService.java index 11ae092..d7385af 100644 --- a/src/main/java/ditda/backend/global/s3/S3FileService.java +++ b/src/main/java/ditda/backend/global/s3/S3FileService.java @@ -1,5 +1,7 @@ package ditda.backend.global.s3; +import java.util.List; + import org.springframework.stereotype.Service; import ditda.backend.global.apipayload.code.GeneralErrorCode; @@ -13,14 +15,17 @@ public class S3FileService { private final S3UploadManager s3UploadManager; + private final S3Properties s3Properties; public PresignedUpload issuePresignedUpload(UploadTarget target, String contentType) { + // 파일 형식 검증 S3ContentType type = S3ContentType.from(contentType); if (type == null || !target.getAllowed().contains(type)) { throw new GeneralException(GeneralErrorCode.UNSUPPORTED_CONTENT_TYPE); } + // temp key 및 presigned URL 발급 return s3UploadManager.issueTempUpload( target.getBucketType(), target.getDir(), @@ -28,4 +33,29 @@ public PresignedUpload issuePresignedUpload(UploadTarget target, String contentT contentType ); } + + // temp key 및 파일 사이즈 검증 + public void validateUploadedKeys(UploadTarget target, List keys) { + + if (keys.size() != keys.stream().distinct().count()) { + throw new GeneralException(GeneralErrorCode.INVALID_FILE); + } + + long maxBytes = s3Properties.getMaxFileSize().toBytes(); + for (String key : keys) { + // temp key 형식 검증 + if (!s3UploadManager.isTempKey(key, target.getDir())) { + throw new GeneralException(GeneralErrorCode.INVALID_FILE); + } + + // 파일 크기 검증 + Long size = s3UploadManager.getObjectSize(target.getBucketType(), key); + if (size == null) { + throw new GeneralException(GeneralErrorCode.INVALID_FILE); + } + if (size > maxBytes) { + throw new GeneralException(GeneralErrorCode.FILE_SIZE_EXCEEDED); + } + } + } } From e8539a00d94d4e172558fa04ba8e20605279769d Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sat, 20 Jun 2026 00:32:56 +0900 Subject: [PATCH 095/102] =?UTF-8?q?Refactor(designer):=20=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=ED=8F=B4=EB=A6=AC=EC=98=A4=20presign/=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EC=9D=84=20=EA=B3=B5=EC=9A=A9=20S3FileService?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designer/exception/DesignerErrorCode.java | 5 +-- .../designer/service/PortfolioService.java | 40 +++---------------- .../backend/global/s3/enums/UploadTarget.java | 3 +- 3 files changed, 9 insertions(+), 39 deletions(-) diff --git a/src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java b/src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java index da57c87..f74d304 100644 --- a/src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java +++ b/src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java @@ -10,10 +10,7 @@ @AllArgsConstructor public enum DesignerErrorCode implements BaseErrorCode { - PORTFOLIO_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "DESIGNER_400_01", "포트폴리오 파일은 최대 3개까지 업로드 가능합니다."), - INVALID_PORTFOLIO_FILE(HttpStatus.BAD_REQUEST, "DESIGNER_400_02", "지원하지 않는 파일 형식입니다."), - PORTFOLIO_FILE_SIZE_EXCEEDED(HttpStatus.CONTENT_TOO_LARGE, "DESIGNER_413_01", "포트폴리오 파일 크기가 제한을 초과했습니다."), - PORTFOLIO_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "DESIGNER_500_01", "포트폴리오 파일 업로드에 실패했습니다."); + PORTFOLIO_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "DESIGNER_400_01", "포트폴리오 파일은 최대 3개까지 업로드 가능합니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java index f0f34d8..fe74caf 100644 --- a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java +++ b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java @@ -1,8 +1,6 @@ package ditda.backend.domain.designer.service; -import java.util.EnumSet; import java.util.List; -import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,10 +11,10 @@ import ditda.backend.domain.designer.repository.PortfolioRepository; import ditda.backend.global.apipayload.exception.GeneralException; import ditda.backend.global.s3.PresignedUpload; -import ditda.backend.global.s3.S3Properties; +import ditda.backend.global.s3.S3FileService; import ditda.backend.global.s3.S3UploadManager; import ditda.backend.global.s3.enums.BucketType; -import ditda.backend.global.s3.enums.S3ContentType; +import ditda.backend.global.s3.enums.UploadTarget; import lombok.RequiredArgsConstructor; @Service @@ -24,23 +22,15 @@ public class PortfolioService { private static final int MAX_PORTFOLIO_COUNT = 3; - private static final String DIR = "portfolio"; private static final BucketType BUCKET = BucketType.PRIVATE; - private static final Set ALLOWED = EnumSet.of(S3ContentType.PNG, S3ContentType.PDF); private final S3UploadManager s3UploadManager; + private final S3FileService s3FileService; private final PortfolioRepository portfolioRepository; - private final S3Properties s3Properties; public PresignedUpload generatePresignedUpload(String contentType) { - // 파일 타입 검증 - S3ContentType type = S3ContentType.from(contentType); - if (type == null || !ALLOWED.contains(type)) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); - } - - return s3UploadManager.issueTempUpload(BUCKET, DIR, type.getExtension(), contentType); + return s3FileService.issuePresignedUpload(UploadTarget.PORTFOLIO, contentType); } public void validateKeys(List keys) { @@ -50,26 +40,8 @@ public void validateKeys(List keys) { throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_LIMIT_EXCEEDED); } - // distinct key 검증 - if (keys.size() != keys.stream().distinct().count()) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); - } - - for (String key : keys) { - // 파일 key 형식 검증 - if (!s3UploadManager.isTempKey(key, DIR)) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); - } - - // 파일 크기 검증 - Long size = s3UploadManager.getObjectSize(BUCKET, key); - if (size == null) { - throw new GeneralException(DesignerErrorCode.INVALID_PORTFOLIO_FILE); // 미업로드 key - } - if (size > s3Properties.getMaxFileSize().toBytes()) { - throw new GeneralException(DesignerErrorCode.PORTFOLIO_FILE_SIZE_EXCEEDED); - } - } + // distinct, 형식, 크기 검증 + s3FileService.validateUploadedKeys(UploadTarget.PORTFOLIO, keys); } @Transactional diff --git a/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java b/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java index 5b7eae1..c0e673f 100644 --- a/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java +++ b/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java @@ -11,7 +11,8 @@ public enum UploadTarget { COMMISSION_MATERIAL("commission/material", BucketType.PRIVATE, EnumSet.of(S3ContentType.PNG)), - COMMISSION_REFERENCE("commission/reference", BucketType.PRIVATE, EnumSet.of(S3ContentType.PNG)); + COMMISSION_REFERENCE("commission/reference", BucketType.PRIVATE, EnumSet.of(S3ContentType.PNG)), + PORTFOLIO("portfolio", BucketType.PRIVATE, EnumSet.of(S3ContentType.PNG, S3ContentType.PDF)); private final String dir; private final BucketType bucketType; From 47b7856bbc1551226bbd5cf0ab19476a2e0ec458 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sat, 20 Jun 2026 00:59:29 +0900 Subject: [PATCH 096/102] =?UTF-8?q?Fix(s3):=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EC=B1=85=20=EB=B6=88=EB=B3=80=ED=99=94=20=EB=B0=8F?= =?UTF-8?q?=20=ED=82=A4=20=EA=B2=80=EC=A6=9D=20null=20=EA=B0=80=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ditda/backend/global/s3/S3FileService.java | 6 ++++++ .../java/ditda/backend/global/s3/enums/UploadTarget.java | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/ditda/backend/global/s3/S3FileService.java b/src/main/java/ditda/backend/global/s3/S3FileService.java index d7385af..c13ff6c 100644 --- a/src/main/java/ditda/backend/global/s3/S3FileService.java +++ b/src/main/java/ditda/backend/global/s3/S3FileService.java @@ -37,6 +37,12 @@ public PresignedUpload issuePresignedUpload(UploadTarget target, String contentT // temp key 및 파일 사이즈 검증 public void validateUploadedKeys(UploadTarget target, List keys) { + // 빈 키 여부 검증 + if (keys == null) { + throw new GeneralException(GeneralErrorCode.INVALID_FILE); + } + + // 중복 키 검증 if (keys.size() != keys.stream().distinct().count()) { throw new GeneralException(GeneralErrorCode.INVALID_FILE); } diff --git a/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java b/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java index c0e673f..e7b39af 100644 --- a/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java +++ b/src/main/java/ditda/backend/global/s3/enums/UploadTarget.java @@ -3,11 +3,9 @@ import java.util.EnumSet; import java.util.Set; -import lombok.AllArgsConstructor; import lombok.Getter; @Getter -@AllArgsConstructor public enum UploadTarget { COMMISSION_MATERIAL("commission/material", BucketType.PRIVATE, EnumSet.of(S3ContentType.PNG)), @@ -17,4 +15,10 @@ public enum UploadTarget { private final String dir; private final BucketType bucketType; private final Set allowed; + + UploadTarget(String dir, BucketType bucketType, Set allowed) { + this.dir = dir; + this.bucketType = bucketType; + this.allowed = Set.copyOf(allowed); + } } From f13f9977f8fdb1ecd22c53d92c17019e78970a19 Mon Sep 17 00:00:00 2001 From: fervovita Date: Sat, 20 Jun 2026 02:08:41 +0900 Subject: [PATCH 097/102] =?UTF-8?q?Refactor(dashboard):=20=EA=B0=95?= =?UTF-8?q?=EC=82=AC=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A5=BC=20=EB=8B=A8=EC=9D=BC=20JOIN=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DraftSubmissionCommissionResponse.java | 25 +++--- .../response/MatchingCommissionResponse.java | 19 +++-- .../response/RevisingCommissionResponse.java | 18 +++-- .../DashboardCommissionRepository.java | 63 +++++++++++++++ .../projection/DraftSubmissionView.java | 10 +++ .../repository/projection/MatchingView.java | 12 +++ .../repository/projection/RevisingView.java | 12 +++ .../service/InstructorDashboardService.java | 79 +++---------------- .../InstructorDashboardServiceTest.java | 43 +++++----- 9 files changed, 162 insertions(+), 119 deletions(-) create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/DraftSubmissionView.java create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/MatchingView.java create mode 100644 src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/RevisingView.java diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java index c04f902..1bfb15f 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/DraftSubmissionCommissionResponse.java @@ -2,27 +2,20 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.entity.enums.CategoryType; +import ditda.backend.domain.commission.dashboard.repository.projection.DraftSubmissionView; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "시안 제출 외주 응답") public record DraftSubmissionCommissionResponse( List commissions ) { - public static DraftSubmissionCommissionResponse of( - List commissions, - Map draftSubmissionCount, - Map viewable - ) { - List items = commissions.stream() - .map(c -> CommissionItem.from( - c, - draftSubmissionCount.get(c.getId()), - viewable.get(c.getId()) - )) + public static DraftSubmissionCommissionResponse of(List views, LocalDate today) { + + List items = views.stream() + .map(view -> CommissionItem.from(view, today)) .toList(); return new DraftSubmissionCommissionResponse(items); @@ -47,13 +40,17 @@ public record CommissionItem( @Schema(description = "1차 시안 마감일", example = "2026-06-23") LocalDate firstDraftDeadline ) { - private static CommissionItem from(Commission commission, long submitted, boolean isViewable) { + private static CommissionItem from(DraftSubmissionView view, LocalDate today) { + + Commission commission = view.getCommission(); + int submitted = view.getSubmissionCount().intValue(); + boolean isViewable = commission.isDraftListViewable(submitted, today); return new CommissionItem( commission.getId(), commission.getTitle(), commission.getCategoryType(), - new DraftSubmissionStatus((int)submitted, commission.getDesignerCount()), + new DraftSubmissionStatus(submitted, commission.getDesignerCount()), isViewable, commission.getFirstDraftDeadline() ); diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java index 5c4e64a..f32d73b 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/MatchingCommissionResponse.java @@ -2,9 +2,9 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.dashboard.repository.projection.MatchingView; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "매칭 중인 외주 응답") @@ -12,9 +12,10 @@ public record MatchingCommissionResponse( List commissions ) { - public static MatchingCommissionResponse of(List commissions, Map matchedCount) { - List items = commissions.stream() - .map(c -> CommissionItem.from(c, matchedCount.get(c.getId()))) + public static MatchingCommissionResponse of(List views) { + + List items = views.stream() + .map(CommissionItem::from) .toList(); return new MatchingCommissionResponse(items); @@ -33,12 +34,18 @@ public record CommissionItem( @Schema(description = "디자이너 모집 마감일", example = "2026-06-23") LocalDate applicationDeadline ) { - private static CommissionItem from(Commission commission, long matched) { + private static CommissionItem from(MatchingView view) { + + Commission commission = view.getCommission(); + int matched = commission.matchedCount( + view.getDistinctLevelCount().intValue(), + view.getTotalCount().intValue() + ); return new CommissionItem( commission.getId(), commission.getTitle(), - new MatchStatusResponse((int)matched, commission.getDesignerCount()), + new MatchStatusResponse(matched, commission.getDesignerCount()), commission.getApplicationDeadline() ); } diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java index 6f54eae..7dce095 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/dto/response/RevisingCommissionResponse.java @@ -2,10 +2,9 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; import ditda.backend.domain.commission.core.entity.Commission; -import ditda.backend.domain.commission.revision.dto.RevisionStatus; +import ditda.backend.domain.commission.dashboard.repository.projection.RevisingView; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "수정 중인 외주 응답") @@ -13,9 +12,10 @@ public record RevisingCommissionResponse( List commissions ) { - public static RevisingCommissionResponse of(List commissions, Map statuses) { - List items = commissions.stream() - .map(c -> CommissionItem.from(c, statuses.get(c.getId()))) + public static RevisingCommissionResponse of(List views) { + + List items = views.stream() + .map(CommissionItem::from) .toList(); return new RevisingCommissionResponse(items); @@ -38,13 +38,15 @@ public record CommissionItem( LocalDate finalDeadline ) { - private static CommissionItem from(Commission commission, RevisionStatus status) { + private static CommissionItem from(RevisingView view) { + + Commission commission = view.getCommission(); return new CommissionItem( commission.getId(), commission.getTitle(), - status.submitted(), - status.hasUpdated(), + view.getSubmitted(), + view.getHasUpdated(), commission.getFinalDeadline() ); } diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java b/src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java new file mode 100644 index 0000000..65438da --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java @@ -0,0 +1,63 @@ +package ditda.backend.domain.commission.dashboard.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; +import ditda.backend.domain.commission.core.entity.Commission; +import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; +import ditda.backend.domain.commission.dashboard.repository.projection.DraftSubmissionView; +import ditda.backend.domain.commission.dashboard.repository.projection.MatchingView; +import ditda.backend.domain.commission.dashboard.repository.projection.RevisingView; + +public interface DashboardCommissionRepository extends Repository { + + // 시안 제출 현황: commission + DRAFT_SUBMITTED 개수 + @Query("SELECT c AS commission, " + + "COUNT(a.id) AS submissionCount " + + "FROM Commission c " + + "LEFT JOIN CommissionApplication a ON a.commission = c AND a.status = :appStatus " + + "WHERE c.instructor.id = :instructorId AND c.status = :status " + + "GROUP BY c.id " + + "ORDER BY c.firstDraftDeadline ASC") + List findDraftSubmissionViews( + @Param("instructorId") Long instructorId, + @Param("commissionStatus") CommissionStatus commissionStatus, + @Param("applicationStatus") ApplicationStatus applicationStatus + ); + + // 매칭 중인 외주: commission + distinct level 수 + PENDING 지원자 수 + @Query("SELECT c AS commission, " + + "COUNT(DISTINCT a.designer.level) AS distinctLevelCount, " + + "COUNT(a.id) AS totalCount " + + "FROM Commission c " + + "LEFT JOIN CommissionApplication a ON a.commission = c AND a.status = :appStatus " + + "WHERE c.instructor.id = :instructorId AND c.status = :status " + + "GROUP BY c.id " + + "ORDER BY c.applicationDeadline ASC") + List findMatchingViews( + @Param("instructorId") Long instructorId, + @Param("commissionStatus") CommissionStatus commissionStatus, + @Param("applicationStatus") ApplicationStatus applicationStatus + ); + + // 수정 중인 외주: commission + submitted(응답 없는 요청 존재) + hasUpdated(미열람 응답 존재) + @Query("SELECT c AS commission, " + + "CASE WHEN COUNT(CASE WHEN rr.id IS NOT NULL AND resp.id IS NULL THEN 1 END) > 0 " + + " THEN true ELSE false END AS submitted, " + + "CASE WHEN COUNT(CASE WHEN resp.id IS NOT NULL AND resp.checked = false THEN 1 END) > 0 " + + " THEN true ELSE false END AS hasUpdated " + + "FROM Commission c " + + "LEFT JOIN RevisionRequest rr ON rr.commission = c " + + "LEFT JOIN RevisionResponse resp ON resp.revisionRequest = rr " + + "WHERE c.instructor.id = :instructorId AND c.status = :status " + + "GROUP BY c.id " + + "ORDER BY c.finalDeadline ASC") + List findRevisingViews( + @Param("instructorId") Long instructorId, + @Param("commissionStatus") CommissionStatus commissionStatus + ); +} diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/DraftSubmissionView.java b/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/DraftSubmissionView.java new file mode 100644 index 0000000..90ce536 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/DraftSubmissionView.java @@ -0,0 +1,10 @@ +package ditda.backend.domain.commission.dashboard.repository.projection; + +import ditda.backend.domain.commission.core.entity.Commission; + +public interface DraftSubmissionView { + + Commission getCommission(); + + Long getSubmissionCount(); +} diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/MatchingView.java b/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/MatchingView.java new file mode 100644 index 0000000..6d28fc0 --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/MatchingView.java @@ -0,0 +1,12 @@ +package ditda.backend.domain.commission.dashboard.repository.projection; + +import ditda.backend.domain.commission.core.entity.Commission; + +public interface MatchingView { + + Commission getCommission(); + + Long getDistinctLevelCount(); + + Long getTotalCount(); +} diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/RevisingView.java b/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/RevisingView.java new file mode 100644 index 0000000..792af8e --- /dev/null +++ b/src/main/java/ditda/backend/domain/commission/dashboard/repository/projection/RevisingView.java @@ -0,0 +1,12 @@ +package ditda.backend.domain.commission.dashboard.repository.projection; + +import ditda.backend.domain.commission.core.entity.Commission; + +public interface RevisingView { + + Commission getCommission(); + + Boolean getSubmitted(); + + Boolean getHasUpdated(); +} diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java index 4965185..f63c49b 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardService.java @@ -2,23 +2,19 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; -import ditda.backend.domain.commission.application.service.CommissionApplicationService; -import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; -import ditda.backend.domain.commission.core.service.InstructorCommissionService; import ditda.backend.domain.commission.dashboard.dto.response.DraftSubmissionCommissionResponse; import ditda.backend.domain.commission.dashboard.dto.response.MatchingCommissionResponse; import ditda.backend.domain.commission.dashboard.dto.response.RevisingCommissionResponse; -import ditda.backend.domain.commission.revision.dto.RevisionStatus; -import ditda.backend.domain.commission.revision.service.RevisionService; +import ditda.backend.domain.commission.dashboard.repository.DashboardCommissionRepository; +import ditda.backend.domain.commission.dashboard.repository.projection.DraftSubmissionView; +import ditda.backend.domain.commission.dashboard.repository.projection.MatchingView; +import ditda.backend.domain.commission.dashboard.repository.projection.RevisingView; import lombok.RequiredArgsConstructor; @Service @@ -26,93 +22,42 @@ @Transactional(readOnly = true) public class InstructorDashboardService { - private final InstructorCommissionService commissionService; - private final CommissionApplicationService commissionApplicationService; - private final RevisionService revisionService; + private final DashboardCommissionRepository dashboardCommissionRepository; // 시안 제출 현황 조회 public DraftSubmissionCommissionResponse getDraftSubmissions(Long instructorId) { - // 시안 제출 중인 외주 조회 - List commissions = commissionService.getCommissionByInstructorAndStatus( + List views = dashboardCommissionRepository.findDraftSubmissionViews( instructorId, CommissionStatus.IN_PROGRESS, - Sort.by("firstDraftDeadline").ascending() - ); - - // 외주 Id 추출 - List commissionIds = commissions.stream().map(Commission::getId).toList(); - - // 시안을 제출한 디자이너 수 조회 - Map draftSubmissionCount = commissionApplicationService.countApplicationByStatus( - commissionIds, ApplicationStatus.DRAFT_SUBMITTED ); - // 외주별 시안 확인 가능 여부 LocalDate today = LocalDate.now(); - Map viewable = commissions.stream() - .collect(Collectors.toMap( - Commission::getId, - c -> c.isDraftListViewable(draftSubmissionCount.get(c.getId()).intValue(), today) - )); - return DraftSubmissionCommissionResponse.of(commissions, draftSubmissionCount, viewable); + return DraftSubmissionCommissionResponse.of(views, today); } // 매칭 중인 외주 조회 public MatchingCommissionResponse getMatchingCommissions(Long instructorId) { - // 모집 중인 외주 조회 - List commissions = commissionService.getCommissionByInstructorAndStatus( + List views = dashboardCommissionRepository.findMatchingViews( instructorId, CommissionStatus.RECRUITING, - Sort.by("applicationDeadline").ascending() - ); - - // 외주 Id 추출 - List commissionIds = commissions.stream().map(Commission::getId).toList(); - - // 외주별 지원자가 존재하는 레벨 종류 수 - Map distinctLevelCount = commissionApplicationService.countDistinctLevelByStatus( - commissionIds, ApplicationStatus.PENDING ); - // 외주별 전체 지원자 수 - Map applicantCount = commissionApplicationService.countApplicationByStatus( - commissionIds, - ApplicationStatus.PENDING - ); - - // 외주별 매칭 확정 인원 계산 - Map matchedCount = commissions.stream() - .collect(Collectors.toMap( - Commission::getId, - c -> (long)c.matchedCount( - distinctLevelCount.get(c.getId()).intValue(), - applicantCount.get(c.getId()).intValue()) - )); - - return MatchingCommissionResponse.of(commissions, matchedCount); + return MatchingCommissionResponse.of(views); } // 수정 중인 외주 조회 public RevisingCommissionResponse getRevisingCommissions(Long instructorId) { - // 수정 중인 외주 조회 - List commissions = commissionService.getCommissionByInstructorAndStatus( + List views = dashboardCommissionRepository.findRevisingViews( instructorId, - CommissionStatus.EDITING, - Sort.by("finalDeadline").ascending() + CommissionStatus.EDITING ); - // 외주 Id 추출 - List commissionIds = commissions.stream().map(Commission::getId).toList(); - - // 수정 관련 데이터 조회 (submitted/hasUpdated) - Map statuses = revisionService.getRevisionStatuses(commissionIds); - - return RevisingCommissionResponse.of(commissions, statuses); + return RevisingCommissionResponse.of(views); } } diff --git a/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java index 8407d47..d893c93 100644 --- a/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java +++ b/src/test/java/ditda/backend/domain/commission/dashboard/service/InstructorDashboardServiceTest.java @@ -5,7 +5,6 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,15 +12,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Sort; import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; -import ditda.backend.domain.commission.application.service.CommissionApplicationService; import ditda.backend.domain.commission.core.entity.Commission; import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.commission.core.entity.enums.PlanCode; -import ditda.backend.domain.commission.core.service.InstructorCommissionService; import ditda.backend.domain.commission.dashboard.dto.response.MatchingCommissionResponse; +import ditda.backend.domain.commission.dashboard.repository.DashboardCommissionRepository; +import ditda.backend.domain.commission.dashboard.repository.projection.MatchingView; @ExtendWith(MockitoExtension.class) class InstructorDashboardServiceTest { @@ -29,10 +27,7 @@ class InstructorDashboardServiceTest { private static final Long INSTRUCTOR_ID = 100L; @Mock - private InstructorCommissionService commissionService; - - @Mock - private CommissionApplicationService commissionApplicationService; + private DashboardCommissionRepository dashboardCommissionRepository; @InjectMocks private InstructorDashboardService instructorDashboardService; @@ -58,15 +53,19 @@ void getMatchingCommissions_matchedCal() { .finalDeadline(deadline) .build(); - List commissionIds = List.of(1L, 2L); + MatchingView maxView = mock(MatchingView.class); + given(maxView.getCommission()).willReturn(max); + given(maxView.getDistinctLevelCount()).willReturn(3L); // 레벨 3종류 + given(maxView.getTotalCount()).willReturn(4L); // 지원자 4명 + + MatchingView basicView = mock(MatchingView.class); + given(basicView.getCommission()).willReturn(basic); + given(basicView.getDistinctLevelCount()).willReturn(2L); // 레벨 2종류 + given(basicView.getTotalCount()).willReturn(4L); // 지원자 4명 - given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, - Sort.by("applicationDeadline").ascending())) - .willReturn(List.of(max, basic)); - given(commissionApplicationService.countDistinctLevelByStatus(commissionIds, ApplicationStatus.PENDING)) - .willReturn(Map.of(1L, 3L, 2L, 2L)); // max -> 레벨 3종류 / basic -> 레벨 2종류 - given(commissionApplicationService.countApplicationByStatus(commissionIds, ApplicationStatus.PENDING)) - .willReturn(Map.of(1L, 4L, 2L, 4L)); // max -> 지원자 4명 / basic -> 지원자 4명 + given(dashboardCommissionRepository.findMatchingViews( + INSTRUCTOR_ID, CommissionStatus.RECRUITING, ApplicationStatus.PENDING + )).willReturn(List.of(maxView, basicView)); // when MatchingCommissionResponse response = instructorDashboardService.getMatchingCommissions(INSTRUCTOR_ID); @@ -77,7 +76,7 @@ void getMatchingCommissions_matchedCal() { MatchingCommissionResponse.CommissionItem maxItem = response.commissions().getFirst(); assertThat(maxItem.commissionId()).isEqualTo(1L); assertThat(maxItem.title()).isEqualTo("MAX 외주"); - assertThat(maxItem.applicationsDeadline()).isEqualTo(deadline); + assertThat(maxItem.applicationDeadline()).isEqualTo(deadline); assertThat(maxItem.matching().matched()).isEqualTo(4); assertThat(maxItem.matching().total()).isEqualTo(5); @@ -92,13 +91,9 @@ void getMatchingCommissions_matchedCal() { void getMatchingCommissions_emptyList() { // given - given(commissionService.getCommissionByInstructorAndStatus(INSTRUCTOR_ID, CommissionStatus.RECRUITING, - Sort.by("applicationDeadline").ascending())) - .willReturn(List.of()); - given(commissionApplicationService.countDistinctLevelByStatus(List.of(), ApplicationStatus.PENDING)) - .willReturn(Map.of()); - given(commissionApplicationService.countApplicationByStatus(List.of(), ApplicationStatus.PENDING)) - .willReturn(Map.of()); + given(dashboardCommissionRepository.findMatchingViews( + INSTRUCTOR_ID, CommissionStatus.RECRUITING, ApplicationStatus.PENDING + )).willReturn(List.of()); // when MatchingCommissionResponse response = instructorDashboardService.getMatchingCommissions(INSTRUCTOR_ID); From c16415d4c08fee9c244b48142b1d441cee2432d4 Mon Sep 17 00:00:00 2001 From: fervovita Date: Sat, 20 Jun 2026 02:29:04 +0900 Subject: [PATCH 098/102] =?UTF-8?q?Refactor(dashboard):=20JOIN=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EC=9D=B4=ED=9B=84=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommissionApplicationRepository.java | 20 ------- .../ApplicationSubmissionCount.java | 9 --- .../service/CommissionApplicationService.java | 58 ------------------- .../core/repository/CommissionRepository.java | 5 -- .../service/InstructorCommissionService.java | 7 --- .../revision/dto/RevisionStatus.java | 7 --- .../repository/RevisionRequestRepository.java | 17 ------ .../projection/RevisionStatusView.java | 10 ---- .../revision/service/RevisionService.java | 43 -------------- 9 files changed, 176 deletions(-) delete mode 100644 src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java delete mode 100644 src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java delete mode 100644 src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java delete mode 100644 src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java delete mode 100644 src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java diff --git a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java index 48d801a..e6acbd9 100644 --- a/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java +++ b/src/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.java @@ -3,30 +3,10 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.application.entity.CommissionApplication; -import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; -import ditda.backend.domain.commission.application.repository.projection.ApplicationSubmissionCount; public interface CommissionApplicationRepository extends JpaRepository { List findByCommission_Id(Long commissionId); - - @Query("SELECT a.commission.id AS commissionId, COUNT(a) AS count " - + "FROM CommissionApplication a " - + "WHERE a.commission.id IN :commissionIds AND a.status = :status " - + "GROUP BY a.commission.id") - List countByCommissionIdsAndStatus( - @Param("commissionIds") List commissionIds, - @Param("status") ApplicationStatus status); - - @Query("SELECT a.commission.id AS commissionId, COUNT(DISTINCT a.designer.level) AS count " - + "FROM CommissionApplication a " - + "WHERE a.commission.id IN :commissionIds AND a.status = :status " - + "GROUP BY a.commission.id") - List countDistinctLevelByCommissionIdsAndStatus( - @Param("commissionIds") List commissionIds, - @Param("status") ApplicationStatus status); } diff --git a/src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java b/src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java deleted file mode 100644 index 8185f76..0000000 --- a/src/main/java/ditda/backend/domain/commission/application/repository/projection/ApplicationSubmissionCount.java +++ /dev/null @@ -1,9 +0,0 @@ -package ditda.backend.domain.commission.application.repository.projection; - -public interface ApplicationSubmissionCount { - - Long getCommissionId(); - - Long getCount(); - -} diff --git a/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java b/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java deleted file mode 100644 index 838207e..0000000 --- a/src/main/java/ditda/backend/domain/commission/application/service/CommissionApplicationService.java +++ /dev/null @@ -1,58 +0,0 @@ -package ditda.backend.domain.commission.application.service; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; - -import ditda.backend.domain.commission.application.entity.enums.ApplicationStatus; -import ditda.backend.domain.commission.application.repository.CommissionApplicationRepository; -import ditda.backend.domain.commission.application.repository.projection.ApplicationSubmissionCount; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class CommissionApplicationService { - - private final CommissionApplicationRepository commissionApplicationRepository; - - // commission별 상태 카운트 - public Map countApplicationByStatus(List commissionIds, ApplicationStatus status) { - if (commissionIds.isEmpty()) { - return Map.of(); - } - - // 집계 결과 - List counts = - commissionApplicationRepository.countByCommissionIdsAndStatus(commissionIds, status); - - return toCountMap(counts, commissionIds); - } - - // commission별 지원자가 존재하는 디자이너 레벨 종류 수 - public Map countDistinctLevelByStatus(List commissionIds, ApplicationStatus status) { - if (commissionIds.isEmpty()) { - return Map.of(); - } - - // 집계 결과 - List counts = - commissionApplicationRepository.countDistinctLevelByCommissionIdsAndStatus(commissionIds, status); - - return toCountMap(counts, commissionIds); - } - - // 집계 결과를 Map으로 변환 + 0 보정 - private Map toCountMap(List counts, List commissionIds) { - Map countByCommissionId = counts.stream() - .collect(Collectors.toMap( - ApplicationSubmissionCount::getCommissionId, - ApplicationSubmissionCount::getCount - )); - - return commissionIds.stream() - .distinct() - .collect(Collectors.toMap(id -> id, id -> countByCommissionId.getOrDefault(id, 0L))); - } -} diff --git a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java index 527a0ba..c433833 100644 --- a/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java +++ b/src/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.java @@ -1,22 +1,17 @@ package ditda.backend.domain.commission.core.repository; import java.time.LocalDateTime; -import java.util.List; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.core.entity.Commission; -import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.designer.entity.Designer; public interface CommissionRepository extends JpaRepository { - List findByInstructorIdAndStatus(Long instructorId, CommissionStatus status, Sort sort); - @Modifying @Query("update Commission c " + "set c.assignedDesigner = :designer, " diff --git a/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java index 7e513c2..f618670 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/InstructorCommissionService.java @@ -3,7 +3,6 @@ import java.time.LocalDateTime; import java.util.List; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,7 +14,6 @@ import ditda.backend.domain.commission.core.entity.CommissionColor; import ditda.backend.domain.commission.core.entity.CommissionConcept; import ditda.backend.domain.commission.core.entity.enums.ColorSelectionMode; -import ditda.backend.domain.commission.core.entity.enums.CommissionStatus; import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.handler.CommissionCategoryHandler; import ditda.backend.domain.commission.core.repository.CommissionColorRepository; @@ -93,11 +91,6 @@ public CommissionCreateResponse createCommission( return CommissionCreateResponse.from(commission); } - // status인 commission 조회 - public List getCommissionByInstructorAndStatus(Long instructorId, CommissionStatus status, Sort sort) { - return commissionRepository.findByInstructorIdAndStatus(instructorId, status, sort); - } - // 외주 조회 + 강사 확인 @Transactional(readOnly = true) public Commission getOwnedCommission(Long commissionId, Long instructorId) { diff --git a/src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java b/src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java deleted file mode 100644 index 4e34532..0000000 --- a/src/main/java/ditda/backend/domain/commission/revision/dto/RevisionStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -package ditda.backend.domain.commission.revision.dto; - -public record RevisionStatus( - boolean submitted, - boolean hasUpdated -) { -} diff --git a/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java index 66934ac..d011fba 100644 --- a/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java +++ b/src/main/java/ditda/backend/domain/commission/revision/repository/RevisionRequestRepository.java @@ -1,25 +1,8 @@ package ditda.backend.domain.commission.revision.repository; -import java.util.List; - import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import ditda.backend.domain.commission.revision.entity.RevisionRequest; -import ditda.backend.domain.commission.revision.repository.projection.RevisionStatusView; public interface RevisionRequestRepository extends JpaRepository { - - // 답변이 없는 수정 요청이 하나라도 있으면 submitted=true (전송 완료) - // 미열람 답변이 하나라도 있으면 hasUpdated=true - @Query("SELECT rr.commission.id AS commissionId," - + "CASE WHEN COUNT(CASE WHEN resp.id IS NULL THEN 1 END) > 0 THEN true ELSE false END AS submitted, " - + "CASE WHEN COUNT(CASE WHEN resp.id IS NOT NULL AND resp.checked = false THEN 1 END) > 0 " - + "THEN true ELSE false END AS hasUpdated " - + "FROM RevisionRequest rr " - + "LEFT JOIN RevisionResponse resp ON resp.revisionRequest = rr " - + "WHERE rr.commission.id IN :commissionIds " - + "GROUP BY rr.commission.id") - List findRevisionStatuses(@Param("commissionIds") List commissionIds); } diff --git a/src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java b/src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java deleted file mode 100644 index abeb2cd..0000000 --- a/src/main/java/ditda/backend/domain/commission/revision/repository/projection/RevisionStatusView.java +++ /dev/null @@ -1,10 +0,0 @@ -package ditda.backend.domain.commission.revision.repository.projection; - -public interface RevisionStatusView { - - Long getCommissionId(); - - Boolean getSubmitted(); - - Boolean getHasUpdated(); -} diff --git a/src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java b/src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java deleted file mode 100644 index 898d72b..0000000 --- a/src/main/java/ditda/backend/domain/commission/revision/service/RevisionService.java +++ /dev/null @@ -1,43 +0,0 @@ -package ditda.backend.domain.commission.revision.service; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import ditda.backend.domain.commission.revision.dto.RevisionStatus; -import ditda.backend.domain.commission.revision.repository.RevisionRequestRepository; -import ditda.backend.domain.commission.revision.repository.projection.RevisionStatusView; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class RevisionService { - - private final RevisionRequestRepository revisionRequestRepository; - - // 외주별 수정 상태(submitted/hasUpdated) - public Map getRevisionStatuses(List commissionIds) { - if (commissionIds.isEmpty()) { - return Map.of(); - } - - Map statuses = revisionRequestRepository.findRevisionStatuses(commissionIds).stream() - .collect(Collectors.toMap( - RevisionStatusView::getCommissionId, - this::toStatus - )); - - // 수정 요청이 없는 외주는 (false, false) - commissionIds.forEach(id -> statuses.putIfAbsent(id, new RevisionStatus(false, false))); - - return statuses; - } - - private RevisionStatus toStatus(RevisionStatusView view) { - return new RevisionStatus(Boolean.TRUE.equals(view.getSubmitted()), Boolean.TRUE.equals(view.getHasUpdated())); - } -} From d362da81acce6802da51daacd8600d9f153c6e3f Mon Sep 17 00:00:00 2001 From: fervovita Date: Sat, 20 Jun 2026 02:31:08 +0900 Subject: [PATCH 099/102] =?UTF-8?q?Fix(dashboard):=20=EC=99=B8=EC=A3=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EB=B3=80=EC=88=98=EB=AA=85=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/DashboardCommissionRepository.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java b/src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java index 65438da..904b095 100644 --- a/src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java +++ b/src/main/java/ditda/backend/domain/commission/dashboard/repository/DashboardCommissionRepository.java @@ -19,8 +19,8 @@ public interface DashboardCommissionRepository extends Repository findDraftSubmissionViews( @@ -34,8 +34,8 @@ List findDraftSubmissionViews( + "COUNT(DISTINCT a.designer.level) AS distinctLevelCount, " + "COUNT(a.id) AS totalCount " + "FROM Commission c " - + "LEFT JOIN CommissionApplication a ON a.commission = c AND a.status = :appStatus " - + "WHERE c.instructor.id = :instructorId AND c.status = :status " + + "LEFT JOIN CommissionApplication a ON a.commission = c AND a.status = :applicationStatus " + + "WHERE c.instructor.id = :instructorId AND c.status = :commissionStatus " + "GROUP BY c.id " + "ORDER BY c.applicationDeadline ASC") List findMatchingViews( @@ -53,7 +53,7 @@ List findMatchingViews( + "FROM Commission c " + "LEFT JOIN RevisionRequest rr ON rr.commission = c " + "LEFT JOIN RevisionResponse resp ON resp.revisionRequest = rr " - + "WHERE c.instructor.id = :instructorId AND c.status = :status " + + "WHERE c.instructor.id = :instructorId AND c.status = :commissionStatus " + "GROUP BY c.id " + "ORDER BY c.finalDeadline ASC") List findRevisingViews( From 7609c07d120a55a5fd9cbefda4cbaf126b41835f Mon Sep 17 00:00:00 2001 From: fervovita Date: Sat, 20 Jun 2026 16:51:59 +0900 Subject: [PATCH 100/102] =?UTF-8?q?Feat:=20JWT=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EA=B0=95=EC=82=AC=20=EB=B0=8F=20=EB=94=94=EC=9E=90=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EC=97=AD=ED=95=A0=20=EC=9D=B8=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 20 +++--- .../auth/service/DesignerAuthService.java | 2 +- .../auth/service/InstructorAuthService.java | 2 +- .../domain/user/service/UserService.java | 5 ++ .../backend/global/config/SecurityConfig.java | 2 + .../backend/global/jwt/JwtTokenProvider.java | 69 +++++++++++++------ .../global/jwt/dto/AccessTokenPayload.java | 11 +++ .../global/jwt/filter/JwtTokenFilter.java | 9 ++- 8 files changed, 79 insertions(+), 41 deletions(-) create mode 100644 src/main/java/ditda/backend/global/jwt/dto/AccessTokenPayload.java diff --git a/src/main/java/ditda/backend/domain/auth/service/AuthService.java b/src/main/java/ditda/backend/domain/auth/service/AuthService.java index 0a74e02..73d46ba 100644 --- a/src/main/java/ditda/backend/domain/auth/service/AuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/AuthService.java @@ -33,28 +33,23 @@ public class AuthService { private final RefreshTokenRepository refreshTokenRepository; @Transactional - public TokenResult issueTokens(Long userId) { + public TokenResult issueTokens(User user) { + + Long userId = user.getId(); // 1. 만료된 토큰 삭제 refreshTokenRepository.deleteExpiredByUserId(userId, LocalDateTime.now()); // 2. sessionId(로그인 기기 식별) 및 JWT 토큰 발급 String sessionId = UUID.randomUUID().toString(); - String accessToken = jwtTokenProvider.generateAccessToken(userId); + String accessToken = jwtTokenProvider.generateAccessToken(userId, user.getRole()); String refreshToken = jwtTokenProvider.generateRefreshToken(userId, sessionId); // 3. DB에는 해시 값으로 저장 String refreshTokenHash = refreshTokenHasher.hash(refreshToken); LocalDateTime expiresAt = jwtTokenProvider.getExpiration(refreshToken); - refreshTokenRepository.save( - RefreshToken.createRefreshToken( - userService.getReferenceById(userId), - sessionId, - refreshTokenHash, - expiresAt - ) - ); + refreshTokenRepository.save(RefreshToken.createRefreshToken(user, sessionId, refreshTokenHash, expiresAt)); return new TokenResult(accessToken, refreshToken); } @@ -71,7 +66,7 @@ public AuthResult login(LoginRequest request) { } // 3. Access / Refresh 토큰 발급 - TokenResult tokens = issueTokens(user.getId()); + TokenResult tokens = issueTokens(user); return new AuthResult( user.getId(), @@ -134,7 +129,8 @@ public TokenResult reissue(String refreshToken) { } // 5. 새 Access / Refresh 토큰 발급. (sessionId는 유지) - String newAccessToken = jwtTokenProvider.generateAccessToken(userId); + User user = userService.findById(userId); + String newAccessToken = jwtTokenProvider.generateAccessToken(userId, user.getRole()); String newRefreshToken = jwtTokenProvider.generateRefreshToken(userId, sessionId); String newRefreshTokenHash = refreshTokenHasher.hash(newRefreshToken); LocalDateTime newExpiresAt = jwtTokenProvider.getExpiration(newRefreshToken); diff --git a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java index 8c79e4a..8d11d6f 100644 --- a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java @@ -72,7 +72,7 @@ public AuthResult signup(DesignerSignupRequest request, List portfolioKe portfolioService.savePortfolios(designer, portfolioKeys); // accessToken&refreshToken 발급 - TokenResult tokens = authService.issueTokens(user.getId()); + TokenResult tokens = authService.issueTokens(user); // 관리자 알림 이벤트 발행 eventPublisher.publishEvent(new DesignerSignedUpEvent( diff --git a/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java index 5eec766..ca4e3ae 100644 --- a/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java @@ -53,7 +53,7 @@ public AuthResult signup(InstructorSignupRequest request) { // 왜인지 모르겠지만 save로 했을때는 user만 저장. Instructor는 저장 X instructorRepository.saveAndFlush(Instructor.createInstructor(user)); - TokenResult tokens = authService.issueTokens(user.getId()); + TokenResult tokens = authService.issueTokens(user); return new AuthResult( user.getId(), diff --git a/src/main/java/ditda/backend/domain/user/service/UserService.java b/src/main/java/ditda/backend/domain/user/service/UserService.java index 491343e..75527cb 100644 --- a/src/main/java/ditda/backend/domain/user/service/UserService.java +++ b/src/main/java/ditda/backend/domain/user/service/UserService.java @@ -23,6 +23,11 @@ public User getReferenceById(Long userId) { return userRepository.getReferenceById(userId); } + public User findById(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + } + public boolean existsByUsername(String username) { return userRepository.existsByUsername(username); } diff --git a/src/main/java/ditda/backend/global/config/SecurityConfig.java b/src/main/java/ditda/backend/global/config/SecurityConfig.java index 0a34101..721a090 100644 --- a/src/main/java/ditda/backend/global/config/SecurityConfig.java +++ b/src/main/java/ditda/backend/global/config/SecurityConfig.java @@ -62,6 +62,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.POST, "/api/v1/designers/auth/signup", "/api/v1/designers/auth/signup/portfolio/presigned-url").permitAll() .requestMatchers(HttpMethod.GET, "/actuator/health").permitAll() + .requestMatchers("/api/v1/instructors/**").hasRole("INSTRUCTOR") + .requestMatchers("/api/v1/designers/**").hasRole("DESIGNER") .anyRequest().authenticated() ) .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/ditda/backend/global/jwt/JwtTokenProvider.java b/src/main/java/ditda/backend/global/jwt/JwtTokenProvider.java index 183d1b3..ed9015c 100644 --- a/src/main/java/ditda/backend/global/jwt/JwtTokenProvider.java +++ b/src/main/java/ditda/backend/global/jwt/JwtTokenProvider.java @@ -9,6 +9,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import ditda.backend.domain.user.entity.enums.UserRole; +import ditda.backend.global.jwt.dto.AccessTokenPayload; import ditda.backend.global.jwt.dto.RefreshTokenPayload; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; @@ -26,6 +28,12 @@ @Slf4j public class JwtTokenProvider { + private static final String SESSION_ID_CLAIM = "sid"; + private static final String TOKEN_TYPE_CLAIM = "type"; + private static final String ROLE_CLAIM = "role"; + private static final String ACCESS_TOKEN_TYPE = "access_token"; + private static final String REFRESH_TOKEN_TYPE = "refresh_token"; + @Value("${jwt.secret}") private String secret; @@ -37,11 +45,6 @@ public class JwtTokenProvider { private SecretKey key; - private static final String SESSION_ID_CLAIM = "sid"; - private static final String TOKEN_TYPE_CLAIM = "type"; - private static final String ACCESS_TOKEN_TYPE = "access_token"; - private static final String REFRESH_TOKEN_TYPE = "refresh_token"; - @PostConstruct public void init() { @@ -49,7 +52,7 @@ public void init() { key = Keys.hmacShaKeyFor(keyBytes); } - public String generateAccessToken(Long userId) { + public String generateAccessToken(Long userId, UserRole role) { Date now = new Date(); Date expiryTime = new Date(now.getTime() + accessTokenExpiration); @@ -57,6 +60,7 @@ public String generateAccessToken(Long userId) { return Jwts.builder() .subject(userId.toString()) .claim(TOKEN_TYPE_CLAIM, ACCESS_TOKEN_TYPE) + .claim(ROLE_CLAIM, role.name()) .issuedAt(now) .expiration(expiryTime) .signWith(key, Jwts.SIG.HS256) @@ -78,7 +82,35 @@ public String generateRefreshToken(Long userId, String sessionId) { .compact(); } - public String getSessionId(Claims claims) { + public AccessTokenPayload getAccessTokenPayload(String token) { + + Claims claims = validateAccessToken(token); + + Long userId = Long.parseLong(claims.getSubject()); + UserRole role = getRole(claims); + + return new AccessTokenPayload(userId, role); + } + + public RefreshTokenPayload getRefreshTokenPayload(String token) { + + Claims claims = validateRefreshToken(token); + + Long userId = Long.parseLong(claims.getSubject()); + String sessionId = getSessionId(claims); + + return new RefreshTokenPayload(userId, sessionId); + } + + public LocalDateTime getExpiration(String token) { + + return getClaims(token).getExpiration() + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } + + private String getSessionId(Claims claims) { String sessionId = claims.get(SESSION_ID_CLAIM, String.class); @@ -89,7 +121,7 @@ public String getSessionId(Claims claims) { return sessionId; } - public Claims validateAccessToken(String token) { + private Claims validateAccessToken(String token) { Claims claims = validateToken(token); if (!ACCESS_TOKEN_TYPE.equals(claims.get(TOKEN_TYPE_CLAIM, String.class))) { @@ -99,7 +131,7 @@ public Claims validateAccessToken(String token) { return claims; } - public Claims validateRefreshToken(String token) { + private Claims validateRefreshToken(String token) { Claims claims = validateToken(token); if (!REFRESH_TOKEN_TYPE.equals(claims.get(TOKEN_TYPE_CLAIM, String.class))) { @@ -109,14 +141,6 @@ public Claims validateRefreshToken(String token) { return claims; } - public LocalDateTime getExpiration(String token) { - - return getClaims(token).getExpiration() - .toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); - } - private Claims getClaims(String token) { return Jwts.parser() @@ -126,14 +150,15 @@ private Claims getClaims(String token) { .getPayload(); } - public RefreshTokenPayload getRefreshTokenPayload(String token) { + private UserRole getRole(Claims claims) { - Claims claims = validateRefreshToken(token); + String role = claims.get(ROLE_CLAIM, String.class); - Long userId = Long.parseLong(claims.getSubject()); - String sessionId = getSessionId(claims); + if (role == null || role.isBlank()) { + throw new JwtException("Access Token에 role 정보가 없습니다."); + } - return new RefreshTokenPayload(userId, sessionId); + return UserRole.valueOf(role); } private Claims validateToken(String token) { diff --git a/src/main/java/ditda/backend/global/jwt/dto/AccessTokenPayload.java b/src/main/java/ditda/backend/global/jwt/dto/AccessTokenPayload.java new file mode 100644 index 0000000..03d367c --- /dev/null +++ b/src/main/java/ditda/backend/global/jwt/dto/AccessTokenPayload.java @@ -0,0 +1,11 @@ +package ditda.backend.global.jwt.dto; + +import ditda.backend.domain.user.entity.enums.UserRole; + +public record AccessTokenPayload( + + Long userId, + + UserRole role +) { +} diff --git a/src/main/java/ditda/backend/global/jwt/filter/JwtTokenFilter.java b/src/main/java/ditda/backend/global/jwt/filter/JwtTokenFilter.java index 1a617dc..543dccc 100644 --- a/src/main/java/ditda/backend/global/jwt/filter/JwtTokenFilter.java +++ b/src/main/java/ditda/backend/global/jwt/filter/JwtTokenFilter.java @@ -12,8 +12,8 @@ import org.springframework.web.filter.OncePerRequestFilter; import ditda.backend.global.jwt.JwtTokenProvider; +import ditda.backend.global.jwt.dto.AccessTokenPayload; import ditda.backend.global.jwt.exceptions.JwtErrorType; -import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; @@ -41,14 +41,13 @@ protected void doFilterInternal( if (StringUtils.hasText(token)) { try { - Claims claims = jwtTokenProvider.validateAccessToken(token); - Long memberId = Long.parseLong(claims.getSubject()); + AccessTokenPayload payload = jwtTokenProvider.getAccessTokenPayload(token); Authentication authentication = new UsernamePasswordAuthenticationToken( - memberId, + payload.userId(), null, - List.of(new SimpleGrantedAuthority("ROLE_USER")) + List.of(new SimpleGrantedAuthority("ROLE_" + payload.role().name())) ); SecurityContextHolder.getContext().setAuthentication(authentication); From 3242facea39759c0df752e35030f89398dca0e05 Mon Sep 17 00:00:00 2001 From: Jonghun Lim Date: Sun, 21 Jun 2026 21:01:04 +0900 Subject: [PATCH 101/102] =?UTF-8?q?Refactor(s3):=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20S3ErrorCode=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/facade/DesignerAuthFacade.java | 2 +- .../auth/mapper/AuthResponseMapper.java | 2 +- .../notification/DesignerSignupNotifier.java | 2 +- .../service/CommissionCreateFileService.java | 4 ++-- .../draft/mapper/DraftResponseMapper.java | 2 +- .../designer/service/PortfolioService.java | 6 ++--- .../apipayload/code/GeneralErrorCode.java | 8 ++----- .../global/s3/{ => config}/S3Properties.java | 2 +- .../s3/controller/S3FileController.java | 4 ++-- .../global/s3/{ => dto}/PresignedUpload.java | 2 +- .../s3/dto/response/PresignResponse.java | 2 +- .../global/s3/exception/S3ErrorCode.java | 24 +++++++++++++++++++ .../s3/{ => manager}/S3FileManager.java | 3 ++- .../S3PresignedUrlGenerator.java | 7 +++--- .../s3/{ => manager}/S3UploadManager.java | 3 ++- .../s3/{ => manager}/S3UrlResolver.java | 3 ++- .../s3/{ => service}/S3FileService.java | 19 ++++++++------- 17 files changed, 61 insertions(+), 34 deletions(-) rename src/main/java/ditda/backend/global/s3/{ => config}/S3Properties.java (94%) rename src/main/java/ditda/backend/global/s3/{ => dto}/PresignedUpload.java (64%) create mode 100644 src/main/java/ditda/backend/global/s3/exception/S3ErrorCode.java rename src/main/java/ditda/backend/global/s3/{ => manager}/S3FileManager.java (94%) rename src/main/java/ditda/backend/global/s3/{ => manager}/S3PresignedUrlGenerator.java (91%) rename src/main/java/ditda/backend/global/s3/{ => manager}/S3UploadManager.java (95%) rename src/main/java/ditda/backend/global/s3/{ => manager}/S3UrlResolver.java (85%) rename src/main/java/ditda/backend/global/s3/{ => service}/S3FileService.java (70%) diff --git a/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java index 090f1b3..7fc5e7e 100644 --- a/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java +++ b/src/main/java/ditda/backend/domain/auth/facade/DesignerAuthFacade.java @@ -12,7 +12,7 @@ import ditda.backend.domain.auth.service.EmailVerificationService; import ditda.backend.domain.designer.service.PortfolioService; import ditda.backend.domain.user.service.UserService; -import ditda.backend.global.s3.PresignedUpload; +import ditda.backend.global.s3.dto.PresignedUpload; import lombok.RequiredArgsConstructor; @Component diff --git a/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java b/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java index ea784f8..a360dd3 100644 --- a/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java +++ b/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java @@ -5,7 +5,7 @@ import ditda.backend.domain.auth.dto.AuthResult; import ditda.backend.domain.auth.dto.response.LoginResponse; import ditda.backend.domain.auth.dto.response.SignupResponse; -import ditda.backend.global.s3.S3UrlResolver; +import ditda.backend.global.s3.manager.S3UrlResolver; import lombok.RequiredArgsConstructor; @Component diff --git a/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupNotifier.java b/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupNotifier.java index 783e4cd..b2f50ef 100644 --- a/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupNotifier.java +++ b/src/main/java/ditda/backend/domain/auth/notification/DesignerSignupNotifier.java @@ -9,7 +9,7 @@ import ditda.backend.domain.auth.event.DesignerSignedUpEvent; import ditda.backend.global.config.AdminProperties; -import ditda.backend.global.s3.S3PresignedUrlGenerator; +import ditda.backend.global.s3.manager.S3PresignedUrlGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java index becb36b..fa94c6e 100644 --- a/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java +++ b/src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java @@ -14,10 +14,10 @@ import ditda.backend.domain.commission.core.exception.CommissionErrorCode; import ditda.backend.domain.commission.core.repository.CommissionFileRepository; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.S3FileService; -import ditda.backend.global.s3.S3UploadManager; import ditda.backend.global.s3.enums.BucketType; import ditda.backend.global.s3.enums.UploadTarget; +import ditda.backend.global.s3.manager.S3UploadManager; +import ditda.backend.global.s3.service.S3FileService; import lombok.RequiredArgsConstructor; @Service diff --git a/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java index 9faba95..5336dff 100644 --- a/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java +++ b/src/main/java/ditda/backend/domain/commission/draft/mapper/DraftResponseMapper.java @@ -13,7 +13,7 @@ import ditda.backend.domain.commission.draft.entity.CommissionDraftFile; import ditda.backend.domain.commission.draft.exception.DraftErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.S3PresignedUrlGenerator; +import ditda.backend.global.s3.manager.S3PresignedUrlGenerator; import lombok.RequiredArgsConstructor; @Component diff --git a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java index fe74caf..f092a54 100644 --- a/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java +++ b/src/main/java/ditda/backend/domain/designer/service/PortfolioService.java @@ -10,11 +10,11 @@ import ditda.backend.domain.designer.exception.DesignerErrorCode; import ditda.backend.domain.designer.repository.PortfolioRepository; import ditda.backend.global.apipayload.exception.GeneralException; -import ditda.backend.global.s3.PresignedUpload; -import ditda.backend.global.s3.S3FileService; -import ditda.backend.global.s3.S3UploadManager; +import ditda.backend.global.s3.dto.PresignedUpload; import ditda.backend.global.s3.enums.BucketType; import ditda.backend.global.s3.enums.UploadTarget; +import ditda.backend.global.s3.manager.S3UploadManager; +import ditda.backend.global.s3.service.S3FileService; import lombok.RequiredArgsConstructor; @Service diff --git a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java index 2ee65dd..ea64ff0 100644 --- a/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java +++ b/src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java @@ -20,10 +20,9 @@ public enum GeneralErrorCode implements BaseErrorCode { MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "REQ_400_01", "필수 파라미터가 누락되었습니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "REQ_400_02", "파라미터 형식이 잘못되었습니다."), INVALID_BODY_TYPE(HttpStatus.BAD_REQUEST, "REQ_400_03", "요청 본문의 형식이 잘못되었거나, 허용되지 않은 값이 포함되어 있습니다."), - INVALID_FILE(HttpStatus.BAD_REQUEST, "REQ_400_04", "유효하지 않거나 업로드되지 않은 파일입니다."), UNSUPPORTED_CONTENT_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "REQ_415_01", "지원하지 않는 Content-Type입니다."), - DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "REQ_409_01", "이미 존재하는 데이터입니다."), FILE_SIZE_EXCEEDED(HttpStatus.CONTENT_TOO_LARGE, "REQ_413_01", "파일 크기가 제한을 초과했습니다."), + DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "REQ_409_01", "이미 존재하는 데이터입니다."), // API/라우팅 에러 API_NOT_FOUND(HttpStatus.NOT_FOUND, "API_404_01", "존재하지 않는 API입니다."), @@ -38,10 +37,7 @@ public enum GeneralErrorCode implements BaseErrorCode { // 서버 내부 에러 INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SERVER_500_01", "서버 내부 오류입니다."), SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "SERVER_503_01", "서버가 일시적으로 불안정합니다."), - EXTERNAL_SERVICE_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT, "SERVER_504_01", "외부 서비스 응답 지연"), - - // S3 파일 에러 - FILE_URL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_500_01", "파일 URL 생성에 실패했습니다."); + EXTERNAL_SERVICE_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT, "SERVER_504_01", "외부 서비스 응답 지연"); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/ditda/backend/global/s3/S3Properties.java b/src/main/java/ditda/backend/global/s3/config/S3Properties.java similarity index 94% rename from src/main/java/ditda/backend/global/s3/S3Properties.java rename to src/main/java/ditda/backend/global/s3/config/S3Properties.java index 78d4163..d8e4470 100644 --- a/src/main/java/ditda/backend/global/s3/S3Properties.java +++ b/src/main/java/ditda/backend/global/s3/config/S3Properties.java @@ -1,4 +1,4 @@ -package ditda.backend.global.s3; +package ditda.backend.global.s3.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; diff --git a/src/main/java/ditda/backend/global/s3/controller/S3FileController.java b/src/main/java/ditda/backend/global/s3/controller/S3FileController.java index 268a514..5bbf52c 100644 --- a/src/main/java/ditda/backend/global/s3/controller/S3FileController.java +++ b/src/main/java/ditda/backend/global/s3/controller/S3FileController.java @@ -6,10 +6,10 @@ import org.springframework.web.bind.annotation.RestController; import ditda.backend.global.apipayload.response.ApiResponse; -import ditda.backend.global.s3.PresignedUpload; -import ditda.backend.global.s3.S3FileService; +import ditda.backend.global.s3.dto.PresignedUpload; import ditda.backend.global.s3.dto.request.PresignRequest; import ditda.backend.global.s3.dto.response.PresignResponse; +import ditda.backend.global.s3.service.S3FileService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; diff --git a/src/main/java/ditda/backend/global/s3/PresignedUpload.java b/src/main/java/ditda/backend/global/s3/dto/PresignedUpload.java similarity index 64% rename from src/main/java/ditda/backend/global/s3/PresignedUpload.java rename to src/main/java/ditda/backend/global/s3/dto/PresignedUpload.java index a45ac19..2dc64c6 100644 --- a/src/main/java/ditda/backend/global/s3/PresignedUpload.java +++ b/src/main/java/ditda/backend/global/s3/dto/PresignedUpload.java @@ -1,4 +1,4 @@ -package ditda.backend.global.s3; +package ditda.backend.global.s3.dto; public record PresignedUpload(String key, String presignedUrl) { } diff --git a/src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java b/src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java index ddf0c5d..b512046 100644 --- a/src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java +++ b/src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java @@ -1,6 +1,6 @@ package ditda.backend.global.s3.dto.response; -import ditda.backend.global.s3.PresignedUpload; +import ditda.backend.global.s3.dto.PresignedUpload; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "파일 업로드 presigned URL 발급 응답") diff --git a/src/main/java/ditda/backend/global/s3/exception/S3ErrorCode.java b/src/main/java/ditda/backend/global/s3/exception/S3ErrorCode.java new file mode 100644 index 0000000..4421410 --- /dev/null +++ b/src/main/java/ditda/backend/global/s3/exception/S3ErrorCode.java @@ -0,0 +1,24 @@ +package ditda.backend.global.s3.exception; + +import org.springframework.http.HttpStatus; + +import ditda.backend.global.apipayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum S3ErrorCode implements BaseErrorCode { + + // 업로드 파일 검증 에러 + INVALID_FILE(HttpStatus.BAD_REQUEST, "S3_400_01", "유효하지 않거나 업로드되지 않은 파일입니다."), + FILE_SIZE_EXCEEDED(HttpStatus.CONTENT_TOO_LARGE, "S3_413_01", "파일 크기가 제한을 초과했습니다."), + UNSUPPORTED_CONTENT_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "S3_415_01", "지원하지 않는 파일 형식입니다."), + + // presigned URL 발급 에러 + FILE_URL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "S3_500_01", "파일 URL 생성에 실패했습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/ditda/backend/global/s3/S3FileManager.java b/src/main/java/ditda/backend/global/s3/manager/S3FileManager.java similarity index 94% rename from src/main/java/ditda/backend/global/s3/S3FileManager.java rename to src/main/java/ditda/backend/global/s3/manager/S3FileManager.java index 26cb17c..5948126 100644 --- a/src/main/java/ditda/backend/global/s3/S3FileManager.java +++ b/src/main/java/ditda/backend/global/s3/manager/S3FileManager.java @@ -1,9 +1,10 @@ -package ditda.backend.global.s3; +package ditda.backend.global.s3.manager; import java.util.List; import org.springframework.stereotype.Component; +import ditda.backend.global.s3.config.S3Properties; import ditda.backend.global.s3.enums.BucketType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java b/src/main/java/ditda/backend/global/s3/manager/S3PresignedUrlGenerator.java similarity index 91% rename from src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java rename to src/main/java/ditda/backend/global/s3/manager/S3PresignedUrlGenerator.java index fd46fa9..0050c17 100644 --- a/src/main/java/ditda/backend/global/s3/S3PresignedUrlGenerator.java +++ b/src/main/java/ditda/backend/global/s3/manager/S3PresignedUrlGenerator.java @@ -1,12 +1,13 @@ -package ditda.backend.global.s3; +package ditda.backend.global.s3.manager; import java.time.Duration; import org.springframework.stereotype.Component; -import ditda.backend.global.apipayload.code.GeneralErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; +import ditda.backend.global.s3.config.S3Properties; import ditda.backend.global.s3.enums.BucketType; +import ditda.backend.global.s3.exception.S3ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import software.amazon.awssdk.core.exception.SdkException; @@ -46,7 +47,7 @@ public String generatePrivateGetUrl(String key, Duration ttl) { return presignedRequest.url().toString(); } catch (SdkException exception) { log.error("Failed to generate private presigned url. key={}", key, exception); - throw new GeneralException(GeneralErrorCode.FILE_URL_GENERATION_FAILED); + throw new GeneralException(S3ErrorCode.FILE_URL_GENERATION_FAILED); } } diff --git a/src/main/java/ditda/backend/global/s3/S3UploadManager.java b/src/main/java/ditda/backend/global/s3/manager/S3UploadManager.java similarity index 95% rename from src/main/java/ditda/backend/global/s3/S3UploadManager.java rename to src/main/java/ditda/backend/global/s3/manager/S3UploadManager.java index b02e513..cc53e41 100644 --- a/src/main/java/ditda/backend/global/s3/S3UploadManager.java +++ b/src/main/java/ditda/backend/global/s3/manager/S3UploadManager.java @@ -1,4 +1,4 @@ -package ditda.backend.global.s3; +package ditda.backend.global.s3.manager; import java.util.ArrayList; import java.util.List; @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; +import ditda.backend.global.s3.dto.PresignedUpload; import ditda.backend.global.s3.enums.BucketType; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ditda/backend/global/s3/S3UrlResolver.java b/src/main/java/ditda/backend/global/s3/manager/S3UrlResolver.java similarity index 85% rename from src/main/java/ditda/backend/global/s3/S3UrlResolver.java rename to src/main/java/ditda/backend/global/s3/manager/S3UrlResolver.java index f8d53d6..6bc5949 100644 --- a/src/main/java/ditda/backend/global/s3/S3UrlResolver.java +++ b/src/main/java/ditda/backend/global/s3/manager/S3UrlResolver.java @@ -1,7 +1,8 @@ -package ditda.backend.global.s3; +package ditda.backend.global.s3.manager; import org.springframework.stereotype.Component; +import ditda.backend.global.s3.config.S3Properties; import lombok.RequiredArgsConstructor; import software.amazon.awssdk.services.s3.S3Client; diff --git a/src/main/java/ditda/backend/global/s3/S3FileService.java b/src/main/java/ditda/backend/global/s3/service/S3FileService.java similarity index 70% rename from src/main/java/ditda/backend/global/s3/S3FileService.java rename to src/main/java/ditda/backend/global/s3/service/S3FileService.java index c13ff6c..6da572c 100644 --- a/src/main/java/ditda/backend/global/s3/S3FileService.java +++ b/src/main/java/ditda/backend/global/s3/service/S3FileService.java @@ -1,13 +1,16 @@ -package ditda.backend.global.s3; +package ditda.backend.global.s3.service; import java.util.List; import org.springframework.stereotype.Service; -import ditda.backend.global.apipayload.code.GeneralErrorCode; import ditda.backend.global.apipayload.exception.GeneralException; +import ditda.backend.global.s3.config.S3Properties; +import ditda.backend.global.s3.dto.PresignedUpload; import ditda.backend.global.s3.enums.S3ContentType; import ditda.backend.global.s3.enums.UploadTarget; +import ditda.backend.global.s3.exception.S3ErrorCode; +import ditda.backend.global.s3.manager.S3UploadManager; import lombok.RequiredArgsConstructor; @Service @@ -22,7 +25,7 @@ public PresignedUpload issuePresignedUpload(UploadTarget target, String contentT // 파일 형식 검증 S3ContentType type = S3ContentType.from(contentType); if (type == null || !target.getAllowed().contains(type)) { - throw new GeneralException(GeneralErrorCode.UNSUPPORTED_CONTENT_TYPE); + throw new GeneralException(S3ErrorCode.UNSUPPORTED_CONTENT_TYPE); } // temp key 및 presigned URL 발급 @@ -39,28 +42,28 @@ public void validateUploadedKeys(UploadTarget target, List keys) { // 빈 키 여부 검증 if (keys == null) { - throw new GeneralException(GeneralErrorCode.INVALID_FILE); + throw new GeneralException(S3ErrorCode.INVALID_FILE); } // 중복 키 검증 if (keys.size() != keys.stream().distinct().count()) { - throw new GeneralException(GeneralErrorCode.INVALID_FILE); + throw new GeneralException(S3ErrorCode.INVALID_FILE); } long maxBytes = s3Properties.getMaxFileSize().toBytes(); for (String key : keys) { // temp key 형식 검증 if (!s3UploadManager.isTempKey(key, target.getDir())) { - throw new GeneralException(GeneralErrorCode.INVALID_FILE); + throw new GeneralException(S3ErrorCode.INVALID_FILE); } // 파일 크기 검증 Long size = s3UploadManager.getObjectSize(target.getBucketType(), key); if (size == null) { - throw new GeneralException(GeneralErrorCode.INVALID_FILE); + throw new GeneralException(S3ErrorCode.INVALID_FILE); } if (size > maxBytes) { - throw new GeneralException(GeneralErrorCode.FILE_SIZE_EXCEEDED); + throw new GeneralException(S3ErrorCode.FILE_SIZE_EXCEEDED); } } } From 5997366e7dcb47da30a56da44881c3cacef21413 Mon Sep 17 00:00:00 2001 From: fervovita Date: Mon, 22 Jun 2026 00:17:14 +0900 Subject: [PATCH 102/102] =?UTF-8?q?Feat(auth):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8/=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=97=90=20userType=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ditda/backend/domain/auth/dto/AuthResult.java | 4 ++++ .../ditda/backend/domain/auth/dto/response/LoginResponse.java | 4 ++++ .../backend/domain/auth/dto/response/SignupResponse.java | 4 ++++ .../ditda/backend/domain/auth/mapper/AuthResponseMapper.java | 2 ++ .../java/ditda/backend/domain/auth/service/AuthService.java | 1 + .../backend/domain/auth/service/DesignerAuthService.java | 1 + .../backend/domain/auth/service/InstructorAuthService.java | 1 + 7 files changed, 17 insertions(+) diff --git a/src/main/java/ditda/backend/domain/auth/dto/AuthResult.java b/src/main/java/ditda/backend/domain/auth/dto/AuthResult.java index 8260ea0..1dcc2a8 100644 --- a/src/main/java/ditda/backend/domain/auth/dto/AuthResult.java +++ b/src/main/java/ditda/backend/domain/auth/dto/AuthResult.java @@ -1,9 +1,13 @@ package ditda.backend.domain.auth.dto; +import ditda.backend.domain.user.entity.enums.UserRole; + public record AuthResult( Long userId, + UserRole userRole, + String name, String profileImage, diff --git a/src/main/java/ditda/backend/domain/auth/dto/response/LoginResponse.java b/src/main/java/ditda/backend/domain/auth/dto/response/LoginResponse.java index cbac86d..1044852 100644 --- a/src/main/java/ditda/backend/domain/auth/dto/response/LoginResponse.java +++ b/src/main/java/ditda/backend/domain/auth/dto/response/LoginResponse.java @@ -1,5 +1,6 @@ package ditda.backend.domain.auth.dto.response; +import ditda.backend.domain.user.entity.enums.UserRole; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "로그인 응답") @@ -8,6 +9,9 @@ public record LoginResponse( @Schema(description = "로그인 유저 ID", example = "1") Long userId, + @Schema(description = "사용자 타입", example = "INSTRUCTOR") + UserRole userType, + @Schema(description = "사용자 이름", example = "홍길동") String name, diff --git a/src/main/java/ditda/backend/domain/auth/dto/response/SignupResponse.java b/src/main/java/ditda/backend/domain/auth/dto/response/SignupResponse.java index 0e8a54c..afa8118 100644 --- a/src/main/java/ditda/backend/domain/auth/dto/response/SignupResponse.java +++ b/src/main/java/ditda/backend/domain/auth/dto/response/SignupResponse.java @@ -1,5 +1,6 @@ package ditda.backend.domain.auth.dto.response; +import ditda.backend.domain.user.entity.enums.UserRole; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "회원가입 응답") @@ -8,6 +9,9 @@ public record SignupResponse( @Schema(description = "User ID") Long userId, + @Schema(description = "사용자 타입", example = "INSTRUCTOR") + UserRole userType, + @Schema(description = "사용자 이름", example = "홍길동") String name, diff --git a/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java b/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java index a360dd3..6ea96c7 100644 --- a/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java +++ b/src/main/java/ditda/backend/domain/auth/mapper/AuthResponseMapper.java @@ -17,6 +17,7 @@ public class AuthResponseMapper { public LoginResponse toLoginResponse(AuthResult result) { return new LoginResponse( result.userId(), + result.userRole(), result.name(), s3UrlResolver.toPublicS3Url(result.profileImage()), result.accessToken() @@ -26,6 +27,7 @@ public LoginResponse toLoginResponse(AuthResult result) { public SignupResponse toSignupResponse(AuthResult result) { return new SignupResponse( result.userId(), + result.userRole(), result.name(), s3UrlResolver.toPublicS3Url(result.profileImage()), result.accessToken() diff --git a/src/main/java/ditda/backend/domain/auth/service/AuthService.java b/src/main/java/ditda/backend/domain/auth/service/AuthService.java index 73d46ba..371abd4 100644 --- a/src/main/java/ditda/backend/domain/auth/service/AuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/AuthService.java @@ -70,6 +70,7 @@ public AuthResult login(LoginRequest request) { return new AuthResult( user.getId(), + user.getRole(), user.getName(), user.getProfileImage(), tokens.accessToken(), diff --git a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java index 8d11d6f..a31a1e4 100644 --- a/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/DesignerAuthService.java @@ -84,6 +84,7 @@ public AuthResult signup(DesignerSignupRequest request, List portfolioKe return new AuthResult( user.getId(), + user.getRole(), user.getName(), user.getProfileImage(), tokens.accessToken(), diff --git a/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java index ca4e3ae..f86ff4a7 100644 --- a/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java +++ b/src/main/java/ditda/backend/domain/auth/service/InstructorAuthService.java @@ -57,6 +57,7 @@ public AuthResult signup(InstructorSignupRequest request) { return new AuthResult( user.getId(), + user.getRole(), user.getName(), user.getProfileImage(), tokens.accessToken(),