Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest;
import org.sopt.kareer.domain.member.dto.request.MemberTermsRequest;
import org.sopt.kareer.domain.member.dto.request.MypageRequest;
import org.sopt.kareer.domain.member.dto.response.*;
import org.sopt.kareer.domain.member.entity.constants.Field;
Expand Down Expand Up @@ -176,4 +177,15 @@ public ResponseEntity<BaseResponse<OcrPassportResponse>> getPassportInfo(
}


@Operation(summary = "약관 동의", description = "약관에 동의합니다.")
@CustomExceptionDescription(TERM_AGREE)
@PostMapping("/term-agreements")
public ResponseEntity<BaseResponse<Void>> agreeTerms(
@AuthenticationPrincipal Long memberId,
@RequestBody @Valid MemberTermsRequest request
) {
memberService.agreeTerms(memberId, request);
return ResponseEntity.status(HttpStatus.OK)
.body(BaseResponse.ok("약관 동의 저장에 성공했습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.sopt.kareer.domain.member.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.util.List;

@Schema(description = "약관 동의 요청")
public record MemberTermsRequest(
@Schema(description = "약관 동의 리스트")
@NotEmpty(message = "약관 동의 목록은 비어있을 수 없습니다.")
List<@NotNull(message = "약관 동의 항목은 null일 수 없습니다.") @Valid TermAgreement> agreements
) {
public record TermAgreement(
@Schema(description = "약관 고유번호", example="1")
@NotNull(message = "약관 ID는 필수입니다.")
Long termId,

@Schema(description = "약관 동의여부", example="true")
@NotNull(message = "동의 여부는 필수입니다.")
boolean agreed
) {}
}
49 changes: 49 additions & 0 deletions src/main/java/org/sopt/kareer/domain/member/entity/MemberTerm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.sopt.kareer.domain.member.entity;

import jakarta.persistence.*;
import lombok.*;
import org.sopt.kareer.domain.term.entity.Term;
import org.sopt.kareer.global.entity.BaseEntity;

@Table(
name = "member_terms",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_member_term",
columnNames = {"member_id", "term_id"}
)
}
)
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberTerm extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "term_agreement_id")
private Long id;

@Column(nullable = false)
private boolean agreed;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "term_id", nullable = false)
private Term term;

public MemberTerm(boolean agreed, Member member, Term term) {
this.agreed = agreed;
this.member = member;
this.term = term;
}

