Skip to content

Commit b77fa0e

Browse files
authored
[fix] 작가 신청 문서 업로드 로직 누락 해결 (#394)
1 parent c575701 commit b77fa0e

File tree

6 files changed

+487
-241
lines changed

6 files changed

+487
-241
lines changed

src/main/java/com/back/domain/artist/controller/ArtistController.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import jakarta.validation.Valid;
1313
import lombok.RequiredArgsConstructor;
1414
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.http.MediaType;
1516
import org.springframework.http.ResponseEntity;
1617
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1718
import org.springframework.web.bind.annotation.*;
19+
import org.springframework.web.multipart.MultipartFile;
1820

1921
import java.util.List;
2022

@@ -101,15 +103,24 @@ public ResponseEntity<RsData<List<ArtistProductResponse>>> getArtistProducts(
101103
/**
102104
* 작가 신청
103105
*/
104-
@PostMapping("/application")
105-
@Operation(summary = "작가 신청", description = "사용자가 작가로 신청합니다.")
106+
@PostMapping(value = "/application", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
107+
@Operation(
108+
summary = "작가 신청",
109+
description = "사용자가 작가 입점을 신청합니다. 필수 서류를 함께 업로드합니다."
110+
)
106111
public ResponseEntity<RsData<Long>> applyForArtist(
107112
@AuthenticationPrincipal CustomUserDetails userDetails,
108-
@Valid @RequestBody ArtistApplicationRequest request) {
113+
@Valid @RequestPart("application") ArtistApplicationRequest request,
114+
@RequestPart(value = "documents", required = true) List<MultipartFile> documents) {
109115

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

112-
Long applicationId = artistApplicationService.createApplication(userDetails.getUserId(), request);
119+
Long applicationId = artistApplicationService.createApplication(
120+
userDetails.getUserId(),
121+
request,
122+
documents
123+
);
113124

114125
return ResponseEntity.ok(
115126
RsData.of("200", "작가 신청 완료", applicationId)
Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
package com.back.domain.artist.dto.request;
22

3-
import com.back.domain.artist.entity.DocumentType;
4-
import com.back.global.s3.S3FileRequest;
53
import jakarta.validation.constraints.Email;
64
import jakarta.validation.constraints.NotBlank;
7-
import jakarta.validation.constraints.NotNull;
85
import jakarta.validation.constraints.Size;
96

10-
import java.util.List;
11-
import java.util.Map;
12-
137
/**
148
* 작가 신청서 생성 요청 DTO
159
*/
@@ -46,9 +40,6 @@ public record ArtistApplicationRequest(
4640
@NotBlank(message = "통신판매업 신고번호는 필수입니다.")
4741
String telecomSalesNumber,
4842

49-
@NotNull(message = "서류는 필수입니다.")
50-
Map<DocumentType, List<S3FileRequest>> documents,
51-
5243
// ==== 선택 필드 ==== //
5344
String businessName, // 상호명 (선택)
5445
String snsAccount, // SNS 계정 (선택)
@@ -59,53 +50,4 @@ public record ArtistApplicationRequest(
5950
String accountName // 예금주명 (선택)
6051

6152
) {
62-
63-
/**
64-
* 필수 서류가 모두 업르도되었는지 확인
65-
* 필수: 사업자등록증, 통신판매업신고증
66-
* 선택: 포트폴리오
67-
*/
68-
public boolean hasRequiredDocuments() {
69-
if (documents == null || documents.isEmpty()) {
70-
return false;
71-
}
72-
73-
// 사업자 등록증 필수
74-
List<S3FileRequest> businessLicense = documents.get(DocumentType.BUSINESS_LICENSE);
75-
if (businessLicense == null || businessLicense.isEmpty()) {
76-
return false;
77-
}
78-
79-
// 통신판매업 신고증 필수
80-
List<S3FileRequest> telecomCert = documents.get(DocumentType.TELECOM_CERTIFICATION);
81-
if (telecomCert == null || telecomCert.isEmpty()) {
82-
return false;
83-
}
84-
85-
return true;
86-
}
87-
88-
/**
89-
* 누락된 필수 서류 목록 반환
90-
*/
91-
public String getMissingRequiredDocuments() {
92-
if (documents == null || documents.isEmpty()) {
93-
return "사업자등록증, 통신판매업신고증";
94-
}
95-
96-
List<String> missing = new java.util.ArrayList<>();
97-
98-
if (documents.get(DocumentType.BUSINESS_LICENSE) == null ||
99-
documents.get(DocumentType.BUSINESS_LICENSE).isEmpty()) {
100-
missing.add("사업자등록증");
101-
}
102-
103-
if (documents.get(DocumentType.TELECOM_CERTIFICATION) == null ||
104-
documents.get(DocumentType.TELECOM_CERTIFICATION).isEmpty()) {
105-
missing.add("통신판매업신고증");
106-
}
107-
108-
return String.join(", ", missing);
109-
}
110-
11153
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.back.domain.artist.repository;
2+
3+
import com.back.domain.artist.entity.ArtistDocument;
4+
import com.back.domain.artist.entity.DocumentType;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
import java.util.List;
8+
9+
public interface ArtistDocumentRepository extends JpaRepository<ArtistDocument, Long> {
10+
11+
/**
12+
* 작가 신청서의 모든 서류 조회
13+
*/
14+
List<ArtistDocument> findByArtistApplicationId(Long applicationId);
15+
16+
/**
17+
* 작가 신청서의 특정 타입 서류 조회
18+
*/
19+
List<ArtistDocument> findByArtistApplicationIdAndDocumentType(
20+
Long applicationId,
21+
DocumentType documentType
22+
);
23+
24+
/**
25+
* 작가 신청서의 서류 존재 여부
26+
*/
27+
boolean existsByArtistApplicationId(Long applicationId);
28+
29+
/**
30+
* 작가 신청서의 서류 개수 조회
31+
*/
32+
long countByArtistApplicationId(Long applicationId);
33+
}

src/main/java/com/back/domain/artist/service/ArtistApplicationService.java

Lines changed: 101 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,26 @@
99
import com.back.domain.artist.entity.ArtistDocument;
1010
import com.back.domain.artist.entity.DocumentType;
1111
import com.back.domain.artist.repository.ArtistApplicationRepository;
12+
import com.back.domain.artist.repository.ArtistDocumentRepository;
1213
import com.back.domain.notification.entity.NotificationType;
1314
import com.back.domain.notification.service.NotificationService;
1415
import com.back.domain.user.entity.User;
1516
import com.back.domain.user.repository.UserRepository;
1617
import com.back.global.exception.ServiceException;
18+
import com.back.global.s3.FileType;
1719
import com.back.global.s3.S3FileRequest;
20+
import com.back.global.s3.S3Service;
21+
import com.back.global.s3.UploadResultResponse;
1822
import lombok.RequiredArgsConstructor;
1923
import lombok.extern.slf4j.Slf4j;
2024
import org.springframework.stereotype.Service;
2125
import org.springframework.transaction.annotation.Transactional;
26+
import org.springframework.web.multipart.MultipartFile;
2227

2328
import java.util.ArrayList;
2429
import java.util.List;
2530
import java.util.Map;
31+
import java.util.stream.Collectors;
2632

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

3642
private final ArtistApplicationRepository artistApplicationRepository;
43+
private final ArtistDocumentRepository artistDocumentRepository;
3744
private final UserRepository userRepository;
3845
private final NotificationService notificationService;
46+
private final S3Service s3Service;
3947

4048
/**
4149
* 작가 신청서 생성
4250
*/
4351
@Transactional
44-
public Long createApplication(Long userId, ArtistApplicationRequest request) {
52+
public Long createApplication(
53+
Long userId,
54+
ArtistApplicationRequest request,
55+
List<MultipartFile> documentFiles) {
56+
4557
// 1. 사용자 조회
4658
User user = userRepository.findById(userId)
4759
.orElseThrow(() -> new ServiceException("404", "사용자를 찾을 수 없습니다."));
4860

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

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

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

60-
// 5. 서류 정보 저장 (ArtistDocument 엔티티 생성)
61-
List<ArtistDocument> documents = createDocuments(savedApplication, request.documents());
62-
savedApplication.getDocuments().addAll(documents);
72+
// 5. S3에 서류 파일 업로드
73+
List<UploadResultResponse> uploadResults = s3Service.uploadFiles(
74+
documentFiles,
75+
"artist-documents/" + userId, // 경로: artist-documents/{userId}/
76+
documentFiles.stream()
77+
.map(file -> FileType.DOCUMENT) // 서류 타입
78+
.collect(Collectors.toList())
79+
);
80+
81+
// 6. 업로드된 파일 정보로 ArtistDocument 엔티티 생성
82+
List<ArtistDocument> documents = new ArrayList<>();
83+
for (int i = 0; i < uploadResults.size(); i++) {
84+
UploadResultResponse result = uploadResults.get(i);
85+
MultipartFile file = documentFiles.get(i);
86+
87+
// ✅ UploadResultResponse 구조에 맞게 수정
88+
ArtistDocument document = ArtistDocument.builder()
89+
.artistApplication(savedApplication)
90+
.documentType(determineDocumentType(file.getOriginalFilename()))
91+
.fileName(result.originalFileName()) // ✅ originalFileName
92+
.fileUrl(result.url()) // ✅ url
93+
.s3Key(result.s3Key()) // ✅ s3Key
94+
.build();
95+
96+
documents.add(document);
97+
}
98+
99+
// ✅ DB에 저장
100+
artistDocumentRepository.saveAll(documents);
101+
102+
// 7. 필수 서류 존재 여부 확인
103+
validateRequiredDocuments(documents);
63104

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

66-
// 6. 알림 발송 - 모든 관리자에게 작가 인증 신청 알림
108+
// 8. 알림 발송 - 모든 관리자에게
67109
List<User> admins = userRepository.findAllAdmins();
68110
for (User admin : admins) {
69111
notificationService.sendNotification(
70-
admin,
71-
NotificationType.ARTIST_VERIFICATION_REQUEST,
72-
user.getName() + "님이 작가 인증을 신청했습니다.",
73-
"/admin/artist-applications/" + savedApplication.getId()
112+
admin,
113+
NotificationType.ARTIST_VERIFICATION_REQUEST,
114+
user.getName() + "님이 작가 인증을 신청했습니다.",
115+
"/admin/artist-applications/" + savedApplication.getId()
74116
);
75117
}
76118

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

89-
List<ArtistApplication> applications = artistApplicationRepository.findByUserIdOrderByCreateDateDesc(userId);
130+
List<ArtistApplication> applications =
131+
artistApplicationRepository.findByUserIdOrderByCreateDateDesc(userId);
90132

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

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

111152
/**
112-
* 작가 신청 취소 (상태 변경)
153+
* 작가 신청 취소
113154
*/
114155
@Transactional
115156
public void cancelApplication(Long userId, Long applicationId) {
116-
// 1. 신청서 조회
117157
ArtistApplication application = getApplicationIfOwner(userId, applicationId);
118-
119-
// 2. 취소 가능한 상태인지 확인
120158
validateCancellable(application);
121-
122-
// 3. 신청서 상태를 취소로 변경
123159
application.cancel();
124160

125161
log.info("작가 신청 취소 완료: userId={}, applicationId={}", userId, applicationId);
@@ -233,4 +269,46 @@ public ArtistBusinessInfoResponse getBusinessInfo(Long userId) {
233269
application.getTelecomSalesNumber() // 통신판매업 신고 번호
234270
);
235271
}
272+
273+
/**
274+
* 파일명으로 서류 타입 판단
275+
*/
276+
private DocumentType determineDocumentType(String filename) {
277+
String lowerName = filename.toLowerCase();
278+
279+
if (lowerName.contains("business") || lowerName.contains("사업자")) {
280+
return DocumentType.BUSINESS_LICENSE;
281+
} else if (lowerName.contains("telecom") || lowerName.contains("통신판매") || lowerName.contains("신고증")) {
282+
return DocumentType.TELECOM_CERTIFICATION;
283+
} else if (lowerName.contains("portfolio") || lowerName.contains("포트폴리오")) {
284+
return DocumentType.PORTFOLIO;
285+
}
286+
287+
return DocumentType.OTHER;
288+
}
289+
290+
/**
291+
* 필수 서류 검증 (사업자등록증, 통신판매업신고증)
292+
*/
293+
private void validateRequiredDocuments(List<ArtistDocument> documents) {
294+
boolean hasBusinessLicense = documents.stream()
295+
.anyMatch(doc -> doc.getDocumentType() == DocumentType.BUSINESS_LICENSE);
296+
297+
boolean hasTelecomCertification = documents.stream()
298+
.anyMatch(doc -> doc.getDocumentType() == DocumentType.TELECOM_CERTIFICATION);
299+
300+
List<String> missingDocs = new ArrayList<>();
301+
if (!hasBusinessLicense) {
302+
missingDocs.add("사업자등록증");
303+
}
304+
if (!hasTelecomCertification) {
305+
missingDocs.add("통신판매업신고증");
306+
}
307+
308+
if (!missingDocs.isEmpty()) {
309+
throw new ServiceException("400",
310+
"필수 서류가 누락되었습니다: " + String.join(", ", missingDocs) +
311+
". 파일명에 '사업자' 또는 '통신판매'를 포함해주세요.");
312+
}
313+
}
236314
}

0 commit comments

Comments
 (0)