11package org .bounswe .jobboardbackend .mentorship .service ;
22
3+ import com .google .cloud .storage .*;
34import lombok .RequiredArgsConstructor ;
45import org .bounswe .jobboardbackend .auth .model .User ;
56import org .bounswe .jobboardbackend .auth .repository .UserRepository ;
910import org .bounswe .jobboardbackend .mentorship .dto .*;
1011import org .bounswe .jobboardbackend .mentorship .model .*;
1112import org .bounswe .jobboardbackend .mentorship .repository .*;
13+ import org .springframework .beans .factory .annotation .Value ;
1214import org .springframework .messaging .simp .SimpMessagingTemplate ;
1315import org .springframework .security .access .AccessDeniedException ;
1416import org .springframework .security .core .Authentication ;
1517import org .springframework .stereotype .Service ;
1618import org .springframework .transaction .annotation .Transactional ;
19+ import org .springframework .web .multipart .MultipartFile ;
20+
21+ import java .io .IOException ;
22+ import java .net .URL ;
1723import java .time .LocalDateTime ;
1824import java .util .List ;
25+ import java .util .concurrent .TimeUnit ;
1926import java .util .stream .Collectors ;
2027import java .util .stream .Stream ;
2128
@@ -31,9 +38,101 @@ public class MentorshipServiceImpl implements MentorshipService {
3138 private final ChatService chatService ;
3239 private final SimpMessagingTemplate messagingTemplate ;
3340 private final ConversationRepository conversationRepository ;
41+ private final Storage storage = StorageOptions .getDefaultInstance ().getService ();
3442 // private final NotificationService notificationService; // (Future implementation)
3543
3644
45+ @ Value ("${app.gcs.bucket}" )
46+ private String gcsBucket ;
47+
48+ @ Value ("${app.gcs.public}" )
49+ private boolean gcsPublic ;
50+
51+ @ Value ("${app.gcs.publicBaseUrl}" )
52+ private String gcsPublicBaseUrl ;
53+
54+ @ Value ("${app.env}" )
55+ private String appEnv ;
56+
57+
58+ @ Override
59+ @ Transactional
60+ public ResumeFileResponseDTO uploadResumeFile (Long resumeReviewId , MultipartFile file ) {
61+ if (file == null || file .isEmpty ()) {
62+ throw new HandleException (ErrorCode .RESUME_FILE_REQUIRED , "Resume file is required" );
63+ }
64+
65+ String ct = file .getContentType ();
66+ if (!"application/pdf" .equalsIgnoreCase (ct )) {
67+ throw new HandleException (ErrorCode .RESUME_FILE_CONTENT_TYPE_INVALID , "Only PDF files are allowed" );
68+ }
69+
70+ ResumeReview review = resumeReviewRepository .findById (resumeReviewId )
71+ .orElseThrow (() -> new HandleException (ErrorCode .RESUME_REVIEW_NOT_FOUND , "Resume review not found" ));
72+
73+ if (review .getResumeUrl () != null ) {
74+ String oldObject = extractObjectNameFromUrl (review .getResumeUrl ());
75+ if (oldObject != null ) {
76+ deleteFromGcs (oldObject );
77+ }
78+ }
79+
80+ String objectName = buildObjectNameForResume (resumeReviewId , file .getOriginalFilename ());
81+ String url ;
82+ try {
83+ url = uploadToGcs (file .getBytes (), ct , objectName );
84+ } catch (IOException e ) {
85+ throw new HandleException (ErrorCode .RESUME_FILE_UPLOAD_FAILED , "Upload failed" , e );
86+ }
87+
88+ LocalDateTime now = LocalDateTime .now ();
89+ review .setResumeUrl (url );
90+ //review.setStatus(ReviewStatus.ACTIVE);
91+ review .setResumeUploadedAt (now );
92+
93+ return ResumeFileResponseDTO .builder ()
94+ .resumeReviewId (review .getId ())
95+ .fileUrl (review .getResumeUrl ())
96+ .reviewStatus (review .getStatus ())
97+ .uploadedAt (review .getResumeUploadedAt ())
98+ .build ();
99+ }
100+
101+
102+ @ Override
103+ @ Transactional (readOnly = true )
104+ public ResumeFileUrlDTO getResumeFileUrl (Long resumeReviewId ) {
105+ ResumeReview review = resumeReviewRepository .findById (resumeReviewId )
106+ .orElseThrow (() -> new HandleException (ErrorCode .RESUME_REVIEW_NOT_FOUND , "Resume review not found" ));
107+
108+ if (review .getResumeUrl () == null ) {
109+ throw new HandleException (ErrorCode .RESUME_FILE_NOT_FOUND , "Resume file not uploaded yet" );
110+ }
111+
112+ return ResumeFileUrlDTO .builder ()
113+ .fileUrl (review .getResumeUrl ())
114+ .build ();
115+ }
116+
117+
118+ @ Override
119+ @ Transactional (readOnly = true )
120+ public ResumeReviewDTO getResumeReview (Long resumeReviewId ) {
121+ ResumeReview review = resumeReviewRepository .findById (resumeReviewId )
122+ .orElseThrow (() -> new HandleException (ErrorCode .RESUME_REVIEW_NOT_FOUND , "Resume review not found" ));
123+
124+ return ResumeReviewDTO .builder ()
125+ .resumeReviewId (review .getId ())
126+ .fileUrl (review .getResumeUrl ())
127+ .reviewStatus (review .getStatus ())
128+ .feedback (review .getFeedback ())
129+ .build ();
130+ }
131+
132+
133+
134+
135+
37136 @ Override
38137 @ Transactional (readOnly = true )
39138 public List <MentorProfileDetailDTO > searchMentors () {
@@ -404,4 +503,55 @@ private MentorshipRequestDTO toMentorshipRequestDTO(MentorshipRequest request) {
404503 request .getCreatedAt ()
405504 );
406505 }
506+
507+ private String buildObjectNameForResume (Long resumeReviewId , String originalFilename ) {
508+ String ext = ".pdf" ;
509+ if (originalFilename != null && originalFilename .contains ("." )) {
510+ String candidate = originalFilename .substring (originalFilename .lastIndexOf ('.' ));
511+ if (candidate .equalsIgnoreCase (".pdf" )) {
512+ ext = candidate ;
513+ }
514+ }
515+ return appEnv + "/resumes/" + resumeReviewId + ext ;
516+ }
517+
518+ private String publicUrl (String objectName ) {
519+ return gcsPublicBaseUrl + "/" + gcsBucket + "/" + objectName ;
520+ }
521+
522+ private String extractObjectNameFromUrl (String url ) {
523+ String prefix = gcsPublicBaseUrl + "/" + gcsBucket + "/" ;
524+ if (url != null && url .startsWith (prefix )) {
525+ return url .substring (prefix .length ());
526+ }
527+ return null ;
528+ }
529+
530+ private String uploadToGcs (byte [] content , String contentType , String objectName ) {
531+ BlobInfo info = BlobInfo .newBuilder (gcsBucket , objectName )
532+ .setContentType (contentType != null ? contentType : "application/pdf" )
533+ .build ();
534+ storage .create (info , content );
535+
536+ if (gcsPublic ) {
537+ return publicUrl (objectName );
538+ } else {
539+ URL signed = storage .signUrl (
540+ BlobInfo .newBuilder (gcsBucket , objectName ).build (),
541+ 15 , TimeUnit .MINUTES ,
542+ Storage .SignUrlOption .withV4Signature (),
543+ Storage .SignUrlOption .httpMethod (HttpMethod .GET )
544+ );
545+ return signed .toString ();
546+ }
547+ }
548+
549+ private void deleteFromGcs (String objectName ) {
550+ if (objectName == null ) return ;
551+ try {
552+ storage .delete (gcsBucket , objectName );
553+ } catch (StorageException ignore ) {
554+ }
555+ }
556+
407557}
0 commit comments