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
@@ -0,0 +1,72 @@
package com.chaineeproject.chainee.controller;

import com.chaineeproject.chainee.dto.ContractNotificationRequest;
import com.chaineeproject.chainee.dto.ContractNotificationResponse;
import com.chaineeproject.chainee.service.EmployerApplicationService;
import com.chaineeproject.chainee.security.SecurityUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/job/applications")
@Tag(name = "Contract Notification", description = "계약 전자서명 관련 알림 API")
@SecurityRequirement(name = "bearerAuth")
public class ContractNotificationController {

private final EmployerApplicationService employerApplicationService;

private Long meId(Jwt jwt) {
return SecurityUtils.uidOrNull(jwt);
}

/**
* 구인자 → 구직자 : 계약서 전자 서명 요청 알림
* 예: POST /api/job/applications/{applicationId}/contract-request
* body: { "transaction": "{...raw tx...}" }
*/
@PostMapping("/{applicationId}/contract-request")
@Operation(
summary = "계약서 전자 서명 요청 알림 (구인자 → 구직자)",
description = "공고 작성자가 지원자에게 계약서 전자 서명을 요청하는 알림을 생성합니다."
)
public ResponseEntity<ContractNotificationResponse> sendContractRequest(
@AuthenticationPrincipal Jwt jwt,
@PathVariable Long applicationId,
@RequestBody ContractNotificationRequest request
) {
Long employerId = meId(jwt);
if (employerId == null) return ResponseEntity.status(401).build();

var res = employerApplicationService.sendContractNotification(employerId, applicationId, request);
return ResponseEntity.ok(res);
}

/**
* 🔥 구직자 → 구인자 : 서명 완료 알림
* 예: POST /api/job/applications/{applicationId}/contract-signed
* body: { "transaction": "{...signed tx...}" }
*/
@PostMapping("/{applicationId}/contract-signed")
@Operation(
summary = "계약서 전자 서명 완료 알림 (구직자 → 구인자)",
description = "지원자가 계약서 전자 서명을 완료했음을 공고 작성자에게 알리는 알림을 생성합니다."
)
public ResponseEntity<ContractNotificationResponse> sendContractSigned(
@AuthenticationPrincipal Jwt jwt,
@PathVariable Long applicationId,
@RequestBody ContractNotificationRequest request
) {
Long applicantId = meId(jwt);
if (applicantId == null) return ResponseEntity.status(401).build();

var res = employerApplicationService.sendSignedContractNotification(applicantId, applicationId, request);
return ResponseEntity.ok(res);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// src/main/java/com/chaineeproject/chainee/controller/EmployerApplicationController.java
package com.chaineeproject.chainee.controller;

import com.chaineeproject.chainee.dto.ApplicationAcceptResponse;
import com.chaineeproject.chainee.dto.PostApplicantsView;
import com.chaineeproject.chainee.dto.ResumeView;
import com.chaineeproject.chainee.dto.*;
import com.chaineeproject.chainee.service.EmployerApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
Expand All @@ -23,9 +21,13 @@ public class EmployerApplicationController {

private final EmployerApplicationService employerApplicationService;

private Long meId(Jwt jwt) { return Long.valueOf(jwt.getSubject()); }
private Long meId(Jwt jwt) {
return Long.valueOf(jwt.getSubject());
}

/** 지원 수락 */
/**
* 지원 수락
*/
@PostMapping("/job/applications/{applicationId}/accept")
@Operation(summary = "지원 수락",
description = "구인자(작성자)가 특정 지원서를 수락합니다. 응답으로 구직자 DID, payment, deadline을 반환합니다.")
Expand All @@ -37,7 +39,9 @@ public ResponseEntity<ApplicationAcceptResponse> accept(
return ResponseEntity.ok(res);
}

/** 공고 지원자 관리 목록 */
/**
* 공고 지원자 관리 목록
*/
@GetMapping("/job/posts/{postId}/applicants")
@Operation(summary = "공고 지원자 관리 목록",
description = "공고 제목, 총 지원자 수, 지원자 리스트(이름/지원 날짜/positions)를 반환합니다.")
Expand All @@ -50,7 +54,9 @@ public ResponseEntity<PostApplicantsView> applicantsOfPost(
);
}

/** 구인자 전용 이력서 열람 (applicationId로 접근) */
/**
* 구인자 전용 이력서 열람 (applicationId로 접근)
*/
@GetMapping("/job/applications/{applicationId}/resume")
@Operation(summary = "지원서 기반 이력서 조회(구인자용)",
description = "공고 작성자가 자신에게 접수된 지원서의 이력서를 열람합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// src/main/java/com/chaineeproject/chainee/dto/ContractNotificationRequest.java
package com.chaineeproject.chainee.dto;

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

@Schema(description = "계약서 전자서명 요청 알림 생성 요청")
public record ContractNotificationRequest(

@Schema(
description = "프론트에서 생성한 트랜잭션 문자열 그대로 (JSON이든 base64든 어떤 형식이든 상관 없음)",
example = "{ \"chain\": \"solana\", \"network\": \"devnet\", \"tx\": \"...\" }"
)
String transaction
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// src/main/java/com/chaineeproject/chainee/dto/ContractNotificationResponse.java
package com.chaineeproject.chainee.dto;

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

@Schema(description = "계약서 전자서명 요청 알림 생성 응답")
public record ContractNotificationResponse(

@Schema(description = "성공 여부", example = "true")
boolean success,

@Schema(description = "메시지 코드", example = "CONTRACT_NOTIFICATION_SENT")
String messageCode,

@Schema(description = "생성된 알림 ID", example = "101")
Long notificationId
) {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.chaineeproject.chainee.entity.enums;

public enum NotificationType {
JOB_APPLICATION_RECEIVED
JOB_APPLICATION_RECEIVED,
CONTRACT_SIGNATURE_REQUEST,
CONTRACT_SIGNED
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
// src/main/java/com/chaineeproject/chainee/service/EmployerApplicationService.java
package com.chaineeproject.chainee.service;

import com.chaineeproject.chainee.dto.ApplicantSummary;
import com.chaineeproject.chainee.dto.ApplicationAcceptResponse;
import com.chaineeproject.chainee.dto.PostApplicantsView;
import com.chaineeproject.chainee.dto.ResumeView;
import com.chaineeproject.chainee.dto.*;
import com.chaineeproject.chainee.entity.JobApplication;
import com.chaineeproject.chainee.entity.JobPost;
import com.chaineeproject.chainee.entity.Notification;
import com.chaineeproject.chainee.entity.Resume;
import com.chaineeproject.chainee.entity.User;
import com.chaineeproject.chainee.entity.enums.NotificationType;
import com.chaineeproject.chainee.exception.ApplicationException;
import com.chaineeproject.chainee.exception.ErrorCode;
import com.chaineeproject.chainee.repository.JobApplicationRepository;
import com.chaineeproject.chainee.repository.JobPostRepository;
import com.chaineeproject.chainee.repository.NotificationRepository;
import com.chaineeproject.chainee.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class EmployerApplicationService {

private final JobApplicationRepository jobApplicationRepository;
private final JobPostRepository jobPostRepository;
private final UserRepository userRepository;
private final NotificationRepository notificationRepository;

/** 지원 수락: applicant DID + post payment + deadline 반환 */
/**
* 지원 수락: applicant DID + post payment + deadline 반환
*/
@Transactional
public ApplicationAcceptResponse acceptApplication(Long applicationId, Long employerUserId) {
JobApplication app = jobApplicationRepository.findByIdWithPostAndUsers(applicationId)
Expand Down Expand Up @@ -63,7 +69,9 @@ public ApplicationAcceptResponse acceptApplication(Long applicationId, Long empl
);
}

/** 공고별 지원자 관리 목록 */
/**
* 공고별 지원자 관리 목록
*/
@Transactional(readOnly = true)
public PostApplicantsView getApplicantsOfPost(Long employerUserId, Long postId) {
JobPost post = jobPostRepository.findById(postId)
Expand All @@ -83,25 +91,29 @@ public PostApplicantsView getApplicantsOfPost(Long employerUserId, Long postId)
a.getApplicant().getPositions() == null ? List.of() : a.getApplicant().getPositions()
)).toList();

long total = post.getApplicantCount(); // 실시간이면 apps.size()
long total = post.getApplicantCount(); // 또는 apps.size() 사용도 가능
return new PostApplicantsView(post.getId(), post.getTitle(), total, items);
}

/** 구인자 전용: 특정 지원서의 이력서를 열람 */
/**
* 구인자 전용: 특정 지원서의 이력서를 열람
*/
@Transactional(readOnly = true)
public ResumeView getResumeForEmployer(Long employerUserId, Long applicationId) {
var app = jobApplicationRepository.findByIdWithPostApplicantAndResume(applicationId)
JobApplication app = jobApplicationRepository.findByIdWithPostApplicantAndResume(applicationId)
.orElseThrow(() -> new ApplicationException(ErrorCode.JOB_APPLICATION_NOT_FOUND));

if (!app.getPost().getAuthor().getId().equals(employerUserId)) {
throw new ApplicationException(ErrorCode.FORBIDDEN);
}

var r = app.getResume();
Resume r = app.getResume();
var skills = (r.getSkills() == null || r.getSkills().isBlank())
? java.util.List.<String>of()
? List.<String>of()
: java.util.Arrays.stream(r.getSkills().split(","))
.map(String::trim).filter(s -> !s.isBlank()).toList();
.map(String::trim)
.filter(s -> !s.isBlank())
.toList();

return new ResumeView(
r.getId(),
Expand All @@ -115,4 +127,125 @@ public ResumeView getResumeForEmployer(Long employerUserId, Long applicationId)
r.getCreatedAt()
);
}

/**
* 구인자 → 구직자에게 계약 전자서명 요청 알림 생성
* - 프론트는 transaction 문자열만 넘긴다.
* - 백엔드는 transaction을 그대로 Notification에 저장하고,
* 타이틀/메시지만 사람이 읽을 수 있게 세팅.
*/
@Transactional
public ContractNotificationResponse sendContractNotification(
Long employerId,
Long applicationId,
ContractNotificationRequest req
) {
// 1) 지원서 + 연관 엔티티 로딩
JobApplication app = jobApplicationRepository.findByIdWithPostAndUsers(applicationId)
.orElseThrow(() -> new ApplicationException(ErrorCode.JOB_APPLICATION_NOT_FOUND));

JobPost post = app.getPost();
User applicant = app.getApplicant();
User author = post.getAuthor();

// 2) 권한 체크: 요청자 == 공고 작성자(구인자)
if (!author.getId().equals(employerId)) {
throw new ApplicationException(ErrorCode.FORBIDDEN);
}

// 3) transaction 필수 체크
if (req.transaction() == null || req.transaction().isBlank()) {
throw new ApplicationException(ErrorCode.INVALID_REQUEST);
}

// 4) 알림 내용 구성
String title = "계약서 전자 서명이 필요합니다";
String message = String.format(
"'%s' 공고에 대한 계약서 전자 서명을 진행해 주세요.",
nz(post.getTitle(), "공고")
);

// 5) 알림 생성
Notification noti = Notification.builder()
.recipient(applicant) // 알림 받는 사람 = 구직자
.actor(author) // 알림을 유발한 사람 = 구인자
.notificationType(NotificationType.CONTRACT_SIGNATURE_REQUEST)
.title(title)
.message(message)
// linkUrl은 프론트 라우팅 전략에 따라 사용/미사용
.linkUrl(null)
.build();

notificationRepository.save(noti);

log.debug("Contract signature notification created: notiId={}, applicationId={}",
noti.getId(), applicationId);

return new ContractNotificationResponse(
true,
"CONTRACT_NOTIFICATION_SENT",
noti.getId()
);
}

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

/**
* 🔥 새로 추가: 구직자 → 구인자에게 "서명 완료" 알림 생성
* - applicantId: JWT uid (요청자 = 구직자)
* - applicationId: 지원 ID
* - req.transaction: 서명된 트랜잭션(raw string)
*/
@Transactional
public ContractNotificationResponse sendSignedContractNotification(
Long applicantId,
Long applicationId,
ContractNotificationRequest req
) {
JobApplication app = jobApplicationRepository.findByIdWithPostAndUsers(applicationId)
.orElseThrow(() -> new ApplicationException(ErrorCode.JOB_APPLICATION_NOT_FOUND));

JobPost post = app.getPost();
User applicant = app.getApplicant();
User author = post.getAuthor(); // 구인자

// 권한: 요청자 == 지원자
if (!applicant.getId().equals(applicantId)) {
throw new ApplicationException(ErrorCode.FORBIDDEN);
}

if (req.transaction() == null || req.transaction().isBlank()) {
throw new ApplicationException(ErrorCode.INVALID_REQUEST);
}

String title = "계약서 전자 서명이 완료되었습니다";
String message = String.format(
"%s님이 '%s' 공고에 대한 계약서 전자 서명을 완료했습니다.",
nz(applicant.getName(), "지원자"),
nz(post.getTitle(), "공고")
);

Notification noti = Notification.builder()
.recipient(author) // 알림 받는 사람 = 구인자
.actor(applicant) // 알림 유발 = 구직자
.notificationType(NotificationType.CONTRACT_SIGNED)
.title(title)
.message(message)
.linkUrl(null)
.build();

notificationRepository.save(noti);

log.debug("Contract signed notification created: notiId={}, applicationId={}",
noti.getId(), applicationId);

return new ContractNotificationResponse(
true,
"CONTRACT_SIGNED_NOTIFICATION_SENT",
noti.getId()
);
}

}