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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

Expand Down Expand Up @@ -101,15 +103,24 @@ public ResponseEntity<RsData<List<ArtistProductResponse>>> getArtistProducts(
/**
* 작가 신청
*/
@PostMapping("/application")
@Operation(summary = "작가 신청", description = "사용자가 작가로 신청합니다.")
@PostMapping(value = "/application", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "작가 신청",
description = "사용자가 작가 입점을 신청합니다. 필수 서류를 함께 업로드합니다."
)
public ResponseEntity<RsData<Long>> applyForArtist(
@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @RequestBody ArtistApplicationRequest request) {
@Valid @RequestPart("application") ArtistApplicationRequest request,
@RequestPart(value = "documents", required = true) List<MultipartFile> documents) {

log.info("작가 신청 - userId: {}", userDetails.getUserId());
log.info("작가 신청 - userId: {}, 서류 개수: {}",
userDetails.getUserId(), documents.size());

Long applicationId = artistApplicationService.createApplication(userDetails.getUserId(), request);
Long applicationId = artistApplicationService.createApplication(
userDetails.getUserId(),
request,
documents
);

return ResponseEntity.ok(
RsData.of("200", "작가 신청 완료", applicationId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package com.back.domain.artist.dto.request;

import com.back.domain.artist.entity.DocumentType;
import com.back.global.s3.S3FileRequest;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.util.List;
import java.util.Map;

/**
* 작가 신청서 생성 요청 DTO
*/
Expand Down Expand Up @@ -46,9 +40,6 @@ public record ArtistApplicationRequest(
@NotBlank(message = "통신판매업 신고번호는 필수입니다.")
String telecomSalesNumber,

@NotNull(message = "서류는 필수입니다.")
Map<DocumentType, List<S3FileRequest>> documents,

// ==== 선택 필드 ==== //
String businessName, // 상호명 (선택)
String snsAccount, // SNS 계정 (선택)
Expand All @@ -59,53 +50,4 @@ public record ArtistApplicationRequest(
String accountName // 예금주명 (선택)

) {

/**
* 필수 서류가 모두 업르도되었는지 확인
* 필수: 사업자등록증, 통신판매업신고증
* 선택: 포트폴리오
*/
public boolean hasRequiredDocuments() {
if (documents == null || documents.isEmpty()) {
return false;
}

// 사업자 등록증 필수
List<S3FileRequest> businessLicense = documents.get(DocumentType.BUSINESS_LICENSE);
if (businessLicense == null || businessLicense.isEmpty()) {
return false;
}

// 통신판매업 신고증 필수
List<S3FileRequest> telecomCert = documents.get(DocumentType.TELECOM_CERTIFICATION);
if (telecomCert == null || telecomCert.isEmpty()) {
return false;
}

return true;
}

/**
* 누락된 필수 서류 목록 반환
*/
public String getMissingRequiredDocuments() {
if (documents == null || documents.isEmpty()) {
return "사업자등록증, 통신판매업신고증";
}

List<String> missing = new java.util.ArrayList<>();

if (documents.get(DocumentType.BUSINESS_LICENSE) == null ||
documents.get(DocumentType.BUSINESS_LICENSE).isEmpty()) {
missing.add("사업자등록증");
}

if (documents.get(DocumentType.TELECOM_CERTIFICATION) == null ||
documents.get(DocumentType.TELECOM_CERTIFICATION).isEmpty()) {
missing.add("통신판매업신고증");
}

return String.join(", ", missing);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.domain.artist.repository;

import com.back.domain.artist.entity.ArtistDocument;
import com.back.domain.artist.entity.DocumentType;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ArtistDocumentRepository extends JpaRepository<ArtistDocument, Long> {

/**
* 작가 신청서의 모든 서류 조회
*/
List<ArtistDocument> findByArtistApplicationId(Long applicationId);

/**
* 작가 신청서의 특정 타입 서류 조회
*/
List<ArtistDocument> findByArtistApplicationIdAndDocumentType(
Long applicationId,
DocumentType documentType
);

/**
* 작가 신청서의 서류 존재 여부
*/
boolean existsByArtistApplicationId(Long applicationId);

/**
* 작가 신청서의 서류 개수 조회
*/
long countByArtistApplicationId(Long applicationId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@
import com.back.domain.artist.entity.ArtistDocument;
import com.back.domain.artist.entity.DocumentType;
import com.back.domain.artist.repository.ArtistApplicationRepository;
import com.back.domain.artist.repository.ArtistDocumentRepository;
import com.back.domain.notification.entity.NotificationType;
import com.back.domain.notification.service.NotificationService;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.ServiceException;
import com.back.global.s3.FileType;
import com.back.global.s3.S3FileRequest;
import com.back.global.s3.S3Service;
import com.back.global.s3.UploadResultResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

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

/**
* 사용자용 작가 신청 서비스
Expand All @@ -34,43 +40,79 @@
public class ArtistApplicationService {

private final ArtistApplicationRepository artistApplicationRepository;
private final ArtistDocumentRepository artistDocumentRepository;
private final UserRepository userRepository;
private final NotificationService notificationService;
private final S3Service s3Service;

/**
* 작가 신청서 생성
*/
@Transactional
public Long createApplication(Long userId, ArtistApplicationRequest request) {
public Long createApplication(
Long userId,
ArtistApplicationRequest request,
List<MultipartFile> documentFiles) {

// 1. 사용자 조회
User user = userRepository.findById(userId)
.orElseThrow(() -> new ServiceException("404", "사용자를 찾을 수 없습니다."));

// 2. 중복 신청 검증 (심사 대기 중인 신청서가 있는지)
// 2. 중복 신청 검증
validateDuplicateApplication(userId);

// 3. 필수 서류 검증
if (!request.hasRequiredDocuments()) {
throw new ServiceException("400", "필수 서류가 누락되었습니다." + request.getMissingRequiredDocuments());
// 3. 필수 서류 파일 검증
if (documentFiles == null || documentFiles.size() < 2) {
throw new ServiceException("400", "필수 서류 파일이 누락되었습니다. (최소 2개: 사업자등록증, 통신판매업신고증)");
}

// 4. 작가 신청서 엔티티 생성 및 저장
ArtistApplication savedApplication = createAndSaveApplication(user, request);

// 5. 서류 정보 저장 (ArtistDocument 엔티티 생성)
List<ArtistDocument> documents = createDocuments(savedApplication, request.documents());
savedApplication.getDocuments().addAll(documents);
// 5. S3에 서류 파일 업로드
List<UploadResultResponse> uploadResults = s3Service.uploadFiles(
documentFiles,
"artist-documents/" + userId, // 경로: artist-documents/{userId}/
documentFiles.stream()
.map(file -> FileType.DOCUMENT) // 서류 타입
.collect(Collectors.toList())
);

// 6. 업로드된 파일 정보로 ArtistDocument 엔티티 생성
List<ArtistDocument> documents = new ArrayList<>();
for (int i = 0; i < uploadResults.size(); i++) {
UploadResultResponse result = uploadResults.get(i);
MultipartFile file = documentFiles.get(i);

// ✅ UploadResultResponse 구조에 맞게 수정
ArtistDocument document = ArtistDocument.builder()
.artistApplication(savedApplication)
.documentType(determineDocumentType(file.getOriginalFilename()))
.fileName(result.originalFileName()) // ✅ originalFileName
.fileUrl(result.url()) // ✅ url
.s3Key(result.s3Key()) // ✅ s3Key
.build();

documents.add(document);
}

// ✅ DB에 저장
artistDocumentRepository.saveAll(documents);

// 7. 필수 서류 존재 여부 확인
validateRequiredDocuments(documents);

log.info("작가 신청서 생성 완료: userId={}, applicationId={}", userId, savedApplication.getId());
log.info("작가 신청서 생성 완료: userId={}, applicationId={}, 업로드된 서류: {}",
userId, savedApplication.getId(), documents.size());

// 6. 알림 발송 - 모든 관리자에게 작가 인증 신청 알림
// 8. 알림 발송 - 모든 관리자에게
List<User> admins = userRepository.findAllAdmins();
for (User admin : admins) {
notificationService.sendNotification(
admin,
NotificationType.ARTIST_VERIFICATION_REQUEST,
user.getName() + "님이 작가 인증을 신청했습니다.",
"/admin/artist-applications/" + savedApplication.getId()
admin,
NotificationType.ARTIST_VERIFICATION_REQUEST,
user.getName() + "님이 작가 인증을 신청했습니다.",
"/admin/artist-applications/" + savedApplication.getId()
);
}

Expand All @@ -81,12 +123,12 @@ public Long createApplication(Long userId, ArtistApplicationRequest request) {
* 내 신청서 목록 조회
*/
public List<ArtistApplicationSimpleResponse> getMyApplications(Long userId) {
// 사용자 존재 여부 확인
if (!userRepository.existsById(userId)) {
throw new ServiceException("404", "사용자를 찾을 수 없습니다.");
}

List<ArtistApplication> applications = artistApplicationRepository.findByUserIdOrderByCreateDateDesc(userId);
List<ArtistApplication> applications =
artistApplicationRepository.findByUserIdOrderByCreateDateDesc(userId);

return applications.stream()
.map(ArtistApplicationSimpleResponse::from)
Expand All @@ -100,7 +142,6 @@ public ArtistApplicationResponse getApplicationById(Long userId, Long applicatio
ArtistApplication application = artistApplicationRepository.findById(applicationId)
.orElseThrow(() -> new ServiceException("404", "신청서를 찾을 수 없습니다."));

// 본인 신청서인지 확인
if (!application.getUser().getId().equals(userId)) {
throw new ServiceException("403", "본인의 신청서만 조회할 수 있습니다.");
}
Expand All @@ -109,17 +150,12 @@ public ArtistApplicationResponse getApplicationById(Long userId, Long applicatio
}

/**
* 작가 신청 취소 (상태 변경)
* 작가 신청 취소
*/
@Transactional
public void cancelApplication(Long userId, Long applicationId) {
// 1. 신청서 조회
ArtistApplication application = getApplicationIfOwner(userId, applicationId);

// 2. 취소 가능한 상태인지 확인
validateCancellable(application);

// 3. 신청서 상태를 취소로 변경
application.cancel();

log.info("작가 신청 취소 완료: userId={}, applicationId={}", userId, applicationId);
Expand Down Expand Up @@ -233,4 +269,46 @@ public ArtistBusinessInfoResponse getBusinessInfo(Long userId) {
application.getTelecomSalesNumber() // 통신판매업 신고 번호
);
}

/**
* 파일명으로 서류 타입 판단
*/
private DocumentType determineDocumentType(String filename) {
String lowerName = filename.toLowerCase();

if (lowerName.contains("business") || lowerName.contains("사업자")) {
return DocumentType.BUSINESS_LICENSE;
} else if (lowerName.contains("telecom") || lowerName.contains("통신판매") || lowerName.contains("신고증")) {
return DocumentType.TELECOM_CERTIFICATION;
} else if (lowerName.contains("portfolio") || lowerName.contains("포트폴리오")) {
return DocumentType.PORTFOLIO;
}

return DocumentType.OTHER;
}

/**
* 필수 서류 검증 (사업자등록증, 통신판매업신고증)
*/
private void validateRequiredDocuments(List<ArtistDocument> documents) {
boolean hasBusinessLicense = documents.stream()
.anyMatch(doc -> doc.getDocumentType() == DocumentType.BUSINESS_LICENSE);

boolean hasTelecomCertification = documents.stream()
.anyMatch(doc -> doc.getDocumentType() == DocumentType.TELECOM_CERTIFICATION);

List<String> missingDocs = new ArrayList<>();
if (!hasBusinessLicense) {
missingDocs.add("사업자등록증");
}
if (!hasTelecomCertification) {
missingDocs.add("통신판매업신고증");
}

if (!missingDocs.isEmpty()) {
throw new ServiceException("400",
"필수 서류가 누락되었습니다: " + String.join(", ", missingDocs) +
". 파일명에 '사업자' 또는 '통신판매'를 포함해주세요.");
}
}
}
Loading