From c4bfb2e71fcdfda74d342237a8fdbf727b512031 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Sat, 15 Aug 2026 19:11:58 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[FIX/#415]=20=EC=95=8C=EB=A6=AC=EA=B3=A0=20?= =?UTF-8?q?SMS=20=EC=9D=B8=EC=A6=9D=20=EB=B0=9C=EC=86=A1=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AligoSmsClient 사용자 ID 파라미터명을 알리고 규격에 맞게 userid -> user_id 로 수정 - ObjectMapper 를 스프링 빈 주입으로 변경하고 규격 외 응답 필드 무시 처리 - HTTP 에러 응답 body 가 비어 있을 때 전송 실패 예외가 유실되지 않도록 defaultIfEmpty 추가 - PhoneAuthServiceImpl 의 result_code 비교를 널 안전 방식으로 변경 - AligoSmsClient 요청 규격 및 응답 파싱 검증 테스트 추가 Co-Authored-By: Claude Opus 5 --- .../auth/service/PhoneAuthServiceImpl.java | 2 +- .../infra/aligo/client/AligoSmsClient.java | 20 +- .../aligo/client/AligoSmsClientTest.java | 201 ++++++++++++++++++ 3 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 src/test/java/com/assu/server/infra/aligo/client/AligoSmsClientTest.java diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java index 9dfe4f59..b0767288 100644 --- a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java @@ -44,7 +44,7 @@ public void checkAndSendAuthNumber(String phoneNumber) { AligoSendResponse response = aligoSmsClient.sendSms(phoneNumber, message, "사용자"); // 실패 처리 - if (!response.getResult_code().equals("1")) { + if (!"1".equals(response.getResult_code())) { redisTemplate.delete(phoneNumber); throw new CustomAuthException(ErrorStatus.FAILED_TO_SEND_SMS); } diff --git a/src/main/java/com/assu/server/infra/aligo/client/AligoSmsClient.java b/src/main/java/com/assu/server/infra/aligo/client/AligoSmsClient.java index d5c89c0a..f5e6a3e8 100644 --- a/src/main/java/com/assu/server/infra/aligo/client/AligoSmsClient.java +++ b/src/main/java/com/assu/server/infra/aligo/client/AligoSmsClient.java @@ -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; @@ -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; @@ -38,7 +38,7 @@ public class AligoSmsClient { public AligoSendResponse sendSms(String phoneNumber, String message, String name) { MultiValueMap 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); @@ -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); diff --git a/src/test/java/com/assu/server/infra/aligo/client/AligoSmsClientTest.java b/src/test/java/com/assu/server/infra/aligo/client/AligoSmsClientTest.java new file mode 100644 index 00000000..7dbac52e --- /dev/null +++ b/src/test/java/com/assu/server/infra/aligo/client/AligoSmsClientTest.java @@ -0,0 +1,201 @@ +package com.assu.server.infra.aligo.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; + +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.ObjectMapper; + +import reactor.core.publisher.Mono; + +class AligoSmsClientTest { + + private static final String API_KEY = "test-api-key"; + private static final String USER_ID = "test-user-id"; + private static final String SENDER = "0212345678"; + private static final String RECEIVER = "01012345678"; + private static final String MESSAGE = "[ASSU] 인증번호: 123456"; + + private final AtomicReference capturedRequest = new AtomicReference<>(); + + private AligoSmsClient buildClient(HttpStatus status, String responseBody, MediaType contentType) { + ExchangeFunction exchangeFunction = request -> { + capturedRequest.set(request); + ClientResponse.Builder builder = ClientResponse.create(status); + if (contentType != null) { + builder.header(HttpHeaders.CONTENT_TYPE, contentType.toString()); + } + return Mono.just(builder.body(responseBody).build()); + }; + + WebClient webClient = WebClient.builder().exchangeFunction(exchangeFunction).build(); + AligoSmsClient client = new AligoSmsClient(webClient, new ObjectMapper()); + ReflectionTestUtils.setField(client, "apiKey", API_KEY); + ReflectionTestUtils.setField(client, "userId", USER_ID); + ReflectionTestUtils.setField(client, "sender", SENDER); + return client; + } + + private String capturedFormBody() { + ClientRequest request = capturedRequest.get(); + assertNotNull(request, "요청이 전송되지 않았습니다."); + + MockClientHttpRequest mockRequest = new MockClientHttpRequest(HttpMethod.POST, "/"); + ExchangeStrategies strategies = ExchangeStrategies.withDefaults(); + + request.body().insert(mockRequest, new BodyInserter.Context() { + @Override + public List> messageWriters() { + return strategies.messageWriters(); + } + + @Override + public Optional serverRequest() { + return Optional.empty(); + } + + @Override + public Map hints() { + return Collections.emptyMap(); + } + }).block(); + + return mockRequest.getBodyAsString().block(); + } + + @BeforeEach + void resetCapture() { + capturedRequest.set(null); + } + + @Test + @DisplayName("알리고 규격대로 사용자 ID를 user_id 파라미터로 전송한다") + void sendSms_SendsUserIdWithSpecCompliantParameterName() { + // 1. Given + AligoSmsClient client = buildClient( + HttpStatus.OK, + "{\"result_code\":1,\"message\":\"success\",\"msg_id\":123,\"success_cnt\":1,\"error_cnt\":0,\"msg_type\":\"SMS\"}", + MediaType.APPLICATION_JSON); + + // 2. When + client.sendSms(RECEIVER, MESSAGE, "사용자"); + + // 3. Then + String body = capturedFormBody(); + assertTrue(body.contains("user_id=" + USER_ID), "알리고 규격 파라미터명은 user_id 입니다. 실제 전송 body: " + body); + assertFalse(body.contains("userid="), "규격에 없는 userid 파라미터가 전송되었습니다. 실제 전송 body: " + body); + } + + @Test + @DisplayName("알리고 필수 파라미터가 폼 데이터로 모두 전송된다") + void sendSms_SendsAllRequiredParameters() { + // 1. Given + AligoSmsClient client = buildClient( + HttpStatus.OK, + "{\"result_code\":1,\"message\":\"success\"}", + MediaType.APPLICATION_JSON); + + // 2. When + client.sendSms(RECEIVER, MESSAGE, "사용자"); + + // 3. Then + String body = capturedFormBody(); + assertTrue(body.contains("key=" + API_KEY), body); + assertTrue(body.contains("sender=" + SENDER), body); + assertTrue(body.contains("receiver=" + RECEIVER), body); + assertTrue(body.contains("msg_type=SMS"), body); + assertTrue(body.contains("msg="), body); + + assertEquals(MediaType.APPLICATION_FORM_URLENCODED, capturedRequest.get().headers().getContentType()); + } + + @Test + @DisplayName("result_code가 숫자 타입인 알리고 성공 응답을 파싱한다") + void sendSms_ParsesNumericResultCodeResponse() { + // 1. Given + AligoSmsClient client = buildClient( + HttpStatus.OK, + "{\"result_code\":1,\"message\":\"success\",\"msg_id\":123,\"success_cnt\":1,\"error_cnt\":0,\"msg_type\":\"SMS\"}", + MediaType.APPLICATION_JSON); + + // 2. When + AligoSendResponse response = client.sendSms(RECEIVER, MESSAGE, "사용자"); + + // 3. Then + assertEquals("1", response.getResult_code()); + assertEquals("success", response.getMessage()); + } + + @Test + @DisplayName("응답에 규격 외 필드가 포함되어도 파싱에 실패하지 않는다") + void sendSms_IgnoresUnknownResponseFields() { + // 1. Given + AligoSmsClient client = buildClient( + HttpStatus.OK, + "{\"result_code\":-101,\"message\":\"인증오류입니다.\",\"unknown_field\":\"x\"}", + MediaType.APPLICATION_JSON); + + // 2. When + AligoSendResponse response = client.sendSms(RECEIVER, MESSAGE, "사용자"); + + // 3. Then + assertEquals("-101", response.getResult_code()); + } + + @Test + @DisplayName("HTTP 에러 응답의 body가 비어 있어도 SMS 전송 실패 예외가 발생한다") + void sendSms_EmptyErrorBody_ThrowsSendFailure() { + // 1. Given + AligoSmsClient client = buildClient(HttpStatus.INTERNAL_SERVER_ERROR, "", null); + + // 2. When + AligoException exception = assertThrows(AligoException.class, + () -> client.sendSms(RECEIVER, MESSAGE, "사용자")); + + // 3. Then + assertEquals(ErrorStatus.FAILED_TO_SEND_SMS, exception.getCode()); + } + + @Test + @DisplayName("응답 본문이 JSON이 아니면 파싱 실패 예외가 발생한다") + void sendSms_NonJsonBody_ThrowsParseFailure() { + // 1. Given + AligoSmsClient client = buildClient(HttpStatus.OK, "error", MediaType.TEXT_HTML); + + // 2. When + AligoException exception = assertThrows(AligoException.class, + () -> client.sendSms(RECEIVER, MESSAGE, "사용자")); + + // 3. Then + assertEquals(ErrorStatus.FAILED_TO_PARSE_ALIGO, exception.getCode()); + } +} From 6fb8a55eff5213b72b44bc29553ef04902cfde62 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Sat, 15 Aug 2026 21:18:36 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[FIX/#415]=20=EC=95=8C=EB=A6=AC=EA=B3=A0=20?= =?UTF-8?q?SMS=20=EC=97=90=EB=9F=AC=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/PhoneAuthServiceImpl.java | 9 ++++++++- .../service/PhoneAuthServiceImplTest.java | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java index b0767288..a8c16e83 100644 --- a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java @@ -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; @@ -41,7 +42,13 @@ 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 (!"1".equals(response.getResult_code())) { diff --git a/src/test/java/com/assu/server/domain/auth/service/PhoneAuthServiceImplTest.java b/src/test/java/com/assu/server/domain/auth/service/PhoneAuthServiceImplTest.java index a21e8dde..f4374c96 100644 --- a/src/test/java/com/assu/server/domain/auth/service/PhoneAuthServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/auth/service/PhoneAuthServiceImplTest.java @@ -24,6 +24,7 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.infra.aligo.client.AligoSmsClient; import com.assu.server.infra.aligo.dto.AligoSendResponse; +import com.assu.server.infra.aligo.exception.AligoException; @ExtendWith(MockitoExtension.class) class PhoneAuthServiceImplTest { @@ -111,6 +112,25 @@ void checkAndSendAuthNumber_SmsFailed_DeletesCodeAndThrows() { verify(redisTemplate, times(1)).delete(PHONE); } + @Test + @DisplayName("SMS 발송 중 알리고 예외가 발생하면 저장했던 인증번호를 삭제하고 예외를 전파한다") + void checkAndSendAuthNumber_SmsThrowsAligoException_DeletesCodeAndRethrows() { + // 1. Given + when(partnerRepository.existsByPhoneNum(PHONE)).thenReturn(false); + when(adminRepository.existsByPhoneNum(PHONE)).thenReturn(false); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(aligoSmsClient.sendSms(eq(PHONE), anyString(), anyString())) + .thenThrow(new AligoException(ErrorStatus.FAILED_TO_PARSE_ALIGO)); + + // 2. When + AligoException exception = assertThrows(AligoException.class, + () -> phoneAuthService.checkAndSendAuthNumber(PHONE)); + + // 3. Then + assertEquals(ErrorStatus.FAILED_TO_PARSE_ALIGO, exception.getCode()); + verify(redisTemplate, times(1)).delete(PHONE); + } + @Test @DisplayName("저장된 인증번호가 없으면 NOT_VERIFIED_PHONE_NUMBER 예외가 발생한다") void verifyAuthNumber_NoStoredCode_ThrowsException() { From 60ffc08747539cd1131a92d8224f197323691f55 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sat, 15 Aug 2026 21:53:19 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[FEAT/#420]=20=EB=8C=80=EA=B8=B0=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=9C=ED=9C=B4=20=EA=B3=84=EC=95=BD=EC=84=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PartnershipController.java | 16 ++++++++ .../service/PartnershipService.java | 2 + .../service/PartnershipServiceImpl.java | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java b/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java index 203ae355..5be33532 100644 --- a/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java +++ b/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java @@ -299,6 +299,22 @@ public BaseResponse 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 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" + diff --git a/src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java b/src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java index c82e7c3e..5d9a2d8e 100644 --- a/src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java @@ -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); } diff --git a/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java index e51569cc..c29a4f0c 100644 --- a/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java @@ -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 contentsToDelete = paperContentRepository.findByPaperId(paperId); + if (contentsToDelete != null && !contentsToDelete.isEmpty()) { + List 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) From bdb68d34a06c145b8a00e92f5fef937b330a9f8f Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Sun, 16 Aug 2026 00:11:51 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[FIX/#415]=20=EC=95=8C=EB=A6=AC=EA=B3=A0=20?= =?UTF-8?q?=EB=B0=9C=EC=8B=A0=EB=B2=88=ED=98=B8=20=ED=8C=8C=EC=8B=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=98=EC=98=81=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20config=20=EC=84=9C=EB=B8=8C=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sender 값이 YAML 1.1 8진수로 파싱되어 150035923으로 전송되던 문제 수정본 반영 - 알리고 -103 등록/인증되지 않은 발신번호 응답의 원인 - dev 로그 레벨 조정(actuator/health DEBUG 소음 및 SQL 로그 제거) 포함 Co-Authored-By: Claude Opus 5 --- config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config b/config index f85f4ad9..e94fd01c 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit f85f4ad9772dc105636672d8226b74027bbdbed7 +Subproject commit e94fd01c17a2d7adb82387bdbbaea8a9203c38d7 From 193f9b86f5f3b8e553015fd6ecf655000febec27 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Sun, 16 Aug 2026 00:15:10 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[FIX/#415]=20manifest=20=EC=84=9C=EB=B8=8C?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=ED=8F=AC=EC=9D=B8=ED=84=B0=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dev startupProbe 추가 커밋 반영 Co-Authored-By: Claude Opus 5 --- manifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest b/manifest index 942cc46b..dd9bcc43 160000 --- a/manifest +++ b/manifest @@ -1 +1 @@ -Subproject commit 942cc46beb5679586634b05e0366380f596a3e2e +Subproject commit dd9bcc43b17b9b82f56e09e01470e66dd9a65b50 From 6744bfe9f150f07a58483f33c1f913f4e2d67b6f Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 19 Aug 2026 13:24:41 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[FEAT/#423]=20=EC=A0=9C=ED=9C=B4=EC=97=85?= =?UTF-8?q?=EC=B2=B4=20=EC=9D=BC=EA=B4=84=20=EA=B0=80=EC=9E=85=20api?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest | 2 +- .../auth/controller/AuthController.java | 17 ++++++ .../dto/signup/PartnerBatchSignUpItemDTO.java | 35 +++++++++++ .../auth/security/jwt/JwtAuthFilter.java | 1 + .../domain/auth/service/SignUpService.java | 5 ++ .../auth/service/SignUpServiceImpl.java | 60 +++++++++++++++++++ .../server/global/config/SecurityConfig.java | 1 + 7 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/PartnerBatchSignUpItemDTO.java diff --git a/manifest b/manifest index dd9bcc43..781a2b2b 160000 --- a/manifest +++ b/manifest @@ -1 +1 @@ -Subproject commit dd9bcc43b17b9b82f56e09e01470e66dd9a65b50 +Subproject commit 781a2b2be444ffec836367114e0c2baadb588b21 diff --git a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java index 85fffe86..cdcb7d8c 100644 --- a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java +++ b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java @@ -1,11 +1,14 @@ 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; import com.assu.server.domain.auth.dto.phone.PhoneAuthSendRequestDTO; import com.assu.server.domain.auth.dto.phone.PhoneAuthVerifyRequestDTO; 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; @@ -22,6 +25,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; @@ -252,6 +256,19 @@ public BaseResponse signupPartner( return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupPartner(request, licenseImage)); } + @Operation( + summary = "제휴업체 단체 회원가입 API", + description = "이메일, 비밀번호, 업체명, 도로명 주소, 위도, 경도 목록을 받아 제휴업체 계정들을 일괄 생성합니다." + ) + @PostMapping(value = "/partners/batch-signup", consumes = MediaType.APPLICATION_JSON_VALUE) + public BaseResponse> signupBatchPartner( + @RequestBody + @Valid + List requests + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupBatchPartner(requests)); + } + @Operation( summary = "관리자 회원가입 API", description = "# [v1.3 (2026-07-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48)\n" + diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerBatchSignUpItemDTO.java b/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerBatchSignUpItemDTO.java new file mode 100644 index 00000000..d106b799 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerBatchSignUpItemDTO.java @@ -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 +) { +} diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java index 683e0a89..3bdb4806 100644 --- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java +++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java @@ -38,6 +38,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", + "/auth/partners/batch-signup", "/auth/admins/signup", "/auth/commons/login", "/auth/backoffice/login", diff --git a/src/main/java/com/assu/server/domain/auth/service/SignUpService.java b/src/main/java/com/assu/server/domain/auth/service/SignUpService.java index 8995c942..5ba216a5 100644 --- a/src/main/java/com/assu/server/domain/auth/service/SignUpService.java +++ b/src/main/java/com/assu/server/domain/auth/service/SignUpService.java @@ -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 signupBatchPartner(List requests); + SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile signImage); } diff --git a/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java index 0d0ea292..70b5b63a 100644 --- a/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java @@ -198,6 +198,66 @@ public SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFil return SignUpResponseDTO.from(member, null); } + @Override + public List signupBatchPartner(List 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 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()) diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java index 2ec28b53..89ba62c2 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -45,6 +45,7 @@ public SecurityFilterChain filterChain( "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", + "/auth/partners/batch-signup", "/auth/admins/signup", "/auth/commons/login", "/auth/backoffice/login", From 69f68012e6f85bd9d66d6c6779e88013eb937aa8 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 19 Aug 2026 13:39:14 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[FEAT/#423]=20=EC=9D=BC=EA=B4=84=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=EC=9D=84=20=EB=B0=B1?= =?UTF-8?q?=EC=98=A4=ED=94=BC=EC=8A=A4=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/assu/server/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java index 89ba62c2..af97b247 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -45,7 +45,6 @@ public SecurityFilterChain filterChain( "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", - "/auth/partners/batch-signup", "/auth/admins/signup", "/auth/commons/login", "/auth/backoffice/login", @@ -55,6 +54,7 @@ public SecurityFilterChain filterChain( "/auth/students/ssu-verify", "/map/place" ).permitAll() + .requestMatchers("/auth/partners/batch-signup").hasRole("BACKOFFICE") .requestMatchers("/backoffice/**").hasRole("BACKOFFICE") .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/partner/**").hasRole("PARTNER") From 327225f5daf48386ee6dd7f81fdabbaf953f8a8c Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 19 Aug 2026 15:14:23 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[FIX/#425]=20QR=20publish=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../certification/service/CertificationServiceImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java b/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java index 83c6038a..52debe29 100644 --- a/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java +++ b/src/main/java/com/assu/server/domain/certification/service/CertificationServiceImpl.java @@ -101,11 +101,12 @@ public CertificationProgressResponseDTO handleCertification(GroupSessionRequest if (!matched) { List currentCertifiedUserIds = sessionManager.snapshotUserIds(sessionId); CertificationProgressResponseDTO response = new CertificationProgressResponseDTO( - "fail", - currentCertifiedUserIds.size(), "mismatch", + currentCertifiedUserIds.size(), + "학생과 매치되지 않는 정보입니다.", currentCertifiedUserIds ); + messagingTemplate.convertAndSend("/certification/progress/" + sessionId, response); return response; } From de467154f9a5f3d50c7c1a3ac2516c5339c0280f Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 19 Aug 2026 15:51:49 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[FEAT/#423]=20=EB=B0=B1=EC=98=A4=ED=94=BC?= =?UTF-8?q?=EC=8A=A4=20controller=EB=A1=9C=20api=20=EC=9D=B4=EA=B4=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B6=8C=ED=95=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 22 ++++++----------- .../auth/security/jwt/JwtAuthFilter.java | 1 - .../BackofficePartnerController.java | 24 +++++++++++++++++++ .../server/global/config/SecurityConfig.java | 1 - 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java index cdcb7d8c..e70b22c7 100644 --- a/src/main/java/com/assu/server/domain/auth/controller/AuthController.java +++ b/src/main/java/com/assu/server/domain/auth/controller/AuthController.java @@ -8,7 +8,6 @@ import com.assu.server.domain.auth.dto.phone.PhoneAuthSendRequestDTO; import com.assu.server.domain.auth.dto.phone.PhoneAuthVerifyRequestDTO; 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; @@ -16,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; @@ -256,19 +261,6 @@ public BaseResponse signupPartner( return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupPartner(request, licenseImage)); } - @Operation( - summary = "제휴업체 단체 회원가입 API", - description = "이메일, 비밀번호, 업체명, 도로명 주소, 위도, 경도 목록을 받아 제휴업체 계정들을 일괄 생성합니다." - ) - @PostMapping(value = "/partners/batch-signup", consumes = MediaType.APPLICATION_JSON_VALUE) - public BaseResponse> signupBatchPartner( - @RequestBody - @Valid - List requests - ) { - return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupBatchPartner(requests)); - } - @Operation( summary = "관리자 회원가입 API", description = "# [v1.3 (2026-07-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48)\n" + diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java index 3bdb4806..683e0a89 100644 --- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java +++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java @@ -38,7 +38,6 @@ public class JwtAuthFilter extends OncePerRequestFilter { "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", - "/auth/partners/batch-signup", "/auth/admins/signup", "/auth/commons/login", "/auth/backoffice/login", diff --git a/src/main/java/com/assu/server/domain/backoffice/controller/BackofficePartnerController.java b/src/main/java/com/assu/server/domain/backoffice/controller/BackofficePartnerController.java index b5597b89..7ab8e338 100644 --- a/src/main/java/com/assu/server/domain/backoffice/controller/BackofficePartnerController.java +++ b/src/main/java/com/assu/server/domain/backoffice/controller/BackofficePartnerController.java @@ -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; @@ -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 @@ -25,6 +34,7 @@ public class BackofficePartnerController { private final BackofficeMemberService backofficeMemberService; + private final SignUpService signUpService; @Operation( summary = "사업자등록증 조회 API", @@ -68,4 +78,18 @@ public BaseResponse getLicenseUrl( public BaseResponse 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> signupBatchPartner( + @RequestBody + @Valid + List requests + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupBatchPartner(requests)); + } } diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java index af97b247..2ec28b53 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -54,7 +54,6 @@ public SecurityFilterChain filterChain( "/auth/students/ssu-verify", "/map/place" ).permitAll() - .requestMatchers("/auth/partners/batch-signup").hasRole("BACKOFFICE") .requestMatchers("/backoffice/**").hasRole("BACKOFFICE") .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/partner/**").hasRole("PARTNER")