Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ditda.backend.domain.commission.core.controller;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
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.core.dto.response.CommissionListResponse;
import ditda.backend.domain.commission.core.facade.DesignerCommissionFacade;
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/designers/commissions")
@RequiredArgsConstructor
@Tag(name = "Designer", description = "디자이너 모집 중 외주 목록 API")
public class DesignerCommissionController {

private final DesignerCommissionFacade designerCommissionFacade;

@Operation(summary = "모집 중 외주 목록 조회", description = "**[외주 찾기]** 디자이너가 모집 중인 외주 목록을 조회합니다.")
@GetMapping
public ApiResponse<CommissionListResponse> getRecruitingCommissions(
@AuthenticationPrincipal Long designerId,
@PageableDefault Pageable pageable
) {

// 지원 마감일 기준 오름차순 정렬
Pageable fixedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.asc("applicationDeadline"), Sort.Order.asc("id"))
);

CommissionListResponse response = designerCommissionFacade.getRecruitingCommissionList(
designerId,
fixedPageable
);

return ApiResponse.onSuccess("디자이너 모집 중 외주 목록 조회 성공", response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ditda.backend.domain.commission.core.dto;

public record PriceInfo(
int baseAmount,
int maxAmount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ditda.backend.domain.commission.core.dto.response;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;

import org.springframework.data.domain.Page;

import ditda.backend.domain.commission.core.dto.PriceInfo;
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 CommissionListResponse(

@Schema(description = "외주 목록")
List<CommissionResponse> commissions,

@Schema(description = "현재 페이지 번호", example = "0")
int page,

@Schema(description = "페이지 크기", example = "10")
int size,

@Schema(description = "전체 데이터 수", example = "23")
long totalElements,

@Schema(description = "전체 페이지 수", example = "3")
int totalPages
) {

public static CommissionListResponse from(Page<Commission> page, Map<Long, PriceInfo> priceInfos) {
List<CommissionResponse> items = page.getContent().stream()
.map(c -> CommissionResponse.from(c, priceInfos.get(c.getId())))
.toList();

return new CommissionListResponse(
items,
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages()
);
}

@Schema(description = "외주 정보")
public record CommissionResponse(

@Schema(description = "외주 ID", example = "1")
Long commissionId,

@Schema(description = "지원 마감일", example = "2026-05-05")
LocalDate applicationDeadline,

@Schema(description = "외주 카테고리", example = "FLYER_TEXTBOOK_COVER_INNER")
CategoryType category,

@Schema(description = "제목", example = "해커스톡 왕초보 - 홍길동")
String title,

@Schema(description = "기본금 (원)", example = "40000")
int baseAmount,

@Schema(description = "최대금액 (원)", example = "210000")
int maxAmount
) {

private static CommissionResponse from(Commission commission, PriceInfo priceInfo) {
return new CommissionResponse(
commission.getId(),
commission.getApplicationDeadline(),
commission.getCategoryType(),
commission.getTitle(),
priceInfo.baseAmount(),
priceInfo.maxAmount()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ditda.backend.domain.commission.core.entity.enums;

import java.util.Arrays;

import ditda.backend.domain.designer.entity.enums.DesignerLevel;
import ditda.backend.domain.designer.exception.DesignerErrorCode;
import ditda.backend.global.apipayload.exception.GeneralException;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum CommissionAmountPolicy {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

#60CommissionPricePolicy를 이미 구현해 놓았는데,
카테고리에 따라서 가격이 달라지고도 하고 환불/정산 등의 계산 로직이 추가로 들어와야 하는데 이를 enum으로만 관리하기에는 확장성 면에서 무리가 있을 것 같습니다.

혹시 CommissionPricePolicy로 통일해서 구현해주실 수 있을까요...?

그렇게 되면 아마 CommissionMapper 없이 단순 from/of 정적 메소드로 변환이 가능할 것 같습니다..!

@Jong0128 Jong0128 Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

지금 CommissionPricePolicy@component로 등록되어 있어서 등록 없이 from 정적 메소드로 변환은 힘들꺼같습니다..!

그래서 CommissionPricePolicyCommissionMapper에 주입해서 금액 계산을 위임하는 방향으로 통일하려고 합니다. 이렇게 되면 기존 CommissionAmountPolicy enum은 제거되고, 가격 로직은 CommissionPricePolicy로 통일할 수 있을꺼같은데 괜찮을까요?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CommissionMapper는 편의에 따라서 유지하셔도 무방할 것 같습니다.
다만, 비즈니스 로직이 포함되어 있는 CommissionPricePolicy를 mapper에서 호출하게 된다면, mapper는 단순 변환 역할 이상의 구조를 가지게 됩니다.

차라리 Service/Facade에서 계산해서 넘기는 구조가 어떨까요..?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@fervovita 해당 부분 CommissionMapper 제거 후 CommissionPricePolicy을 통해 계산하는걸로 수정했습니다!
확인해주시고 다른 문제나 변경사항이 필요하면 편하게 말씀해주세요!

만약 문제가 따로 없다면 해당 부분 merge 후 #65 해당 PR도 수정하겠습니다 😄

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CommissionAmountPolicy가 여전히 남아 있는데 이 부분만 삭제 부탁드립니다!
아마 그러면 DesignerErrorCode에 새로 추가한 에러도 불필요 해질 것 같아요!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

어 해당부분 못보고 머지했네요 ㅠㅠ;;;
해당 부분 #65 여기서 수정해서 올리겠습니다..!


LEVEL_1(DesignerLevel.LEVEL_1, 40_000),
LEVEL_2(DesignerLevel.LEVEL_2, 50_000),
LEVEL_3(DesignerLevel.LEVEL_3, 60_000);

private static final int MAX_AMOUNT_BONUS = 150_000;

private final DesignerLevel level;
private final int baseAmount;

public int getMaxAmount() {
return baseAmount + MAX_AMOUNT_BONUS;
}

public static CommissionAmountPolicy from(DesignerLevel level) {
return Arrays.stream(values())
.filter(policy -> policy.level == level)
.findFirst()
.orElseThrow(() -> new GeneralException(DesignerErrorCode.DESIGNER_LEVEL_POLICY_MISSING));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package ditda.backend.domain.commission.core.facade;

import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import ditda.backend.domain.commission.core.dto.PriceInfo;
import ditda.backend.domain.commission.core.dto.response.CommissionListResponse;
import ditda.backend.domain.commission.core.entity.Commission;
import ditda.backend.domain.commission.core.policy.CommissionPricePolicy;
import ditda.backend.domain.commission.core.service.DesignerCommissionService;
import ditda.backend.domain.designer.entity.Designer;
import ditda.backend.domain.designer.entity.enums.DesignerLevel;
import ditda.backend.domain.designer.service.DesignerService;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class DesignerCommissionFacade {

private final DesignerCommissionService designerCommissionService;
private final DesignerService designerService;
private final CommissionPricePolicy commissionPricePolicy;

@Transactional(readOnly = true)
public CommissionListResponse getRecruitingCommissionList(Long designerId, Pageable pageable) {

Designer designer = designerService.getById(designerId);
DesignerLevel level = designer.getLevel();
Page<Commission> commissions = designerCommissionService.getRecruitingCommissions(pageable);

Map<Long, PriceInfo> priceInfos = commissions.getContent().stream()
.collect(Collectors.toMap(
Commission::getId,
c -> commissionPricePolicy.getPriceInfo(c.getCategoryType(), level)
));

return CommissionListResponse.from(commissions, priceInfos);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.springframework.stereotype.Component;

import ditda.backend.domain.commission.core.dto.PriceInfo;
import ditda.backend.domain.commission.core.entity.enums.CategoryType;
import ditda.backend.domain.designer.entity.enums.DesignerLevel;

Expand Down Expand Up @@ -42,6 +43,14 @@ public int calculateFinalPayout(CategoryType category, DesignerLevel level) {
return calculateDraftSubmissionReward(category, level) + calculateFinalSelectionIncentive(category);
}

// 기본금 및 최종금액 policy가 만들어서 반환
public PriceInfo getPriceInfo(CategoryType category, DesignerLevel level) {
return new PriceInfo(
calculateDraftSubmissionReward(category, level),
calculateFinalPayout(category, level)
);
}

private int textbookCoverInnerReward(DesignerLevel level) {
return switch (level) {
case LEVEL_1 -> 40_000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.util.List;
import java.util.Optional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand Down Expand Up @@ -70,4 +72,6 @@ List<Commission> findByStatusInAndFinalDeadlineBefore(
+ "LEFT JOIN FETCH d.user "
+ "WHERE c.id = :commissionId")
Optional<Commission> findWithInstructorAndAssignedDesignerById(@Param("commissionId") Long commissionId);

Page<Commission> findByStatus(CommissionStatus status, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ditda.backend.domain.commission.core.service;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.entity.enums.CommissionStatus;
import ditda.backend.domain.commission.core.repository.CommissionRepository;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class DesignerCommissionService {

private final CommissionRepository commissionRepository;

// 모집 중 외주 목록 조회
@Transactional(readOnly = true)
public Page<Commission> getRecruitingCommissions(Pageable pageable) {
return commissionRepository.findByStatus(
CommissionStatus.RECRUITING,
pageable
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
@AllArgsConstructor
public enum DesignerErrorCode implements BaseErrorCode {

DESIGNER_NOT_FOUND(HttpStatus.NOT_FOUND, "DESIGNER_404_01", "존재하지 않는 디자이너입니다."),
DESIGNER_LEVEL_POLICY_MISSING(HttpStatus.INTERNAL_SERVER_ERROR, "DESIGNER_500_01", "존재하지 않는 디자이너 레벨 정책입니다."),
PORTFOLIO_FILE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "DESIGNER_400_01", "포트폴리오 파일은 최대 3개까지 업로드 가능합니다.");

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ditda.backend.domain.designer.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import ditda.backend.domain.designer.entity.Designer;
import ditda.backend.domain.designer.exception.DesignerErrorCode;
import ditda.backend.domain.designer.repository.DesignerRepository;
import ditda.backend.global.apipayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class DesignerService {

private final DesignerRepository designerRepository;

@Transactional(readOnly = true)
public Designer getById(Long designerId) {
return designerRepository.findById(designerId)
.orElseThrow(() -> new GeneralException(DesignerErrorCode.DESIGNER_NOT_FOUND));
}
}