Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion config
2 changes: 1 addition & 1 deletion manifest
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.assu.server.domain.auth.controller;

import java.util.List;

import com.assu.server.domain.auth.dto.login.CommonLoginRequestDTO;
import com.assu.server.domain.auth.dto.login.LoginResponseDTO;
import com.assu.server.domain.auth.dto.login.RefreshResponseDTO;
Expand All @@ -13,7 +15,13 @@
import com.assu.server.domain.auth.dto.ssu.USaintAuthRequestDTO;
import com.assu.server.domain.auth.dto.ssu.USaintAuthResponseDTO;
import com.assu.server.domain.auth.dto.email.EmailVerificationCheckRequestDTO;
import com.assu.server.domain.auth.service.*;
import com.assu.server.domain.auth.service.EmailAuthService;
import com.assu.server.domain.auth.service.LoginService;
import com.assu.server.domain.auth.service.LogoutService;
import com.assu.server.domain.auth.service.PhoneAuthService;
import com.assu.server.domain.auth.service.SSUAuthService;
import com.assu.server.domain.auth.service.SignUpService;
import com.assu.server.domain.auth.service.WithdrawalService;
import com.assu.server.domain.common.entity.enums.University;
import com.assu.server.global.apiPayload.BaseResponse;
import com.assu.server.global.apiPayload.code.status.SuccessStatus;
Expand All @@ -22,6 +30,7 @@
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.RequestBody;
import jakarta.validation.Valid;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.assu.server.domain.auth.dto.signup;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