public static MemberTerm create(boolean agreed, Member member, Term term) {
return new MemberTerm(agreed, member, term);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.kareer.domain.member.repository;

import org.sopt.kareer.domain.member.entity.MemberTerm;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberTermRepository extends JpaRepository<MemberTerm, Long> {

boolean existsByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
import lombok.RequiredArgsConstructor;
import org.sopt.kareer.domain.member.dto.request.MemberOnboardRequest;
import org.sopt.kareer.domain.member.dto.request.MemberOnboardV2Request;
import org.sopt.kareer.domain.member.dto.request.MemberTermsRequest;
import org.sopt.kareer.domain.member.dto.response.*;
import org.sopt.kareer.domain.member.entity.Member;
import org.sopt.kareer.domain.member.entity.MemberTerm;
import org.sopt.kareer.domain.member.entity.MemberVisa;
import org.sopt.kareer.domain.member.entity.enums.MemberStatus;
import org.sopt.kareer.domain.member.exception.MemberErrorCode;
import org.sopt.kareer.domain.member.exception.MemberException;
import org.sopt.kareer.domain.member.repository.MemberRepository;
import org.sopt.kareer.domain.member.repository.MemberTermRepository;
import org.sopt.kareer.domain.member.repository.MemberVisaRepository;
import org.sopt.kareer.domain.member.service.dto.request.MypageCommand;
import org.sopt.kareer.domain.member.util.PassportOcrParser;
import org.sopt.kareer.domain.member.util.VisaOcrParser;
import org.sopt.kareer.domain.term.entity.Term;
import org.sopt.kareer.domain.term.exception.TermErrorCode;
import org.sopt.kareer.domain.term.exception.TermException;
import org.sopt.kareer.domain.term.service.TermService;
import org.sopt.kareer.global.document.exception.DocumentErrorCode;
import org.sopt.kareer.global.document.exception.DocumentException;
import org.sopt.kareer.global.document.service.DocumentProcessingService;
Expand All @@ -25,13 +32,20 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

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

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {

private final MemberRepository memberRepository;
private final MemberVisaRepository memberVisaRepository;
private final TermService termService;
private final MemberTermRepository memberTermRepository;
private final MemberDeletionService memberDeletionService;
private final DocumentProcessingService documentProcessingService;
private final VisaOcrParser visaOcrParser;
Expand Down Expand Up @@ -212,4 +226,52 @@ public OcrPassportResponse getPassportOcr(MultipartFile file) {
);
}
}

@Transactional
public void agreeTerms(Long memberId, MemberTermsRequest request) {
if (memberTermRepository.existsByMemberId(memberId)) {
throw new TermException(TermErrorCode.ALREADY_AGREED_TERMS);
}

Member member = getById(memberId);
List<MemberTermsRequest.TermAgreement> agreements = request.agreements();

Map<Long, Boolean> agreementMap = agreements.stream()
.collect(Collectors.toMap(
MemberTermsRequest.TermAgreement::termId,
MemberTermsRequest.TermAgreement::agreed,

// 중복된 term이 있는지 체크
(existing, replacement) -> {
throw new TermException(TermErrorCode.DUPLICATE_TERM);
}
));

// 받아야하는 약관들 리스트
List<Term> activeTerms = termService.getActiveTerms();

// 필요한 약관들에 대해 잘 요청됐는지 검증
Set<Long> activeTermIds = activeTerms.stream()
.map(Term::getId)
.collect(Collectors.toSet());

if (!activeTermIds.equals(agreementMap.keySet())) {
throw new TermException(TermErrorCode.MISSING_TERM);
}

List<MemberTerm> memberTerms = activeTerms.stream()
.map(term -> {
Boolean agreed = agreementMap.get(term.getId());

// 필수 약관인데 동의하지 않은 경우
if (term.isRequired() && !Boolean.TRUE.equals(agreed)) {
throw new TermException(TermErrorCode.REQUIRED_TERM_NOT_AGREED);
}

return MemberTerm.create(agreed, member, term);
})
.toList();

memberTermRepository.saveAll(memberTerms);
Comment thread
hyomee2 marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.sopt.kareer.domain.term.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.sopt.kareer.domain.term.dto.response.TermsResponse;
import org.sopt.kareer.domain.term.service.TermService;
import org.sopt.kareer.global.response.BaseResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/terms")
@Tag(name = "Term API")
public class TermController {

private final TermService termService;

@GetMapping
@Operation(summary = "약관 조회", description = "약관 내용을 조회합니다.")
public ResponseEntity<BaseResponse<TermsResponse>> getTerms() {
return ResponseEntity
.status(HttpStatus.OK)
.body(BaseResponse.ok(termService.getTerms(), "약관 조회에 성공했습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.sopt.kareer.domain.term.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import org.sopt.kareer.domain.term.entity.Term;

import java.util.List;

@Schema(description = "약관 응답")
public record TermsResponse(
@Schema(description = "약관 리스트")
List<TermResponse> terms
) {
public static TermsResponse from(List<TermResponse> terms) {
return new TermsResponse(terms);
}

public record TermResponse(
@Schema(description = "약관 고유번호", example="1")
Long termId,

@Schema(description = "약관 제목", example="Terms of Service")
String title,

@Schema(description = "약관 내용", example="1. Purpose ~")
String content,

@Schema(description = "필수 여부", example="true")
boolean required
) {
public static TermResponse from(Term term) {
return new TermResponse(
term.getId(),
term.getTitle(),
term.getContent(),
term.isRequired()
);
}
}
}
39 changes: 39 additions & 0 deletions src/main/java/org/sopt/kareer/domain/term/entity/Term.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.sopt.kareer.domain.term.entity;

import jakarta.persistence.*;
import lombok.*;
import org.sopt.kareer.domain.term.entity.enums.TermType;
import org.sopt.kareer.global.entity.BaseEntity;

@Table(name = "terms")
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Term extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "term_id")
private Long id;

@Column(nullable = false)
private String title;

@Column(nullable = false, columnDefinition = "TEXT")
private String content;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private TermType type;

@Column(nullable = false)
private String version;

@Column(nullable = false)
private boolean required;

@Column(nullable = false)
private boolean active;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.sopt.kareer.domain.term.entity.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum TermType {

SERVICE(1),
PERSONAL(2),
IDENTIFICATION(3),
SENSITIVE(4),
ENTRUSTMENT(5),
MARKETING(6);

private final int order;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.sopt.kareer.domain.term.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.sopt.kareer.global.exception.errorcode.ErrorCode;
import org.springframework.http.HttpStatus;

@AllArgsConstructor
@Getter
public enum TermErrorCode implements ErrorCode {

TERM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 약관입니다."),
DUPLICATE_TERM(HttpStatus.BAD_REQUEST.value(), "중복된 약관 ID가 존재합니다."),
REQUIRED_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST.value(), "필수 약관에 동의해야 합니다."),
MISSING_TERM(HttpStatus.BAD_REQUEST.value(), "누락된 약관 동의가 존재합니다."),
INVALID_ARGUMENT(HttpStatus.BAD_REQUEST.value(), "잘못된 요청 파라미터입니다."),
ALREADY_AGREED_TERMS(HttpStatus.CONFLICT.value(), "이미 약관 동의가 완료되었습니다.")
;

private final int httpStatus;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.kareer.domain.term.exception;

import org.sopt.kareer.global.exception.customexception.CustomException;

public class TermException extends CustomException {
public TermException(TermErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.sopt.kareer.domain.term.repository;

import org.sopt.kareer.domain.term.entity.Term;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface TermRepository extends JpaRepository<Term, Long> {

List<Term> findByActiveTrue();
}
Loading
Loading