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
@@ -1,13 +1,12 @@
// src/main/java/com/chaineeproject/chainee/controller/JobApplicationController.java
package com.chaineeproject.chainee.controller;

import com.chaineeproject.chainee.dto.JobApplicationRequest;
import com.chaineeproject.chainee.dto.JobApplicationApplyRequest;
import com.chaineeproject.chainee.dto.JobApplicationResponse;
import com.chaineeproject.chainee.security.SecurityUtils;
import com.chaineeproject.chainee.service.JobApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -23,17 +22,19 @@ public class JobApplicationController {
private final JobApplicationService jobApplicationService;

@PostMapping("/{postId}/apply")
@Operation(summary = "채용 공고 지원", description = "요청 사용자의 JWT uid를 지원자로 하여 지정한 공고에 지원합니다.")
@Operation(summary = "채용 공고 지원",
description = "요청 사용자의 JWT uid를 지원자로 하여 지정한 공고에 지원합니다.")
public ResponseEntity<JobApplicationResponse> applyToJob(
@AuthenticationPrincipal Jwt jwt,
@Parameter(description = "지원할 채용 공고 ID", example = "1")
@PathVariable Long postId,
@Valid @RequestBody JobApplicationRequest request
@AuthenticationPrincipal Jwt jwt,
@RequestBody JobApplicationApplyRequest request
) {
Long applicantId = com.chaineeproject.chainee.security.SecurityUtils.uidOrNull(jwt);
if (applicantId == null) return ResponseEntity.status(401).build();

jobApplicationService.applyToJob(postId, applicantId, request.resumeId());
Long currentUserId = SecurityUtils.uidOrNull(jwt);
if (currentUserId == null) {
return ResponseEntity.status(401).body(new JobApplicationResponse(false, "UNAUTHORIZED"));
}
jobApplicationService.applyToJob(postId, currentUserId, request.getResumeId());
return ResponseEntity.ok(new JobApplicationResponse(true, "APPLICATION_SUBMITTED"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.chaineeproject.chainee.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "채용 공고 지원 요청 DTO(JWT uid 사용)")
public class JobApplicationApplyRequest {

@Schema(description = "지원자가 선택한 이력서 ID", example = "42", required = true)
private Long resumeId;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// src/main/java/com/chaineeproject/chainee/service/JobApplicationService.java
package com.chaineeproject.chainee.service;

import com.chaineeproject.chainee.entity.*;
import com.chaineeproject.chainee.event.JobApplicationCreatedEvent;
import com.chaineeproject.chainee.entity.enums.NotificationType;
import com.chaineeproject.chainee.exception.ApplicationException;
import com.chaineeproject.chainee.exception.ErrorCode;
import com.chaineeproject.chainee.repository.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -23,8 +21,11 @@ public class JobApplicationService {
private final JobPostRepository jobPostRepository;
private final ResumeRepository resumeRepository;
private final JobApplicationRepository jobApplicationRepository;
private final ApplicationEventPublisher eventPublisher;
private final NotificationRepository notificationRepository; // ✅ 알림 직접 저장

/**
* JWT uid = applicantId
*/
@Transactional
public void applyToJob(Long postId, Long applicantId, Long resumeId) {
User applicant = userRepository.findById(applicantId)
Expand All @@ -49,11 +50,27 @@ public void applyToJob(Long postId, Long applicantId, Long resumeId) {
application.setResume(resume);
application.setStatus("pending");
application.setCreatedAt(LocalDateTime.now());

jobApplicationRepository.save(application);

jobPostRepository.incrementApplicantCount(postId);

log.debug("Publishing JobApplicationCreatedEvent: {}", application.getId());
eventPublisher.publishEvent(new JobApplicationCreatedEvent(application.getId()));
// ✅ 지원 저장 직후 바로 알림 생성 (이벤트/트랜잭션 훅 의존 X)
Notification noti = Notification.builder()
.recipient(post.getAuthor()) // 공고 작성자
.actor(applicant) // 지원자
.notificationType(NotificationType.JOB_APPLICATION_RECEIVED)
.title("새 지원이 도착했어요")
.message(String.format("%s님이 \"%s\" 공고에 지원했습니다.",
nz(applicant.getName(), "익명"),
nz(post.getTitle(), "공고")))
.linkUrl("/jobs/" + postId + "/applications/" + application.getId())
.build();
notificationRepository.save(noti);

log.debug("Application {} created; Notification {} saved.", application.getId(), noti.getId());
}

private static String nz(String s, String def) {
return (s == null || s.isBlank()) ? def : s;
}
}
Original file line number Diff line number Diff line change
@@ -1,60 +1,20 @@
// src/main/java/com/chaineeproject/chainee/service/NotificationService.java
package com.chaineeproject.chainee.service;

import com.chaineeproject.chainee.dto.NotificationView;
import com.chaineeproject.chainee.dto.PagedNotificationsResponse;
import com.chaineeproject.chainee.entity.Notification;
import com.chaineeproject.chainee.entity.User;
import com.chaineeproject.chainee.entity.enums.NotificationType;
import com.chaineeproject.chainee.event.JobApplicationCreatedEvent;
import com.chaineeproject.chainee.repository.JobApplicationRepository;
import com.chaineeproject.chainee.repository.NotificationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.event.TransactionPhase;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationService {

private final NotificationRepository notificationRepository;
private final JobApplicationRepository jobApplicationRepository;

// 지원 발생 후 커밋이 완료된 다음 알림 생성
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onJobApplicationCreated(JobApplicationCreatedEvent event) {
jobApplicationRepository.findByIdWithPostAndUsers(event.applicationId()) // ✅ 메서드명 변경
.ifPresent(app -> {
User author = app.getPost().getAuthor();
User applicant = app.getApplicant();

String title = "새 지원이 도착했어요";
String message = String.format("%s님이 \"%s\" 공고에 지원했습니다.",
nullSafe(applicant.getName(), "익명"),
nullSafe(app.getPost().getTitle(), "공고"));

String link = "/jobs/" + app.getPost().getId()
+ "/applications/" + app.getId();

Notification noti = Notification.builder()
.recipient(author)
.actor(applicant)
.notificationType(NotificationType.JOB_APPLICATION_RECEIVED)
.title(title)
.message(message)
.linkUrl(link)
.build();
notificationRepository.save(noti);
});
}

private String nullSafe(String s, String def) { return (s == null || s.isBlank()) ? def : s; }

// ===== 조회/읽음 처리 =====
@Transactional(readOnly = true)
Expand Down