@Schema(description = "제휴업체 단체 가입 개별 요청 정보")
public record PartnerBatchSignUpItemDTO(
@Schema(description = "이메일 주소", example = "partner1@example.com")
@Email(message = "올바른 이메일 형식이 아닙니다.")
@NotBlank(message = "이메일은 필수입니다.")
String email,

@Schema(description = "비밀번호(평문)", example = "Password123!")
@Size(min = 8, max = 72, message = "비밀번호는 8~72자여야 합니다.")
@NotBlank(message = "비밀번호는 필수입니다.")
String password,

@Schema(description = "업체명", example = "숭실카페 1호점")
@Size(min = 1, max = 50, message = "업체명은 1~50자여야 합니다.")
@NotBlank(message = "업체명은 필수입니다.")
String name,

@Schema(description = "도로명 주소", example = "서울특별시 동작구 상도로 369")
String roadAddress,

@Schema(description = "위도", example = "37.4963")
Double latitude,

@Schema(description = "경도", example = "126.9573")
Double longitude
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.assu.server.domain.auth.exception.CustomAuthException;
import com.assu.server.infra.aligo.client.AligoSmsClient;
import com.assu.server.infra.aligo.dto.AligoSendResponse;
import com.assu.server.infra.aligo.exception.AligoException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
Expand Down Expand Up @@ -41,10 +42,16 @@ public void checkAndSendAuthNumber(String phoneNumber) {

String message = "[ASSU] 인증번호: " + authNumber;

AligoSendResponse response = aligoSmsClient.sendSms(phoneNumber, message, "사용자");
AligoSendResponse response;
try {
response = aligoSmsClient.sendSms(phoneNumber, message, "사용자");
} catch (AligoException e) {
redisTemplate.delete(phoneNumber);
throw e;
}

// 실패 처리
if (!response.getResult_code().equals("1")) {
if (!"1".equals(response.getResult_code())) {
redisTemplate.delete(phoneNumber);
throw new CustomAuthException(ErrorStatus.FAILED_TO_SEND_SMS);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.assu.server.domain.auth.service;

import com.assu.server.domain.auth.dto.signup.AdminSignUpRequestDTO;
import com.assu.server.domain.auth.dto.signup.PartnerBatchSignUpItemDTO;
import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequestDTO;
import com.assu.server.domain.auth.dto.signup.SignUpResponseDTO;
import com.assu.server.domain.auth.dto.signup.StudentTokenSignUpRequestDTO;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

public interface SignUpService {
SignUpResponseDTO signupSsuStudent(StudentTokenSignUpRequestDTO req);

SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFile licenseImage);

List<SignUpResponseDTO> signupBatchPartner(List<PartnerBatchSignUpItemDTO> requests);

SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile signImage);
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,66 @@ public SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFil
return SignUpResponseDTO.from(member, null);
}

@Override
public List<SignUpResponseDTO> signupBatchPartner(List<PartnerBatchSignUpItemDTO> requests) {
return requests.stream().map(req -> {
Member member = memberRepository.save(
Member.builder()
.isLocationTermAgreed(true)
.isMarketingTermAgreed(true)
.role(UserRole.PARTNER)
.isActivated(ActivationStatus.SUSPEND)
.build());

RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON);
adapter.registerCredentials(member, req.email(), req.password());

String roadAddress = req.roadAddress() != null ? req.roadAddress() : "";
Double lat = req.latitude() != null ? req.latitude() : 0.0;
Double lng = req.longitude() != null ? req.longitude() : 0.0;
Point point = toPoint(lat, lng);

Partner partner = partnerRepository.save(
Partner.builder()
.member(member)
.name(req.name())
.phoneNum(null)
.isPhoneVerified(false)
.address(roadAddress)
.detailAddress(null)
.licenseUrl(null)
.point(point)
.latitude(lat)
.longitude(lng)
.build());
member.setProfile(partner);

Optional<Store> storeOpt = storeRepository.findBySameAddress(roadAddress, null);
if (storeOpt.isPresent()) {
Store store = storeOpt.get();
store.linkPartner(partner);
store.setName(req.name());
store.setGeo(lat, lng, point);
storeRepository.save(store);
} else {
Store newly = Store.builder()
.partner(partner)
.rate(0)
.isActivate(ActivationStatus.SUSPEND)
.name(req.name())
.address(roadAddress)
.detailAddress(null)
.latitude(lat)
.longitude(lng)
.point(point)
.build();
storeRepository.save(newly);
}

return SignUpResponseDTO.from(member, null);
}).toList();
}

@Override
public SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile signImage) {
if (partnerRepository.existsByPhoneNum(req.phoneNumber())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.assu.server.domain.backoffice.controller;

import com.assu.server.domain.auth.dto.signup.PartnerBatchSignUpItemDTO;
import com.assu.server.domain.auth.dto.signup.SignUpResponseDTO;
import com.assu.server.domain.auth.service.SignUpService;
import com.assu.server.domain.backoffice.annotation.BackofficeAudited;
import com.assu.server.domain.backoffice.dto.BackofficeDocumentUrlResponseDTO;
import com.assu.server.domain.backoffice.dto.BackofficeMemberSummaryDTO;
Expand All @@ -9,14 +12,20 @@
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.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Tag(name = "Backoffice", description = "백오피스 운영 API")
@RestController
@RequiredArgsConstructor
Expand All @@ -25,6 +34,7 @@
public class BackofficePartnerController {

private final BackofficeMemberService backofficeMemberService;
private final SignUpService signUpService;

@Operation(
summary = "사업자등록증 조회 API",
Expand Down Expand Up @@ -68,4 +78,18 @@ public BaseResponse<BackofficeDocumentUrlResponseDTO> getLicenseUrl(
public BaseResponse<BackofficeMemberSummaryDTO> verifyLicense(@PathVariable Long memberId) {
return BaseResponse.onSuccess(SuccessStatus._OK, backofficeMemberService.verifyPartnerLicense(memberId));
}

@BackofficeAudited(action = "PARTNER_BATCH_SIGNUP")
@Operation(
summary = "제휴업체 단체 회원가입 API",
description = "이메일, 비밀번호, 업체명, 도로명 주소, 위도, 경도 목록을 받아 제휴업체 계정들을 일괄 생성합니다."
)
@PostMapping(value = "/batch-signup", consumes = MediaType.APPLICATION_JSON_VALUE)
public BaseResponse<List<SignUpResponseDTO>> signupBatchPartner(
@RequestBody
@Valid
List<PartnerBatchSignUpItemDTO> requests
) {
return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupBatchPartner(requests));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,12 @@ public CertificationProgressResponseDTO handleCertification(GroupSessionRequest
if (!matched) {
List<Long> currentCertifiedUserIds = sessionManager.snapshotUserIds(sessionId);
CertificationProgressResponseDTO response = new CertificationProgressResponseDTO(
"fail",
currentCertifiedUserIds.size(),
"mismatch",
currentCertifiedUserIds.size(),
"학생과 매치되지 않는 정보입니다.",
currentCertifiedUserIds
);
messagingTemplate.convertAndSend("/certification/progress/" + sessionId, response);
return response;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,22 @@ public BaseResponse<Void> deletePartnership(
return BaseResponse.onSuccess(SuccessStatus._OK, null);
}

@Operation(
summary = "대기 중인 제휴 계약서 삭제 API",
description = "- 관리자 관점에서 대기 중(SUSPEND) 상태인 제휴 계약서를 삭제합니다.\n" +
"- 로그인한 관리자의 ID와 계약서의 관리자 ID가 일치해야 합니다.\n" +
"- 계약서의 상태가 SUSPEND(대기 중)여야 삭제가 가능합니다.\n"
)
@DeleteMapping("/suspended/{paperId}")
@PreAuthorize("hasRole('ADMIN')")
public BaseResponse<Void> deleteSuspendedPaper(
@PathVariable @Parameter(required = true) Long paperId,
@AuthenticationPrincipal PrincipalDetails pd
) {
partnershipService.deleteSuspendedPaper(paperId, pd.getId());
return BaseResponse.onSuccess(SuccessStatus._OK, null);
}

@Operation(
summary = "제휴 중인 가게 조회 API",
description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/_-2241197c19ed81b1b9adf724adc4600c)\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ PartnershipStatusUpdateResponseDTO updatePartnershipStatus(

void deletePartnership(Long paperId, Long memberId, UserRole role);

void deleteSuspendedPaper(Long paperId, Long adminId);

AdminPartnershipCheckResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId);
PartnerPartnershipCheckResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,44 @@ public void deletePartnership(Long paperId, Long memberId, UserRole role) {
}
}

@Override
@Transactional
public void deleteSuspendedPaper(Long paperId, Long adminId) {
Paper paper = paperRepository.findById(paperId)
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER));

if (!paper.getAdmin().getId().equals(adminId)) {
throw new GeneralException(ErrorStatus._FORBIDDEN);
}

if (paper.getIsActivated() != ActivationStatus.SUSPEND) {
throw new GeneralException(ErrorStatus._BAD_REQUEST);
}

List<PaperContent> contentsToDelete = paperContentRepository.findByPaperId(paperId);
if (contentsToDelete != null && !contentsToDelete.isEmpty()) {
List<Long> contentIds = contentsToDelete.stream()
.map(PaperContent::getId)
.toList();

goodsRepository.deleteAllByContentIds(contentIds);
paperContentRepository.deleteAll(contentsToDelete);
}

Store store = paper.getStore();
boolean isTempStore = (store != null && paper.getPartner() == null);

paperRepository.delete(paper);

if (isTempStore) {
Long storeId = store.getId();
long remainingPaperRefs = paperRepository.countByStore_Id(storeId);
if (remainingPaperRefs == 0) {
storeRepository.delete(store);
}
}
}

// Todo: 추후 checkPartnershipWithPartner와 checkPartnershipWithAdmin를 Role 기반 로직으로 변경하여 메소드 합칠 것
@Override
@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.assu.server.infra.aligo.client;

import com.assu.server.domain.auth.exception.CustomAuthException;
import com.assu.server.global.apiPayload.code.status.ErrorStatus;
import com.assu.server.infra.aligo.dto.AligoSendResponse;
import com.assu.server.infra.aligo.exception.AligoException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -22,7 +22,7 @@
public class AligoSmsClient {

private final WebClient webClient;
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper;

@Value("${aligo.key}")
private String apiKey;
Expand All @@ -38,7 +38,7 @@ public class AligoSmsClient {
public AligoSendResponse sendSms(String phoneNumber, String message, String name) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("key", apiKey);
params.add("userid", userId);
params.add("user_id", userId);
params.add("sender", sender);
params.add("receiver", phoneNumber);
params.add("msg", message);
Expand All @@ -52,16 +52,20 @@ public AligoSendResponse sendSms(String phoneNumber, String message, String name
.retrieve()
.onStatus(
status -> status.is4xxClientError() || status.is5xxServerError(),
clientResponse -> clientResponse.bodyToMono(String.class).flatMap(errorBody -> {
log.error("Aligo API 호출 실패. status={}, body={}", clientResponse.statusCode(), errorBody);
return Mono.error(new AligoException(ErrorStatus.FAILED_TO_SEND_SMS));
})
clientResponse -> clientResponse.bodyToMono(String.class)
.defaultIfEmpty("")
.flatMap(errorBody -> {
log.error("Aligo API 호출 실패. status={}, body={}", clientResponse.statusCode(), errorBody);
return Mono.error(new AligoException(ErrorStatus.FAILED_TO_SEND_SMS));
})
)
.bodyToMono(String.class)
.block();

try {
return objectMapper.readValue(body, AligoSendResponse.class);
return objectMapper.readerFor(AligoSendResponse.class)
.without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.readValue(body);
} catch (Exception e) {
log.error("Aligo 응답 파싱 실패. 원본 body: {}", body, e);
throw new AligoException(ErrorStatus.FAILED_TO_PARSE_ALIGO);
Expand Down
Loading
Loading