Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -15,6 +16,7 @@
import kr.co.yourplanet.core.entity.settlement.SettlementPaymentStatus;
import kr.co.yourplanet.core.entity.settlement.SettlementStatus;
import kr.co.yourplanet.core.enums.StatusCode;
import kr.co.yourplanet.online.business.settlement.dto.ProjectSettlementDetailInfo;
import kr.co.yourplanet.online.business.settlement.dto.ProjectSettlementSummariesInfo;
import kr.co.yourplanet.online.business.settlement.service.ProjectSettlementQueryService;
import kr.co.yourplanet.online.common.ResponseForm;
Expand Down Expand Up @@ -44,11 +46,19 @@ public ResponseEntity<ResponseForm<Long>> getSettlementCountByStatus(
@Operation(summary = "프로젝트 정산 정보 목록 조회")
@GetMapping("/project")
public ResponseEntity<ResponseForm<ProjectSettlementSummariesInfo>> getProjectSettlements(
@AuthenticationPrincipal JwtPrincipal jwtPrincipal,
Pageable pageable
) {
ProjectSettlementSummariesInfo response = projectSettlementQueryService.getSummariesInfo(pageable);
return new ResponseEntity<>(new ResponseForm<>(StatusCode.OK, response), HttpStatus.OK);
}

@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "프로젝트 정산 상세 정보 조회")
@GetMapping("/project/{projectId}")
public ResponseEntity<ResponseForm<ProjectSettlementDetailInfo>> getProjectSettlementDetail(
@PathVariable Long projectId
) {
ProjectSettlementDetailInfo response = projectSettlementQueryService.getDetailInfo(projectId);
return new ResponseEntity<>(new ResponseForm<>(StatusCode.OK, response), HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package kr.co.yourplanet.online.business.settlement.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
public record ProjectSettlementDetailInfo(
@Schema(description = "브랜드 파트너 정보")
SponsorInfo sponsorInfo,

@Schema(description = "프로젝트 정보")
ProjectBasicInfo projectBasicInfo,

@Schema(description = "프로젝트 상세 정보")
ProjectSpecInfo projectSpecInfo,

@Schema(description = "정산 정보")
SettlementInfo settlementInfo
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import com.fasterxml.jackson.annotation.JsonFormat;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
public record ProjectSpecInfo(

@Schema(description = "작업 기한")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import kr.co.yourplanet.core.entity.member.Member;
import kr.co.yourplanet.core.entity.project.Project;
import kr.co.yourplanet.core.entity.project.ProjectHistory;
import kr.co.yourplanet.core.entity.settlement.ProjectSettlement;
import kr.co.yourplanet.core.entity.settlement.SettlementPaymentStatus;
import kr.co.yourplanet.core.entity.settlement.SettlementStatus;
import kr.co.yourplanet.core.entity.studio.Price;
import kr.co.yourplanet.core.enums.StatusCode;
import kr.co.yourplanet.core.model.PageInfo;
import kr.co.yourplanet.online.business.settlement.dto.ProjectBasicInfo;
import kr.co.yourplanet.online.business.settlement.dto.ProjectSettlementDetailInfo;
import kr.co.yourplanet.online.business.settlement.dto.ProjectSettlementSummariesInfo;
import kr.co.yourplanet.online.business.settlement.dto.ProjectSettlementSummaryInfo;
import kr.co.yourplanet.online.business.settlement.dto.ProjectSpecInfo;
import kr.co.yourplanet.online.business.settlement.dto.SettlementInfo;
import kr.co.yourplanet.online.business.settlement.dto.SponsorInfo;
import kr.co.yourplanet.online.business.settlement.repository.ProjectSettlementRepository;
Expand Down Expand Up @@ -59,31 +62,61 @@ public ProjectSettlementSummariesInfo getSummariesInfo(Pageable pageable) {
.build();
}

public ProjectSettlementDetailInfo getDetailInfo(long projectId) {
ProjectSettlement settlement = getByProjectId(projectId);

return ProjectSettlementDetailInfo.builder()
.projectBasicInfo(toProjectBasicInfo(settlement.getProject()))
.sponsorInfo(toSponsorInfo(settlement.getProject()))
.settlementInfo(toSettlementInfo(settlement))
.projectSpecInfo(toProjectSpecInfo(settlement.getProject()))
.build();
}

private ProjectSettlementSummaryInfo createSummaryInfo(ProjectSettlement settlement) {
Project project = settlement.getProject();
Member sponsor = project.getSponsor();
return ProjectSettlementSummaryInfo.builder()
.projectBasicInfo(toProjectBasicInfo(settlement.getProject()))
.sponsorInfo(toSponsorInfo(settlement.getProject()))
.settlementInfo(toSettlementInfo(settlement))
.build();
}

ProjectBasicInfo projectBasicInfo = ProjectBasicInfo.builder()
private ProjectBasicInfo toProjectBasicInfo(Project project) {
return ProjectBasicInfo.builder()
.orderCode(project.getOrderCode())
.orderTitle(project.getOrderTitle())
.build();
}

SponsorInfo sponsorInfo = SponsorInfo.builder()
private SponsorInfo toSponsorInfo(Project project) {
return SponsorInfo.builder()
.name(project.getBrandName())
.email(sponsor.getEmail())
.email(project.getSponsor().getEmail())
.build();
}

SettlementInfo settlementInfo = SettlementInfo.builder()
private SettlementInfo toSettlementInfo(ProjectSettlement settlement) {
return SettlementInfo.builder()
.paymentAmount(settlement.getPaymentAmount())
.settlementAmount(settlement.getSettlementAmount())
.paymentCompletedAt(settlement.getPaymentDate())
.contractCompletedAt(settlement.getContractDate())
.build();
}

return ProjectSettlementSummaryInfo.builder()
.sponsorInfo(sponsorInfo)
.projectBasicInfo(projectBasicInfo)
.settlementInfo(settlementInfo)
private ProjectSpecInfo toProjectSpecInfo(Project project) {
ProjectHistory history = project.getAcceptedHistory()
.orElseThrow(() -> new BusinessException(StatusCode.CONFLICT, "수락된 프로젝트가 아닙니다.", false));
Price price = project.getCreatorPrice();

return ProjectSpecInfo.builder()
.dueDate(history.getDueDate())
.cutCount(price.getCuts())
.additionalCutCount(history.getAdditionalPanelCount())
.finalCutCount(price.getCuts() + history.getAdditionalPanelCount())
.modificationCount(price.getModificationCount())
.additionalModificationCount(history.getAdditionalModificationCount())
.finalModificationCount(price.getModificationCount() + history.getAdditionalModificationCount())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
Expand All @@ -21,23 +22,9 @@
public class GlobalExceptionHandler {
private static final String WARN_LOG_TEMPLATE = "[WARN] {}: {}";

@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ResponseForm<Void>> handleBusinessException(BusinessException e) {
log.warn(WARN_LOG_TEMPLATE, e.getClass().getSimpleName(), e.getMessage(), e);

StatusCode statusCode = e.getStatusCode();
ResponseForm<Void> exceptionResponse = new ResponseForm<>(statusCode, e.getMessage(), false);
HttpStatus headerStatus;

try {
headerStatus = HttpStatus.valueOf(statusCode.getStatusCode());
} catch(IllegalArgumentException iae) {
headerStatus = HttpStatus.valueOf(500);
}

return new ResponseEntity<>(exceptionResponse, headerStatus);
}

/**
* 유효하지 않은 입력
*/
@ExceptionHandler(value = {
HttpMessageNotReadableException.class,
MissingServletRequestParameterException.class
Expand All @@ -47,7 +34,9 @@ protected ResponseEntity<ResponseForm<Void>> handleMissingServletRequestParamete
return new ResponseEntity<>(exceptionResponse, HttpStatus.BAD_REQUEST);
}

// Constraints Violation
/**
* 제약사항 위반
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ResponseForm<Map>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
Expand All @@ -64,6 +53,38 @@ protected ResponseEntity<ResponseForm<Void>> handleBindException(BindException e
return new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND);
}

/**
* 접근 제한
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ResponseForm<Void>> handleAccessDeniedException(AccessDeniedException ex) {
ResponseForm<Void> exceptionResponse = new ResponseForm<>(StatusCode.FORBIDDEN);
return new ResponseEntity<>(exceptionResponse, HttpStatus.FORBIDDEN);
}

/**
* 비즈니스 예외 처리
*/
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ResponseForm<Void>> handleBusinessException(BusinessException e) {
log.warn(WARN_LOG_TEMPLATE, e.getClass().getSimpleName(), e.getMessage(), e);

StatusCode statusCode = e.getStatusCode();
ResponseForm<Void> exceptionResponse = new ResponseForm<>(statusCode, e.getMessage(), false);
HttpStatus headerStatus;

try {
headerStatus = HttpStatus.valueOf(statusCode.getStatusCode());
} catch(IllegalArgumentException iae) {
headerStatus = HttpStatus.valueOf(500);
}

return new ResponseEntity<>(exceptionResponse, headerStatus);
}

/**
* 기본 예외 처리
*/
@ExceptionHandler(Exception.class)
protected ResponseEntity<ResponseForm<Void>> hanleException(Exception e) {
log.error("정의되지 않은 예외 발생: {} {}", e.getClass().getSimpleName(), e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- 멤버의 전화번호에 unique 제약 추가
ALTER TABLE member
ADD CONSTRAINT uq_member_tel UNIQUE (tel);
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,66 @@ void success_without_settlement_payment_status() throws Exception {
.andExpect(status().isOk());
}
}

@Nested
@DisplayName("프로젝트 정산 목록 조회 API")
class GetProjectSettlementSummaries {

private static final String PATH = "/settlement/project";

@DisplayName("[성공] 관리자는 프로젝트 정산 목록 조회에 성공한다.")
@Test
@WithMockJwtPrincipal(id = 0L, memberType = MemberType.ADMIN)
void success_admin() throws Exception {
mockMvc.perform(get(PATH)
.header(HeaderConstant.ACCESS_TOKEN, TokenStub.getMockAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.param("page", "0")
.param("size", "10"))
.andDo(print())
.andExpect(status().isOk());
}

@DisplayName("[실패] 관리자가 아니라면 프로젝트 정산 목록 조회에 실패한다.")
@Test
@WithMockJwtPrincipal(id = 1L, memberType = MemberType.CREATOR)
void fail_when_is_not_admin() throws Exception {
mockMvc.perform(get(PATH)
.header(HeaderConstant.ACCESS_TOKEN, TokenStub.getMockAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.param("page", "0")
.param("size", "10"))
.andDo(print())
.andExpect(status().isForbidden());
}
}

@Nested
@DisplayName("프로젝트 정산 상세 조회 API")
class GetProjectSettlementDetail {

private static final String PATH = "/settlement/project/{projectId}";

@DisplayName("[성공] 관리자는 프로젝트 정산 상세 조회에 성공한다.")
@Test
@WithMockJwtPrincipal(id = 0L, memberType = MemberType.ADMIN)
void success_admin() throws Exception {
mockMvc.perform(get(PATH, 1)
.header(HeaderConstant.ACCESS_TOKEN, TokenStub.getMockAccessToken())
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());
}

@DisplayName("[실패] 관리자가 아니라면 프로젝트 정산 목록 조회에 실패한다.")
@Test
@WithMockJwtPrincipal(id = 1L, memberType = MemberType.CREATOR)
void fail_when_is_not_admin() throws Exception {
mockMvc.perform(get(PATH, 1)
.header(HeaderConstant.ACCESS_TOKEN, TokenStub.getMockAccessToken())
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isForbidden());
}
}
}
27 changes: 24 additions & 3 deletions yp-online/src/test/resources/db/setup.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
-- MEMBER --

-- ID: 0
-- 관리자
INSERT INTO member (id,
email,
name,
tel,
password,
privacy_policy_agreed_time,
terms_of_service_agreed_time)
VALUES (0,
'[email protected]',
'관리자',
'01012345678',
'2CJn2ppvaleyrs3bZk+dP1Pe2DfoO5eKD+1h1rnI/kQ=',
NOW(),
NOW());

INSERT INTO member_salt (id, member_id, create_date, update_date, salt)
VALUES (0, 0, '2025-02-24 13:07:45.755085', '2025-02-24 13:07:45.755085',
'ici1vf8rgNYbn5s0n9ik3cFM492EQUIwlxcveOV9//k=');

-- ID: 1
-- 작가: 사업자
INSERT INTO member (id, email, password, name, gender_type, member_type, tel, birth_date,
Expand Down Expand Up @@ -282,7 +303,7 @@ VALUES (3, 4, 1, 'IN_PROGRESS', 3, 1, 2,
INSERT INTO project_contract (id, project_id, accept_date_time, due_date, contract_amount,
provider_company_name, provider_registration_number, provider_address,
provider_representative_name, provider_written_date_time)
VALUES (1, 3, '2025-03-18 12:00:00', '2025-03-20 18:00:00', 500000,
VALUES (1, 3, '2025-03-18 12:00:00', '2025-03-20', 500000,
'디자인 주식회사', '987-65-43210', '부산광역시 해운대구 센텀로 45',
'이영희', '2025-03-18 10:00:00');

Expand All @@ -293,7 +314,7 @@ INSERT INTO project_contract (id, project_id, accept_date_time, due_date, contra
provider_company_name, provider_registration_number, provider_address,
provider_representative_name,
provider_written_date_time, client_written_date_time, complete_date_time)
VALUES (2, 4, '2025-03-18 12:00:00', '2025-03-20 18:00:00', 750000,
VALUES (2, 4, '2025-03-18 12:00:00', '2025-03-20', 750000,
'ABC 기업', '123-45-67890', '서울특별시 강남구 테헤란로 123', '김철수',
'디자인 주식회사', '987-65-43210', '부산광역시 해운대구 센텀로 45', '이영희',
'2025-03-17 15:00:00', '2025-03-18 10:00:00', '2025-03-18 10:00:00');
Expand Down Expand Up @@ -348,7 +369,7 @@ VALUES (4,
750000,
675000,
75000,
'2025-03-18 10:00:00',
'2025-03-18 10:00:00',
'2025-03-30 09:00:00',
NULL,
'PAYMENT_PENDING',
Expand Down