From 611742cfd73b565ed54fd07de32733b339cfce88 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Tue, 15 Jul 2025 13:28:10 +0900 Subject: [PATCH 001/270] =?UTF-8?q?[FEAT/#7]=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/apiPayload/code/BaseCode.java | 5 + .../global/apiPayload/code/BaseErrorCode.java | 5 + .../apiPayload/code/ErrorReasonDTO.java | 14 ++ .../global/apiPayload/code/ReasonDTO.java | 15 ++ .../apiPayload/code/status/ErrorStatus.java | 40 ++++ .../exception/DatabaseException.java | 11 ++ .../exception/exception/GeneralException.java | 18 ++ .../exception/GlobalExceptionAdvice.java | 174 ++++++++++++++++++ .../exception/annotation/CheckPage.java | 19 ++ .../validator/CheckPageValidator.java | 32 ++++ 10 files changed, 333 insertions(+) create mode 100644 src/main/java/com/assu/server/global/apiPayload/code/BaseCode.java create mode 100644 src/main/java/com/assu/server/global/apiPayload/code/BaseErrorCode.java create mode 100644 src/main/java/com/assu/server/global/apiPayload/code/ErrorReasonDTO.java create mode 100644 src/main/java/com/assu/server/global/apiPayload/code/ReasonDTO.java create mode 100644 src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java create mode 100644 src/main/java/com/assu/server/global/exception/exception/DatabaseException.java create mode 100644 src/main/java/com/assu/server/global/exception/exception/GeneralException.java create mode 100644 src/main/java/com/assu/server/global/exception/exception/GlobalExceptionAdvice.java create mode 100644 src/main/java/com/assu/server/global/exception/exception/annotation/CheckPage.java create mode 100644 src/main/java/com/assu/server/global/exception/exception/validator/CheckPageValidator.java diff --git a/src/main/java/com/assu/server/global/apiPayload/code/BaseCode.java b/src/main/java/com/assu/server/global/apiPayload/code/BaseCode.java new file mode 100644 index 0000000..fbef9c9 --- /dev/null +++ b/src/main/java/com/assu/server/global/apiPayload/code/BaseCode.java @@ -0,0 +1,5 @@ +package com.assu.server.global.apiPayload.code; + +public interface BaseCode { + public ReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/BaseErrorCode.java b/src/main/java/com/assu/server/global/apiPayload/code/BaseErrorCode.java new file mode 100644 index 0000000..706a1f6 --- /dev/null +++ b/src/main/java/com/assu/server/global/apiPayload/code/BaseErrorCode.java @@ -0,0 +1,5 @@ +package com.assu.server.global.apiPayload.code; + +public interface BaseErrorCode { + public ErrorReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/ErrorReasonDTO.java b/src/main/java/com/assu/server/global/apiPayload/code/ErrorReasonDTO.java new file mode 100644 index 0000000..4bca93f --- /dev/null +++ b/src/main/java/com/assu/server/global/apiPayload/code/ErrorReasonDTO.java @@ -0,0 +1,14 @@ +package com.assu.server.global.apiPayload.code; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ErrorReasonDTO { + private final HttpStatus httpStatus; + private final String code; + private final String message; + private final boolean isSuccess; +} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/ReasonDTO.java b/src/main/java/com/assu/server/global/apiPayload/code/ReasonDTO.java new file mode 100644 index 0000000..24c72f5 --- /dev/null +++ b/src/main/java/com/assu/server/global/apiPayload/code/ReasonDTO.java @@ -0,0 +1,15 @@ +package com.assu.server.global.apiPayload.code; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ReasonDTO { + + private final HttpStatus httpStatus; + private final boolean isSuccess; + private final String code; + private final String message; +} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java new file mode 100644 index 0000000..ddefa9e --- /dev/null +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -0,0 +1,40 @@ +package com.assu.server.global.apiPayload.code.status; + +import com.assu.server.global.apiPayload.code.BaseErrorCode; +import com.assu.server.global.apiPayload.code.ErrorReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + // 기본 에러 + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), + _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), + _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), + _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + + //페이징 에러 + PAGE_UNDER_ONE(HttpStatus.BAD_REQUEST,"PAGE_4001","페이지는 1이상이여야 합니다."), + + // 멤버 에러 + NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."), + + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .httpStatus(httpStatus) + .build(); + } +} diff --git a/src/main/java/com/assu/server/global/exception/exception/DatabaseException.java b/src/main/java/com/assu/server/global/exception/exception/DatabaseException.java new file mode 100644 index 0000000..90718b2 --- /dev/null +++ b/src/main/java/com/assu/server/global/exception/exception/DatabaseException.java @@ -0,0 +1,11 @@ +package com.assu.server.global.exception.exception; + + +import com.assu.server.global.apiPayload.code.BaseErrorCode; + +public class DatabaseException extends GeneralException { + + public DatabaseException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/assu/server/global/exception/exception/GeneralException.java b/src/main/java/com/assu/server/global/exception/exception/GeneralException.java new file mode 100644 index 0000000..c2a3bed --- /dev/null +++ b/src/main/java/com/assu/server/global/exception/exception/GeneralException.java @@ -0,0 +1,18 @@ +package com.assu.server.global.exception.exception; + + +import com.assu.server.global.apiPayload.code.BaseErrorCode; +import com.assu.server.global.apiPayload.code.ErrorReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GeneralException extends RuntimeException { + + private BaseErrorCode code; + + public ErrorReasonDTO getErrorReasonHttpStatus() { + return this.code.getReasonHttpStatus(); + } +} diff --git a/src/main/java/com/assu/server/global/exception/exception/GlobalExceptionAdvice.java b/src/main/java/com/assu/server/global/exception/exception/GlobalExceptionAdvice.java new file mode 100644 index 0000000..2ffc115 --- /dev/null +++ b/src/main/java/com/assu/server/global/exception/exception/GlobalExceptionAdvice.java @@ -0,0 +1,174 @@ +package com.assu.server.global.exception.exception; + + +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.ErrorReasonDTO; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +@Slf4j +@RestControllerAdvice(annotations = {RestController.class}) +public class GlobalExceptionAdvice extends ResponseEntityExceptionHandler { + + @Override + protected ResponseEntity handleTypeMismatch( + TypeMismatchException e, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String errorMessage = e.getPropertyName() + ": 올바른 값이 아닙니다."; + + return handleExceptionInternalMessage(e, headers, request, errorMessage); + } + + @Override + protected ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException e, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String errorMessage = e.getParameterName() + ": 올바른 값이 아닙니다."; + + return handleExceptionInternalMessage(e, headers, request, errorMessage); + } + + @ExceptionHandler + public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + String errorMessage = + e.getConstraintViolations().stream() + .map(constraintViolation -> constraintViolation.getMessage()) + .findFirst() + .orElseThrow( + () -> + new RuntimeException( + "ConstraintViolationException 추출 도중 에러 발생")); + + return handleExceptionInternalConstraint( + e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request); + } + + @Override + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + + Map errors = new LinkedHashMap<>(); + + e.getBindingResult().getFieldErrors().stream() + .forEach( + fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage; + try { + errorMessage = Optional.ofNullable(ErrorStatus.valueOf(fieldError.getDefaultMessage()).getMessage()).orElse(""); + } catch (IllegalArgumentException ex) { + errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + } + errors.merge( + fieldName, + errorMessage, + (existingErrorMessage, newErrorMessage) -> + existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs( + e, HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"), request, errors); + } + + @ExceptionHandler + public ResponseEntity exception(Exception e, WebRequest request) { + e.printStackTrace(); + + return handleExceptionInternalFalse( + e, + ErrorStatus._INTERNAL_SERVER_ERROR, + HttpHeaders.EMPTY, + ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), + request, + e.getMessage()); + } + + @ExceptionHandler(value = GeneralException.class) + public ResponseEntity onThrowException( + GeneralException generalException, HttpServletRequest request) { + ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); + return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request); + } + + private ResponseEntity handleExceptionInternal( + Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) { + + BaseResponse body = + BaseResponse.onFailure(reason.getCode(), reason.getMessage(), null); + + WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal(e, body, headers, reason.getHttpStatus(), webRequest); + } + + private ResponseEntity handleExceptionInternalFalse( + Exception e, + ErrorStatus errorCommonStatus, + HttpHeaders headers, + HttpStatus status, + WebRequest request, + String errorPoint) { + BaseResponse body = + BaseResponse.onFailure( + errorCommonStatus.getCode(), errorCommonStatus.getMessage(), errorPoint); + return super.handleExceptionInternal(e, body, headers, status, request); + } + + private ResponseEntity handleExceptionInternalArgs( + Exception e, + HttpHeaders headers, + ErrorStatus errorCommonStatus, + WebRequest request, + Map errorArgs) { + BaseResponse body = + BaseResponse.onFailure( + errorCommonStatus.getCode(), errorCommonStatus.getMessage(), errorArgs); + return super.handleExceptionInternal( + e, body, headers, errorCommonStatus.getHttpStatus(), request); + } + + private ResponseEntity handleExceptionInternalConstraint( + Exception e, ErrorStatus errorCommonStatus, HttpHeaders headers, WebRequest request) { + BaseResponse body = + BaseResponse.onFailure( + errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); + return super.handleExceptionInternal( + e, body, headers, errorCommonStatus.getHttpStatus(), request); + } + + private ResponseEntity handleExceptionInternalMessage( + Exception e, HttpHeaders headers, WebRequest request, String errorMessage) { + ErrorStatus errorStatus = ErrorStatus._BAD_REQUEST; + BaseResponse body = + BaseResponse.onFailure( + errorStatus.getCode(), errorStatus.getMessage(), errorMessage); + + return super.handleExceptionInternal( + e, body, headers, errorStatus.getHttpStatus(), request); + } +} diff --git a/src/main/java/com/assu/server/global/exception/exception/annotation/CheckPage.java b/src/main/java/com/assu/server/global/exception/exception/annotation/CheckPage.java new file mode 100644 index 0000000..959c6ca --- /dev/null +++ b/src/main/java/com/assu/server/global/exception/exception/annotation/CheckPage.java @@ -0,0 +1,19 @@ +package com.assu.server.global.exception.exception.annotation; + + +import com.assu.server.global.exception.exception.validator.CheckPageValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckPageValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPage { + + String message() default "페이지가 1보다 작을 수 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/assu/server/global/exception/exception/validator/CheckPageValidator.java b/src/main/java/com/assu/server/global/exception/exception/validator/CheckPageValidator.java new file mode 100644 index 0000000..abdcc84 --- /dev/null +++ b/src/main/java/com/assu/server/global/exception/exception/validator/CheckPageValidator.java @@ -0,0 +1,32 @@ +package com.assu.server.global.exception.exception.validator; + + +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.annotation.CheckPage; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CheckPageValidator implements ConstraintValidator { + + @Override + public void initialize(CheckPage constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + boolean isValid = value > 0; + + if (!isValid) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(ErrorStatus.PAGE_UNDER_ONE.toString()).addConstraintViolation(); + } + + return isValid; + + } +} From 285b7d99b5a19ff7cf47d2627ea63b5ab2e24a16 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Tue, 15 Jul 2025 13:28:40 +0900 Subject: [PATCH 002/270] =?UTF-8?q?[FEAT/#8]=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/admin/entity/Admin.java | 39 ++++++ .../server/domain/common/entity/Member.java | 45 +++++++ .../domain/common/enums/ActivationStatus.java | 5 + .../server/domain/common/enums/UserRole.java | 5 + .../server/domain/partner/entity/Partner.java | 39 ++++++ .../assu/server/domain/user/entity/User.java | 36 ++++++ .../user/entity/enums/EnrollmentStatus.java | 5 + .../domain/user/entity/enums/Major.java | 5 + .../apiPayload/code/status/SuccessStatus.java | 115 ++++++++++++++++++ 9 files changed, 294 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/admin/entity/Admin.java create mode 100644 src/main/java/com/assu/server/domain/common/entity/Member.java create mode 100644 src/main/java/com/assu/server/domain/common/enums/ActivationStatus.java create mode 100644 src/main/java/com/assu/server/domain/common/enums/UserRole.java create mode 100644 src/main/java/com/assu/server/domain/partner/entity/Partner.java create mode 100644 src/main/java/com/assu/server/domain/user/entity/User.java create mode 100644 src/main/java/com/assu/server/domain/user/entity/enums/EnrollmentStatus.java create mode 100644 src/main/java/com/assu/server/domain/user/entity/enums/Major.java create mode 100644 src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java new file mode 100644 index 0000000..271200b --- /dev/null +++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java @@ -0,0 +1,39 @@ +package com.assu.server.domain.admin.entity; + +import com.assu.server.domain.common.entity.Member; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Id; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Admin { + + @Id + private Long id; // member_id와 동일 + + @OneToOne + @MapsId + @JoinColumn(name = "id") + private Member member; + + private String name; + + private String officeAddress; + + private String detailAddress; + + private String signUrl; + + private Boolean isSignVerified; + + private LocalDateTime signVerifiedAt; +} diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java new file mode 100644 index 0000000..e8f70c5 --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/entity/Member.java @@ -0,0 +1,45 @@ +package com.assu.server.domain.common.entity; + +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Entity +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String phoneNum; + + private Boolean isPhoneVerified; + + private LocalDateTime phoneVerifiedAt; + + @Enumerated(EnumType.STRING) + private UserRole role; // User, ADMIN, PARTNER + + @Enumerated(EnumType.STRING) + private ActivationStatus isActivated; // ACTIVE, INACTIVE, SUSPEND + + // 역할별 프로필 - 선택적으로 연관 + @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) + private User studentProfile; + + @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) + private Admin adminProfile; + + @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) + private Partner partnerProfile; + + // 편의 메서드 및 Builder 등 생략 +} + diff --git a/src/main/java/com/assu/server/domain/common/enums/ActivationStatus.java b/src/main/java/com/assu/server/domain/common/enums/ActivationStatus.java new file mode 100644 index 0000000..9ddfab2 --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/enums/ActivationStatus.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.common.enums; + +public enum ActivationStatus { + ACTIVE, INACTIVE, SUSPEND +} diff --git a/src/main/java/com/assu/server/domain/common/enums/UserRole.java b/src/main/java/com/assu/server/domain/common/enums/UserRole.java new file mode 100644 index 0000000..7b18c01 --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/enums/UserRole.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.common.enums; + +public enum UserRole { + USER, ADMIN, PARTNER +} diff --git a/src/main/java/com/assu/server/domain/partner/entity/Partner.java b/src/main/java/com/assu/server/domain/partner/entity/Partner.java new file mode 100644 index 0000000..ddeec3f --- /dev/null +++ b/src/main/java/com/assu/server/domain/partner/entity/Partner.java @@ -0,0 +1,39 @@ +package com.assu.server.domain.partner.entity; + +import com.assu.server.domain.common.entity.Member; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Id; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Partner { + + @Id + private Long id; // member_id와 동일 + + @OneToOne + @MapsId + @JoinColumn(name = "id") + private Member member; + + private String name; + + private String address; + + private String detailAddress; + + private String licenseUrl; + + private Boolean isLicenseVerified; + + private LocalDateTime licenseVerifiedAt; +} diff --git a/src/main/java/com/assu/server/domain/user/entity/User.java b/src/main/java/com/assu/server/domain/user/entity/User.java new file mode 100644 index 0000000..d964f2c --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/entity/User.java @@ -0,0 +1,36 @@ +package com.assu.server.domain.user.entity; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.user.entity.enums.EnrollmentStatus; +import com.assu.server.domain.user.entity.enums.Major; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class User { + @Id + private Long id; + + @OneToOne + @JoinColumn(name = "id") // member_id와 공유 + @MapsId + private Member member; + + private String department; + + @Enumerated(EnumType.STRING) + private EnrollmentStatus enrollmentStatus; + + private String yearSemester; + + private String university; + + private int stamp; + + @Enumerated(EnumType.STRING) + private Major major; +} diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/EnrollmentStatus.java b/src/main/java/com/assu/server/domain/user/entity/enums/EnrollmentStatus.java new file mode 100644 index 0000000..163e0a4 --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/entity/enums/EnrollmentStatus.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.user.entity.enums; + +public enum EnrollmentStatus { + ENROLLED, LEAVE, GRADUATED +} diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java new file mode 100644 index 0000000..3dea912 --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.user.entity.enums; + +public enum Major { + SW, GM, COM, EE, IP +} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java new file mode 100644 index 0000000..0ca43cf --- /dev/null +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java @@ -0,0 +1,115 @@ +package com.assu.server.global.apiPayload.code.status; + +import com.assu.server.global.apiPayload.code.BaseCode; +import com.assu.server.global.apiPayload.code.ReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum SuccessStatus implements BaseCode { + _OK(HttpStatus.OK, "COMMON200", "성공입니다."), + _CREATED(HttpStatus.CREATED, "COMMON201", "요청 성공 및 리소스 생성됨"), + + //멤버 성공 + MEMBER_SUCCESS(HttpStatus.OK, "MEMBER_200", "성공적으로 조회되었습니다."), + MEMBER_CREATED(HttpStatus.CREATED, "MEMBER_201", "성공적으로 생성되었습니다."), + + //옷 성공 + CLOTH_SUCCESS(HttpStatus.OK, "CLOTH_200", "옷이 성공적으로 조회되었습니다."), + CLOTH_CREATED(HttpStatus.CREATED, "CLOTH_201", "옷이 성공적으로 생성되었습니다."), + CLOTH_EDITED(HttpStatus.NO_CONTENT, "CLOTH_204", "옷이 성공적으로 수정되었습니다."), + CLOTH_DELETED(HttpStatus.NO_CONTENT, "CLOTH_204", "옷이 성공적으로 삭제되었습니다."), + + //카테고리 성공 + CATEGORY_SUCCESS(HttpStatus.OK, "CATEGORY_200", "성공적으로 조회되었습니다."), + CATEGORY_CREATED(HttpStatus.CREATED, "CATEGORY_201", "성공적으로 생성되었습니다."), + + //폴더 성공 + FOLDER_SUCCESS(HttpStatus.OK, "FOLDER_200", "성공적으로 조회되었습니다."), + FOLDER_CREATED(HttpStatus.CREATED, "FOLDER_201", "성공적으로 생성되었습니다."), + FOLDER_DELETED(HttpStatus.NO_CONTENT, "FOLDER_204", "성공적으로 삭제되었습니다."), + FOLDER_EDIT_SUCCESS(HttpStatus.NO_CONTENT, "FOLDER_204", "성공적으로 수정되었습니다."), + FOLDER_ADD_CLOTHES_SUCCESS(HttpStatus.CREATED, "FOLDER_201", "성공적으로 추가되었습니다."), + FOLDER_DELETE_CLOTHES_SUCCESS(HttpStatus.NO_CONTENT, "FOLDER_204", "성공적으로 삭제되었습니다."), + FOLDER_CLOTHES_SUCCESS(HttpStatus.OK, "FOLDER_200", "성공적으로 반영되었습니다."), + + //검색 성공 + SEARCH_SUCCESS(HttpStatus.OK, "SEARCH_200", "성공적으로 조회되었습니다."), + + //기록 성공 + HISTORY_SUCCESS(HttpStatus.OK, "HISTORY_200", "성공적으로 조회되었습니다."), + HISTORY_CREATED(HttpStatus.CREATED, "HISTORY_201", "성공적으로 생성되었습니다."), + HISTORY_LIKE_STATUS_CHANGED(HttpStatus.OK,"HISTORY_200","좋아요 상태가 성공적으로 변경되었습니다."), + HISTORY_COMMENT_CREATED(HttpStatus.CREATED,"HISTORY_201","성공적으로 댓글이 생성되었습니다."), + HISTORY_UPDATED(HttpStatus.NO_CONTENT,"HISTORY_204","성공적으로 수정되었습니다"), + HISTORY_COMMENT_DELETED(HttpStatus.NO_CONTENT,"HISTORY_204","댓글이 성공적으로 삭제되었습니다"), + HISTORY_COMMENT_UPDATED(HttpStatus.NO_CONTENT,"HISTORY_204","댓글이 성공적으로 수정되었습니다"), + HISTORY_DELETED(HttpStatus.NO_CONTENT,"HISTORY_204","기록이 성공적으로 삭제되었습니다"), + HISTORY_LIKE_USER(HttpStatus.OK,"HISTORY_200","기록의 좋아요를 누른 유저 정보를 성공적으로 조회했습니다."), + HISTORY_CHECK_SUCCESS(HttpStatus.OK, "HISTORY_200","나의 기록인지 성공적으로 조회했습니다."), + + //알림 성공 + NOTIFICATION_SUCCESS(HttpStatus.OK, "NOTIFICATION_200", "성공적으로 조회되었습니다."), + UNREAD_NOTIFICATION_CHECKED(HttpStatus.OK,"NOTIFICATION_200","읽지 않은 알림 여부가 성공적으로 조회되었습니다."), + NOTIFICATION_READ(HttpStatus.NO_CONTENT,"NOTIFICATION_204","알림이 성공적으로 읽음 처리되었습니다."), + NOTIFICATION_SEND_SUCCESS(HttpStatus.NO_CONTENT,"NOTIFICATION_204","알림이 성공적으로 발송되었습니다"), + NOTIFICATION_HISTORY_LIKED_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","기록 좋아요 알림이 성공적으로 발송되었습니다."), + NOTIFICATION_NEW_FOLLOWER_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","팔로우 알림이 성공적으로 발송되었습니다."), + NOTIFICATION_HISTORY_COMMENT_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","기록 댓글 알림이 성공적으로 발송되었습니다."), + NOTIFICATION_REPLY_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","댓글에 대한 답글 알림이 성공적으로 발송되었습니다."), + + //홈 성공 + HOME_SUCCESS(HttpStatus.OK, "HOME_200", "성공적으로 조회되었습니다."), + + //기타 멤버 관련 성공 + MEMBER_ACTION_SUCCESS(HttpStatus.OK, "MEMBER_ACTION_200", "멤버 관련 요소가 성공적으로 조회되었습니다."), + MEMBER_ACTION_CREATED(HttpStatus.CREATED, "MEMBER_ACTION_201", "멤버 관련 요소가 성공적으로 생성되었습니다."), + MEMBER_ACTION_EDITED(HttpStatus.OK, "MEMBER_ACTION_204", "멤버 관련 요소가 성공적으로 수정되었습니다."), + + + //아이디 성공 + MEMBER_ID_SUCCESS(HttpStatus.OK, "MEMBER_ID_200", "사용가능한 아이디입니다."), + + //로그인 성공 + LOGIN_SUCCESS(HttpStatus.OK, "LOGIN_200", "로그인에 성공하였습니다."), + LOGIN_CREATED(HttpStatus.CREATED, "LOGIN_201", "회원가입과 로그인에 성공하였습니다."), + LOGIN_UPDATED(HttpStatus.NO_CONTENT, "LOGIN_204", "로그인 정보가 성공적으로 수정되었습니다."), + + //로그아웃 성공 + LOGOUT_SUCCESS(HttpStatus.OK, "LOGOUT_200", "로그아웃에 성공하였습니다."), + UNLINK_SUCCESS(HttpStatus.OK, "UNLINK_200", "회원탈퇴에 성공하였습니다."), + + //Elastic Search 인덱스 생성 및 동기화 성공 + CLOTH_SYNC_CREATED(HttpStatus.CREATED, "SEARCH_201", "옷 검색 인덱스가 성공적으로 생성되었습니다."), + HISTORY_SYNC_CREATED(HttpStatus.CREATED, "SEARCH_201", "기록 검색 인덱스가 성공적으로 생성되었습니다."), + MEMBER_SYNC_CREATED(HttpStatus.CREATED, "SEARCH_201", "유저 검색 인덱스가 성공적으로 생성되었습니다."), + + + //신고 성공 + REPORT_HISTORY_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "기록 신고의 정보가 성공적으로 조회되었습니다."), + REPORT_HISTORY_SUCCESS(HttpStatus.OK, "REPORT_201", "기록을 성공적으로 신고했습니다."), + REPORT_COMMENT_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "댓글 신고의 정보가 성공적으로 조회되었습니다."), + REPORT_COMMENT_SUCCESS(HttpStatus.OK, "REPORT_201", "댓글을 성공적으로 신고했습니다."), + REPORT_PROFILE_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "계정 신고의 정보가 성공적으로 조회되었습니다."), + REPORT_PROFILE_SUCCESS(HttpStatus.OK, "REPORT_201", "계정을 성공적으로 신고했습니다."), + REPORT_ADMIN_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200","관리자용 신고 기록이 성공적으로 조회되었습니다."), + REPORT_ADMIN_PROCESSED(HttpStatus.OK,"REPORT_204","신고가 성공적으로 처리되었습니다.") + + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDTO getReasonHttpStatus() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .httpStatus(httpStatus) + .build(); + } +} From c3db1a9337b12a5a8f1919862c5b510d9326e2d0 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Tue, 15 Jul 2025 13:29:02 +0900 Subject: [PATCH 003/270] =?UTF-8?q?[FEAT/#8]=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EC=A6=88=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index d88fee3..29f8d3b 100644 --- a/build.gradle +++ b/build.gradle @@ -24,23 +24,40 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-amqp' - implementation 'org.springframework.boot:spring-boot-starter-batch' + // Spring boot implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + + // spring security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // rabbit mq + implementation 'org.springframework.boot:spring-boot-starter-amqp' + testImplementation 'org.springframework.amqp:spring-rabbit-test' + + // batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + testImplementation 'org.springframework.batch:spring-batch-test' + + // lombok compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-docker-compose' + annotationProcessor 'org.projectlombok:lombok' + // Swagger 3 (SpringDoc OpenAPI) + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + + // maria db runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' - annotationProcessor 'org.projectlombok:lombok' + + // spring test testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.amqp:spring-rabbit-test' - testImplementation 'org.springframework.batch:spring-batch-test' - testImplementation 'org.springframework.security:spring-security-test' + + // junit testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // h2 db (test) runtimeOnly 'com.h2database:h2' } From aca5c569fb23736a0e71c8c1b7753d819816428d Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 16 Jul 2025 11:25:39 +0900 Subject: [PATCH 004/270] =?UTF-8?q?[FEAT/#11]=20Term=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=EC=A1=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/domain/term/controller/TermController.java | 4 ++++ .../assu/server/domain/term/converter/TermConverter.java | 4 ++++ .../com/assu/server/domain/term/dto/TermRequestDTO.java | 4 ++++ .../com/assu/server/domain/term/dto/TermResponseDTO.java | 4 ++++ src/main/java/com/assu/server/domain/term/entity/Term.java | 4 ++++ .../assu/server/domain/term/repository/TermRepository.java | 4 ++++ .../com/assu/server/domain/term/service/TermService.java | 7 +++++++ .../assu/server/domain/term/service/TermServiceImpl.java | 4 ++++ 8 files changed, 35 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/term/controller/TermController.java create mode 100644 src/main/java/com/assu/server/domain/term/converter/TermConverter.java create mode 100644 src/main/java/com/assu/server/domain/term/dto/TermRequestDTO.java create mode 100644 src/main/java/com/assu/server/domain/term/dto/TermResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/term/entity/Term.java create mode 100644 src/main/java/com/assu/server/domain/term/repository/TermRepository.java create mode 100644 src/main/java/com/assu/server/domain/term/service/TermService.java create mode 100644 src/main/java/com/assu/server/domain/term/service/TermServiceImpl.java diff --git a/src/main/java/com/assu/server/domain/term/controller/TermController.java b/src/main/java/com/assu/server/domain/term/controller/TermController.java new file mode 100644 index 0000000..abd9dfb --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/controller/TermController.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.term.controller; + +public class TermController { +} diff --git a/src/main/java/com/assu/server/domain/term/converter/TermConverter.java b/src/main/java/com/assu/server/domain/term/converter/TermConverter.java new file mode 100644 index 0000000..a5da940 --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/converter/TermConverter.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.term.converter; + +public class TermConverter { +} diff --git a/src/main/java/com/assu/server/domain/term/dto/TermRequestDTO.java b/src/main/java/com/assu/server/domain/term/dto/TermRequestDTO.java new file mode 100644 index 0000000..65ec572 --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/dto/TermRequestDTO.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.term.dto; + +public class TermRequestDTO { +} diff --git a/src/main/java/com/assu/server/domain/term/dto/TermResponseDTO.java b/src/main/java/com/assu/server/domain/term/dto/TermResponseDTO.java new file mode 100644 index 0000000..2d45f0e --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/dto/TermResponseDTO.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.term.dto; + +public class TermResponseDTO { +} diff --git a/src/main/java/com/assu/server/domain/term/entity/Term.java b/src/main/java/com/assu/server/domain/term/entity/Term.java new file mode 100644 index 0000000..68cb0bb --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/entity/Term.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.term.entity; + +public class Term { +} diff --git a/src/main/java/com/assu/server/domain/term/repository/TermRepository.java b/src/main/java/com/assu/server/domain/term/repository/TermRepository.java new file mode 100644 index 0000000..7ffd80e --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/repository/TermRepository.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.term.repository; + +public class TermRepository { +} diff --git a/src/main/java/com/assu/server/domain/term/service/TermService.java b/src/main/java/com/assu/server/domain/term/service/TermService.java new file mode 100644 index 0000000..e90bbf6 --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/service/TermService.java @@ -0,0 +1,7 @@ +package com.assu.server.domain.term.service; + +import org.springframework.stereotype.Service; + +@Service +public interface TermService { +} diff --git a/src/main/java/com/assu/server/domain/term/service/TermServiceImpl.java b/src/main/java/com/assu/server/domain/term/service/TermServiceImpl.java new file mode 100644 index 0000000..b3d80b2 --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/service/TermServiceImpl.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.term.service; + +public class TermServiceImpl implements TermService { +} From 570c7d92a5c80211084f941b2fb1d7a1332f9ec3 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 16 Jul 2025 14:30:53 +0900 Subject: [PATCH 005/270] =?UTF-8?q?[FEAT/#11]=20Entity=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/AssociateCertification.java | 44 ++++++++++++++++ .../certification/entity/Certification.java | 4 -- .../certification/entity/QRCertification.java | 37 +++++++++++++ .../domain/chat/entity/ChattingRoom.java | 44 ++++++++++++++++ .../server/domain/chat/entity/Message.java | 45 ++++++++++++++++ .../domain/chat/entity/enums/MessageType.java | 5 ++ .../domain/common/entity/CommonAuth.java | 36 +++++++++++++ .../server/domain/common/entity/SSUAuth.java | 34 ++++++++++++ .../notificaiton/entity/Notification.java | 39 +++++++++++++- .../domain/partnership/entity/Paper.java | 50 ++++++++++++++++++ .../partnership/entity/PaperContent.java | 51 ++++++++++++++++++ .../domain/partnership/entity/Patnership.java | 4 -- .../entity/enums/PaperContentType.java | 5 ++ .../server/domain/review/entity/Review.java | 52 ++++++++++++++++++- .../domain/review/entity/ReviewPhoto.java | 34 ++++++++++++ .../server/domain/store/entity/Store.java | 46 +++++++++++++++- .../domain/suggestion/entity/Suggestion.java | 38 +++++++++++++- .../assu/server/domain/term/entity/Term.java | 40 +++++++++++++- .../term/entity/mapping/TermAgreement.java | 36 +++++++++++++ .../domain/user/entity/PartnershipUsage.java | 40 ++++++++++++++ .../server/domain/user/entity/UserPaper.java | 40 ++++++++++++++ 21 files changed, 708 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java delete mode 100644 src/main/java/com/assu/server/domain/certification/entity/Certification.java create mode 100644 src/main/java/com/assu/server/domain/certification/entity/QRCertification.java create mode 100644 src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java create mode 100644 src/main/java/com/assu/server/domain/chat/entity/Message.java create mode 100644 src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java create mode 100644 src/main/java/com/assu/server/domain/common/entity/CommonAuth.java create mode 100644 src/main/java/com/assu/server/domain/common/entity/SSUAuth.java create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/Paper.java create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/entity/Patnership.java create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java create mode 100644 src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java create mode 100644 src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java create mode 100644 src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java create mode 100644 src/main/java/com/assu/server/domain/user/entity/UserPaper.java diff --git a/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java b/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java new file mode 100644 index 0000000..ee95d3e --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java @@ -0,0 +1,44 @@ +package com.assu.server.domain.certification.entity; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.user.entity.User; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class AssociateCertification extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer tableNumber; + private Boolean isCertified; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id") + private Store store; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "partner_id") + private Partner partner; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/entity/Certification.java b/src/main/java/com/assu/server/domain/certification/entity/Certification.java deleted file mode 100644 index 33d1695..0000000 --- a/src/main/java/com/assu/server/domain/certification/entity/Certification.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.certification.entity; - -public class Certification { -} diff --git a/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java b/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java new file mode 100644 index 0000000..bb42589 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java @@ -0,0 +1,37 @@ +package com.assu.server.domain.certification.entity; + +import java.time.LocalDateTime; + +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.user.entity.User; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class QRCertification extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private User user; + + private Boolean isVerified; + private LocalDateTime verifiedTime; +} diff --git a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java new file mode 100644 index 0000000..cda9072 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java @@ -0,0 +1,44 @@ +package com.assu.server.domain.chat.entity; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partner.entity.Partner; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class ChattingRoom extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Enumerated(EnumType.STRING) + private ActivationStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "admin_id") + private Admin admin; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "partner_id") + private Partner partner; +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java new file mode 100644 index 0000000..7422897 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -0,0 +1,45 @@ +package com.assu.server.domain.chat.entity; +import java.time.LocalDateTime; + +import com.assu.server.domain.chat.entity.enums.MessageType; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Message { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id") + private ChattingRoom chattingRoom; + + @Enumerated(EnumType.STRING) + private MessageType type; + + private String content; + + private LocalDateTime sendTime; + private LocalDateTime readTime; + + private Boolean isRead; + + +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java b/src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java new file mode 100644 index 0000000..a3255d2 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.chat.entity.enums; + +public enum MessageType { + TEXT, PROPOSAL, SYSTEM +} diff --git a/src/main/java/com/assu/server/domain/common/entity/CommonAuth.java b/src/main/java/com/assu/server/domain/common/entity/CommonAuth.java new file mode 100644 index 0000000..f12bd70 --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/entity/CommonAuth.java @@ -0,0 +1,36 @@ +package com.assu.server.domain.common.entity; +import java.time.LocalDateTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class CommonAuth extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + private String email; + private String password; + private Boolean isEmailVerified; + private LocalDateTime lastLoginAt; + +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java new file mode 100644 index 0000000..dfbb779 --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java @@ -0,0 +1,34 @@ +package com.assu.server.domain.common.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class SSUAuth extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name="member_id") + private Member member; + + private String passwordCipher; + private Boolean isAuthenticated; + private LocalDateTime authenticated_at; +} diff --git a/src/main/java/com/assu/server/domain/notificaiton/entity/Notification.java b/src/main/java/com/assu/server/domain/notificaiton/entity/Notification.java index aa4f081..cded40c 100644 --- a/src/main/java/com/assu/server/domain/notificaiton/entity/Notification.java +++ b/src/main/java/com/assu/server/domain/notificaiton/entity/Notification.java @@ -1,4 +1,39 @@ package com.assu.server.domain.notificaiton.entity; +import com.assu.server.domain.certification.entity.QRCertification; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.partner.entity.Partner; -public class Notification { -} +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Notification extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "qr_id") + private QRCertification qrVerification; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "partner_id") + private Partner partner; + + private String content; + private Boolean isChecked; +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java new file mode 100644 index 0000000..faeb633 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java @@ -0,0 +1,50 @@ +package com.assu.server.domain.partnership.entity; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.store.entity.Store; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Paper extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String partnershipPeriod; // 이게 뭘로 들어오는거지. 그냥 LocalDate 로 하는게 낫지 않나? + + @Enumerated(EnumType.STRING) + private ActivationStatus isActivated; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "admin_id") + private Admin admin; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "partner_id") + private Partner partner; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id") + private Store store; + +} diff --git a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java new file mode 100644 index 0000000..29e3195 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java @@ -0,0 +1,51 @@ +package com.assu.server.domain.partnership.entity; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.partnership.entity.enums.PaperContentType; +import com.assu.server.domain.user.entity.enums.Major; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PaperContent extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paper_id") + private Paper paper; + + @Enumerated(EnumType.STRING) + private PaperContentType type; + + private Integer people; + + private String belonging; + + private Long cost; + + private Long discount; + + private String goods; + + @Enumerated(EnumType.STRING) + private Major major; + +} diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Patnership.java b/src/main/java/com/assu/server/domain/partnership/entity/Patnership.java deleted file mode 100644 index 55d828a..0000000 --- a/src/main/java/com/assu/server/domain/partnership/entity/Patnership.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.entity; - -public class Patnership { -} diff --git a/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java b/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java new file mode 100644 index 0000000..80f7023 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.partnership.entity.enums; + +public enum PaperContentType{ + PEOPLE, BELONGING, COST +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/review/entity/Review.java b/src/main/java/com/assu/server/domain/review/entity/Review.java index 01603a4..33a74a1 100644 --- a/src/main/java/com/assu/server/domain/review/entity/Review.java +++ b/src/main/java/com/assu/server/domain/review/entity/Review.java @@ -1,4 +1,54 @@ package com.assu.server.domain.review.entity; +import java.util.ArrayList; +import java.util.List; -public class Review { +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.user.entity.User; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Review extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "partner_id") + private Partner partner; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id") + private Store store; + + @OneToMany(mappedBy= "review", cascade = CascadeType.ALL) + private List imageList = new ArrayList<>(); + + private Integer rate; + private String content; } diff --git a/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java b/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java new file mode 100644 index 0000000..187b36a --- /dev/null +++ b/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java @@ -0,0 +1,34 @@ +package com.assu.server.domain.review.entity; +import com.assu.server.domain.common.entity.BaseEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class ReviewPhoto extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id") + private Review review; + + private String photoUrl; +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java index 40005b6..f808887 100644 --- a/src/main/java/com/assu/server/domain/store/entity/Store.java +++ b/src/main/java/com/assu/server/domain/store/entity/Store.java @@ -1,4 +1,46 @@ package com.assu.server.domain.store.entity; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partner.entity.Partner; -public class Store { -} +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Store extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "partner_id") + private Partner partner; + + private Integer rate; + + @Enumerated(EnumType.STRING) + private ActivationStatus isActivate; + + private String name; + + private String adderess; + + private String detailAddress; + +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java index 5354c8d..4a9b2e8 100644 --- a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java +++ b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java @@ -1,4 +1,40 @@ package com.assu.server.domain.suggestion.entity; -public class Suggestion { +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.user.entity.User; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Entity +@AllArgsConstructor +@RequiredArgsConstructor +@Builder +@Getter +public class Suggestion extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "admin_id") + private Admin admin; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private String shopName; + private String content; } diff --git a/src/main/java/com/assu/server/domain/term/entity/Term.java b/src/main/java/com/assu/server/domain/term/entity/Term.java index 68cb0bb..dcece7d 100644 --- a/src/main/java/com/assu/server/domain/term/entity/Term.java +++ b/src/main/java/com/assu/server/domain/term/entity/Term.java @@ -1,4 +1,40 @@ package com.assu.server.domain.term.entity; -public class Term { -} +import java.time.LocalDate; + +import com.assu.server.domain.common.entity.BaseEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Term extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + private String content; + + private Boolean isAgreed; + + private LocalDate agreedDate; + + private LocalDate disagreedDate; +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java b/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java new file mode 100644 index 0000000..4bd281c --- /dev/null +++ b/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java @@ -0,0 +1,36 @@ +package com.assu.server.domain.term.entity.mapping; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.term.entity.Term; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class TermAgreement extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "term_id") + private Term term; +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java new file mode 100644 index 0000000..901b294 --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java @@ -0,0 +1,40 @@ +package com.assu.server.domain.user.entity; +import java.time.LocalDate; + +import com.assu.server.domain.common.entity.BaseEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PartnershipUsage extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private String place; + private LocalDate date; + private String partnershipContent; + private Boolean isReviewed; + private Integer discount; + + +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/user/entity/UserPaper.java b/src/main/java/com/assu/server/domain/user/entity/UserPaper.java new file mode 100644 index 0000000..be3b67b --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/entity/UserPaper.java @@ -0,0 +1,40 @@ +package com.assu.server.domain.user.entity; +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.domain.partnership.entity.PaperContent; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class UserPaper extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id") // 제안서 내용 id + private PaperContent paperContent; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paper_id") + private Paper paper; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; +} \ No newline at end of file From 82adb0544f241a58dbbaa3958c8d6f13e3a986e1 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Thu, 17 Jul 2025 14:35:49 +0900 Subject: [PATCH 006/270] =?UTF-8?q?[MOD/#11]=20User(=EC=98=88=EC=95=BD?= =?UTF-8?q?=EC=96=B4=20=EC=9D=B4=EC=8A=88)=20->=20Student=20=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/certification/entity/AssociateCertification.java | 6 +++--- .../server/domain/certification/entity/QRCertification.java | 4 ++-- src/main/java/com/assu/server/domain/chat/entity/Chat.java | 4 ---- .../java/com/assu/server/domain/common/entity/Member.java | 4 ++-- .../java/com/assu/server/domain/review/entity/Review.java | 6 ++---- .../assu/server/domain/suggestion/entity/Suggestion.java | 6 +++--- .../{UserController.java => StudentController.java} | 2 +- .../converter/{UserConverter.java => StudentConverter.java} | 2 +- .../dto/{UserResponseDTO.java => StudentRequestDTO.java} | 2 +- .../dto/{UserRequestDTO.java => StudentResponseDTO.java} | 2 +- .../assu/server/domain/user/entity/PartnershipUsage.java | 4 ++-- .../server/domain/user/entity/{User.java => Student.java} | 2 +- .../java/com/assu/server/domain/user/entity/UserPaper.java | 4 ++-- .../{UserRepository.java => StudentRepository.java} | 2 +- .../user/service/{UserService.java => StudentService.java} | 2 +- .../{UserServiceImpl.java => StudentServiceImpl.java} | 2 +- 16 files changed, 24 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/com/assu/server/domain/chat/entity/Chat.java rename src/main/java/com/assu/server/domain/user/controller/{UserController.java => StudentController.java} (60%) rename src/main/java/com/assu/server/domain/user/converter/{UserConverter.java => StudentConverter.java} (60%) rename src/main/java/com/assu/server/domain/user/dto/{UserResponseDTO.java => StudentRequestDTO.java} (57%) rename src/main/java/com/assu/server/domain/user/dto/{UserRequestDTO.java => StudentResponseDTO.java} (56%) rename src/main/java/com/assu/server/domain/user/entity/{User.java => Student.java} (97%) rename src/main/java/com/assu/server/domain/user/repository/{UserRepository.java => StudentRepository.java} (60%) rename src/main/java/com/assu/server/domain/user/service/{UserService.java => StudentService.java} (58%) rename src/main/java/com/assu/server/domain/user/service/{UserServiceImpl.java => StudentServiceImpl.java} (58%) diff --git a/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java b/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java index ee95d3e..7befda2 100644 --- a/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java +++ b/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java @@ -2,7 +2,7 @@ import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.store.entity.Store; -import com.assu.server.domain.user.entity.User; +import com.assu.server.domain.user.entity.Student; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -39,6 +39,6 @@ public class AssociateCertification extends BaseEntity { private Partner partner; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; + @JoinColumn(name = "student_id") + private Student student; } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java b/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java index bb42589..588f380 100644 --- a/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java +++ b/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java @@ -3,7 +3,7 @@ import java.time.LocalDateTime; import com.assu.server.domain.common.entity.BaseEntity; -import com.assu.server.domain.user.entity.User; +import com.assu.server.domain.user.entity.Student; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -30,7 +30,7 @@ public class QRCertification extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") - private User user; + private Student student; private Boolean isVerified; private LocalDateTime verifiedTime; diff --git a/src/main/java/com/assu/server/domain/chat/entity/Chat.java b/src/main/java/com/assu/server/domain/chat/entity/Chat.java deleted file mode 100644 index 3cc45c0..0000000 --- a/src/main/java/com/assu/server/domain/chat/entity/Chat.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.chat.entity; - -public class Chat { -} diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java index e8f70c5..4687dbc 100644 --- a/src/main/java/com/assu/server/domain/common/entity/Member.java +++ b/src/main/java/com/assu/server/domain/common/entity/Member.java @@ -4,7 +4,7 @@ import com.assu.server.domain.common.enums.UserRole; import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.partner.entity.Partner; -import com.assu.server.domain.user.entity.User; +import com.assu.server.domain.user.entity.Student; import jakarta.persistence.*; import lombok.Getter; @@ -32,7 +32,7 @@ public class Member { // 역할별 프로필 - 선택적으로 연관 @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) - private User studentProfile; + private Student studentProfile; @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) private Admin adminProfile; diff --git a/src/main/java/com/assu/server/domain/review/entity/Review.java b/src/main/java/com/assu/server/domain/review/entity/Review.java index 33a74a1..0045706 100644 --- a/src/main/java/com/assu/server/domain/review/entity/Review.java +++ b/src/main/java/com/assu/server/domain/review/entity/Review.java @@ -5,12 +5,10 @@ import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.store.entity.Store; -import com.assu.server.domain.user.entity.User; +import com.assu.server.domain.user.entity.Student; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -36,7 +34,7 @@ public class Review extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "student_id") - private User user; + private Student student; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "partner_id") diff --git a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java index 4a9b2e8..7a22608 100644 --- a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java +++ b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java @@ -3,7 +3,7 @@ import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.store.entity.Store; -import com.assu.server.domain.user.entity.User; +import com.assu.server.domain.user.entity.Student; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -32,8 +32,8 @@ public class Suggestion extends BaseEntity { private Admin admin; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; + @JoinColumn(name = "student_id") + private Student student; private String shopName; private String content; diff --git a/src/main/java/com/assu/server/domain/user/controller/UserController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java similarity index 60% rename from src/main/java/com/assu/server/domain/user/controller/UserController.java rename to src/main/java/com/assu/server/domain/user/controller/StudentController.java index 3e59480..303f234 100644 --- a/src/main/java/com/assu/server/domain/user/controller/UserController.java +++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java @@ -1,4 +1,4 @@ package com.assu.server.domain.user.controller; -public class UserController { +public class StudentController { } diff --git a/src/main/java/com/assu/server/domain/user/converter/UserConverter.java b/src/main/java/com/assu/server/domain/user/converter/StudentConverter.java similarity index 60% rename from src/main/java/com/assu/server/domain/user/converter/UserConverter.java rename to src/main/java/com/assu/server/domain/user/converter/StudentConverter.java index 97c6d6f..8a937ab 100644 --- a/src/main/java/com/assu/server/domain/user/converter/UserConverter.java +++ b/src/main/java/com/assu/server/domain/user/converter/StudentConverter.java @@ -1,4 +1,4 @@ package com.assu.server.domain.user.converter; -public class UserConverter { +public class StudentConverter { } diff --git a/src/main/java/com/assu/server/domain/user/dto/UserResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentRequestDTO.java similarity index 57% rename from src/main/java/com/assu/server/domain/user/dto/UserResponseDTO.java rename to src/main/java/com/assu/server/domain/user/dto/StudentRequestDTO.java index 935e4e2..7507793 100644 --- a/src/main/java/com/assu/server/domain/user/dto/UserResponseDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentRequestDTO.java @@ -1,4 +1,4 @@ package com.assu.server.domain.user.dto; -public class UserResponseDTO { +public class StudentRequestDTO { } diff --git a/src/main/java/com/assu/server/domain/user/dto/UserRequestDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java similarity index 56% rename from src/main/java/com/assu/server/domain/user/dto/UserRequestDTO.java rename to src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java index 99ab532..1ebaae2 100644 --- a/src/main/java/com/assu/server/domain/user/dto/UserRequestDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java @@ -1,4 +1,4 @@ package com.assu.server.domain.user.dto; -public class UserRequestDTO { +public class StudentResponseDTO { } diff --git a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java index 901b294..85959cf 100644 --- a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java +++ b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java @@ -27,8 +27,8 @@ public class PartnershipUsage extends BaseEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; + @JoinColumn(name = "student_id") + private Student student; private String place; private LocalDate date; diff --git a/src/main/java/com/assu/server/domain/user/entity/User.java b/src/main/java/com/assu/server/domain/user/entity/Student.java similarity index 97% rename from src/main/java/com/assu/server/domain/user/entity/User.java rename to src/main/java/com/assu/server/domain/user/entity/Student.java index d964f2c..89544b0 100644 --- a/src/main/java/com/assu/server/domain/user/entity/User.java +++ b/src/main/java/com/assu/server/domain/user/entity/Student.java @@ -11,7 +11,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder -public class User { +public class Student { @Id private Long id; diff --git a/src/main/java/com/assu/server/domain/user/entity/UserPaper.java b/src/main/java/com/assu/server/domain/user/entity/UserPaper.java index be3b67b..a00962c 100644 --- a/src/main/java/com/assu/server/domain/user/entity/UserPaper.java +++ b/src/main/java/com/assu/server/domain/user/entity/UserPaper.java @@ -35,6 +35,6 @@ public class UserPaper extends BaseEntity { private Paper paper; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; + @JoinColumn(name = "student_id") + private Student student; } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/user/repository/UserRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java similarity index 60% rename from src/main/java/com/assu/server/domain/user/repository/UserRepository.java rename to src/main/java/com/assu/server/domain/user/repository/StudentRepository.java index 1c3be7b..e042199 100644 --- a/src/main/java/com/assu/server/domain/user/repository/UserRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java @@ -1,4 +1,4 @@ package com.assu.server.domain.user.repository; -public class UserRepository { +public class StudentRepository { } diff --git a/src/main/java/com/assu/server/domain/user/service/UserService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java similarity index 58% rename from src/main/java/com/assu/server/domain/user/service/UserService.java rename to src/main/java/com/assu/server/domain/user/service/StudentService.java index d73902e..84c57a1 100644 --- a/src/main/java/com/assu/server/domain/user/service/UserService.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java @@ -1,4 +1,4 @@ package com.assu.server.domain.user.service; -public interface UserService { +public interface StudentService { } diff --git a/src/main/java/com/assu/server/domain/user/service/UserServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java similarity index 58% rename from src/main/java/com/assu/server/domain/user/service/UserServiceImpl.java rename to src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java index 3c088c9..61a060f 100644 --- a/src/main/java/com/assu/server/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java @@ -1,4 +1,4 @@ package com.assu.server.domain.user.service; -public class UserServiceImpl { +public class StudentServiceImpl { } From 90db8db2fcd73278b17956f1243488e7d3d43e4b Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 23 Jul 2025 10:12:57 +0900 Subject: [PATCH 007/270] =?UTF-8?q?[MOD/#11]=20yml=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/assu/server/domain/common/entity/SSUAuth.java | 2 +- src/main/resources/application.yml | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java index dfbb779..20b3461 100644 --- a/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java +++ b/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java @@ -21,7 +21,7 @@ @AllArgsConstructor public class SSUAuth extends BaseEntity { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToOne(fetch = FetchType.LAZY) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 68e093c..692f20a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,11 +1,12 @@ spring: + profiles: + active: local # 여기에 local, blue, green 셋중 하나로 입력 config: import: - - optional:classpath:application-secret.yml - - optional:file:/app/config/application-secret.yml + - classpath:application-secret.yml jpa: hibernate: - ddl-auto: update + ddl-auto: update # 여기 properties: hibernate: jdbc: From e5910cf7c116d86ea9dcf57e10c7542faf28fc20 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Fri, 25 Jul 2025 15:36:58 +0900 Subject: [PATCH 008/270] =?UTF-8?q?feat/#16=20-=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80(websocket)=20-=20settin?= =?UTF-8?q?gs.gradle=EC=97=90=EB=8F=84=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20Config,=20Handler=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ settings.gradle | 2 +- .../domain/chat/config/WebSocketConfig.java | 24 +++++++++++++ .../domain/chat/entity/ChattingRoom.java | 16 ++++----- .../server/domain/chat/entity/Message.java | 3 +- .../chat/handler/ChatWebSocketHandler.java | 36 +++++++++++++++++++ 6 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java create mode 100644 src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java diff --git a/build.gradle b/build.gradle index 29f8d3b..f2faa18 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ dependencies { // Swagger 3 (SpringDoc OpenAPI) implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + // chatting + implementation 'org.springframework.book:spring-boot-starter-websocket' + // maria db runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' diff --git a/settings.gradle b/settings.gradle index 096502d..5d4a021 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'server' +rootProject.name = 'Assu' diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java new file mode 100644 index 0000000..f7d137e --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java @@ -0,0 +1,24 @@ +package com.assu.server.domain.chat.config; + +import com.assu.server.domain.chat.handler.ChatWebSocketHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + private final ChatWebSocketHandler chatWebSocketHandler; + + public WebSocketConfig (ChatWebSocketHandler chatWebSocketHandler) { + this.chatWebSocketHandler = chatWebSocketHandler; + } + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(chatWebSocketHandler, "/ws/chat") + .setAllowedOrigins("*"); + } +} diff --git a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java index cda9072..0d7488e 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java +++ b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java @@ -5,20 +5,14 @@ import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.partner.entity.Partner; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.List; + @Entity @Getter @NoArgsConstructor @@ -41,4 +35,8 @@ public class ChattingRoom extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "partner_id") private Partner partner; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id") + private List messages; } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java index 7422897..e9338b4 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/Message.java +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -3,6 +3,7 @@ import com.assu.server.domain.chat.entity.enums.MessageType; +import com.assu.server.domain.common.entity.BaseEntity; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -22,7 +23,7 @@ @NoArgsConstructor @Builder @AllArgsConstructor -public class Message { +public class Message extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java b/src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java new file mode 100644 index 0000000..be858ff --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java @@ -0,0 +1,36 @@ +package com.assu.server.domain.chat.handler; + +import org.springframework.stereotype.Component; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +@Component +public class ChatWebSocketHandler extends TextWebSocketHandler { + private final Set sessions = new CopyOnWriteArraySet<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + sessions.add(session); + System.out.println("Connected to " + session.getId()); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + System.out.println("Received: " + message.getPayload()); + + for (WebSocketSession s : sessions) { + s.sendMessage(new TextMessage(message.getPayload())); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) throws Exception { + sessions.remove(session); + System.out.println("Disconnected from " + session.getId()); + } +} From a48a2ba94b07da85c34a138f0877ba192db00b85 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Fri, 25 Jul 2025 15:56:04 +0900 Subject: [PATCH 009/270] =?UTF-8?q?feat/#16=20-=20websocket=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=ED=97=88=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?SecurityConfig=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../chat/handler/ChatWebSocketHandler.java | 1 - .../server/global/config/SecurityConfig.java | 24 +++++++++++++++++++ src/main/resources/application.yml | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/assu/server/global/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index f2faa18..e9b56b5 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' // chatting - implementation 'org.springframework.book:spring-boot-starter-websocket' + implementation 'org.springframework.boot:spring-boot-starter-websocket' // maria db runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' diff --git a/src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java b/src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java index be858ff..56ee07d 100644 --- a/src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java +++ b/src/main/java/com/assu/server/domain/chat/handler/ChatWebSocketHandler.java @@ -5,7 +5,6 @@ import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; -import java.util.HashSet; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java new file mode 100644 index 0000000..daf7267 --- /dev/null +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.assu.server.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/ws/**").permitAll() // websocket 경로 허용 + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.disable()) // websocket은 csrf 필요 없음 + .formLogin(login -> login.disable()) + .httpBasic(basic -> basic.disable()); + + return http.build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 692f20a..a383e9d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,7 +3,7 @@ spring: active: local # 여기에 local, blue, green 셋중 하나로 입력 config: import: - - classpath:application-secret.yml +# - classpath:application-secret.yml jpa: hibernate: ddl-auto: update # 여기 From 0edb4a18e13124b8597c38a12fc4c712e4c54b95 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sat, 26 Jul 2025 19:10:08 +0900 Subject: [PATCH 010/270] =?UTF-8?q?feat/#16-chatting=20-=20mariadb=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EC=8B=9C=20RSA=20public=20key=20is=20not?= =?UTF-8?q?=20available=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20Swagger=20test=EB=A5=BC=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?SecurityConfig=EC=97=90=EC=84=9C=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20=EC=A0=91=EA=B7=BC=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ .../com/assu/server/global/config/SecurityConfig.java | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e9b56b5..af0cc6c 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,8 @@ dependencies { // h2 db (test) runtimeOnly 'com.h2database:h2' + + implementation group: 'org.javassist', name: 'javassist', version: '3.15.0-GA' } tasks.named('test') { 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 daf7267..2384c4d 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -12,7 +12,14 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - .requestMatchers("/ws/**").permitAll() // websocket 경로 허용 + .requestMatchers( + "/ws/**", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**" + ).permitAll() .anyRequest().authenticated() ) .csrf(csrf -> csrf.disable()) // websocket은 csrf 필요 없음 From 480415b01f000e28530934265e07d2dcd7219c23 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sat, 26 Jul 2025 19:10:33 +0900 Subject: [PATCH 011/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=A1=B0=ED=9A=8C=20Response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/dto/ChatResponseDTO.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index b494949..4e69343 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -1,4 +1,32 @@ package com.assu.server.domain.chat.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + public class ChatResponseDTO { + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ChatRoomListResultDTO { + private String roomId; + private String lastMessage; + private LocalDateTime lastMessageTime; + private int unreadMessagesCount; + private Opponent opponent; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Opponent { + private Long opponentId; + private String opponentName; + private String profileImageUrl; + } } From 6df50d6663acac504a5b213b736fd67a4cafe710 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Mon, 28 Jul 2025 13:04:43 +0900 Subject: [PATCH 012/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20=ED=98=84=EC=9E=AC=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=9D=B4=20=EA=B5=AC=ED=98=84=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=95=84=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=EC=9D=84=20=ED=86=B5=ED=95=B4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- settings.gradle | 5 +++ .../chat/controller/ChatController.java | 22 ++++++++++ .../domain/chat/converter/ChatConverter.java | 27 +++++++++++++ .../domain/chat/dto/ChatResponseDTO.java | 36 +++++++---------- .../chat/dto/ChatRoomListResultDTO.java | 22 ++++++++++ .../server/domain/chat/entity/Message.java | 17 ++++++-- .../chat/repository/ChatRepository.java | 40 ++++++++++++++++++- .../domain/chat/service/ChatService.java | 4 ++ .../domain/chat/service/ChatServiceImpl.java | 24 ++++++++++- .../server/domain/common/entity/Member.java | 4 +- .../common/repository/MemberRepository.java | 9 +++++ .../server/global/config/SecurityConfig.java | 1 + 13 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java create mode 100644 src/main/java/com/assu/server/domain/common/repository/MemberRepository.java diff --git a/build.gradle b/build.gradle index af0cc6c..b75bd14 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.3' id 'io.spring.dependency-management' version '1.1.7' + id 'org.jetbrains.kotlin.jvm' } group = 'com.assu' @@ -46,7 +47,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' // Swagger 3 (SpringDoc OpenAPI) - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4' // chatting implementation 'org.springframework.boot:spring-boot-starter-websocket' @@ -64,6 +65,7 @@ dependencies { runtimeOnly 'com.h2database:h2' implementation group: 'org.javassist', name: 'javassist', version: '3.15.0-GA' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } tasks.named('test') { diff --git a/settings.gradle b/settings.gradle index 5d4a021..1248596 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,6 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version '2.2.0' + } +} rootProject.name = 'Assu' diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 027e6b5..97f7461 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -1,4 +1,26 @@ package com.assu.server.domain.chat.controller; +import com.assu.server.domain.chat.service.ChatService; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import com.assu.server.global.apiPayload.BaseResponse; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chat") public class ChatController { + private final ChatService chatService; + + @Operation( + summary = "채팅방 목록을 조회하는 API 입니다.", + description = "Request Header에 User id를 입력해 주세요." + ) + @GetMapping("/rooms") + public BaseResponse> getChatRoomList() { + return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList()); + } } diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 4f654db..9464b13 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -1,4 +1,31 @@ package com.assu.server.domain.chat.converter; +import com.assu.server.domain.chat.dto.ChatResponseDTO; +import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; +import com.assu.server.domain.chat.entity.ChattingRoom; + +import java.util.List; +import java.util.stream.Collectors; + public class ChatConverter { + + // 채팅방 리스트 아이템 하나 + public static ChatRoomListResultDTO toChatRoomResultDTO(ChatRoomListResultDTO request) { + return ChatRoomListResultDTO.builder() + .roomId(request.getRoomId()) + .lastMessage(request.getLastMessage()) + .lastMessageTime(request.getLastMessageTime()) + .unreadMessagesCount(request.getUnreadMessagesCount()) + .opponentId(request.getOpponentId()) + .opponentName(request.getOpponentName()) + .opponentProfileImage(request.getOpponentProfileImage()) + .build(); + } + + // 리스트 변환 + public static List toChatRoomListResultDTO(List dtos) { + return dtos.stream() + .map(ChatConverter::toChatRoomResultDTO) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 4e69343..78611e6 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -8,25 +8,17 @@ import java.time.LocalDateTime; public class ChatResponseDTO { - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class ChatRoomListResultDTO { - private String roomId; - private String lastMessage; - private LocalDateTime lastMessageTime; - private int unreadMessagesCount; - private Opponent opponent; - } - - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class Opponent { - private Long opponentId; - private String opponentName; - private String profileImageUrl; - } -} +// @Getter +// @Builder +// @AllArgsConstructor +// @NoArgsConstructor +// public static class ChatRoomListResultDTO { +// private Long roomId; +// private String lastMessage; +// private LocalDateTime lastMessageTime; +// private Long unreadMessagesCount; +// private Long opponentId; +// private String opponentName; +// private String opponentProfileImage; +// } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java new file mode 100644 index 0000000..bfeb0b8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java @@ -0,0 +1,22 @@ +package com.assu.server.domain.chat.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChatRoomListResultDTO { + private Long roomId; + private String lastMessage; + private LocalDateTime lastMessageTime; + private Long unreadMessagesCount; + private Long opponentId; + private String opponentName; + private String opponentProfileImage; +} diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java index e9338b4..7204e20 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/Message.java +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -4,6 +4,7 @@ import com.assu.server.domain.chat.entity.enums.MessageType; import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.entity.Member; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -32,15 +33,25 @@ public class Message extends BaseEntity { @JoinColumn(name = "room_id") private ChattingRoom chattingRoom; - @Enumerated(EnumType.STRING) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sender_id", nullable = false) + private Member sender; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "receiver_id", nullable = true) // 그룹 채팅이면 nullable + private Member receiver; + + + @Enumerated(EnumType.STRING) private MessageType type; - private String content; + private String message; private LocalDateTime sendTime; private LocalDateTime readTime; private Boolean isRead; - + @Builder.Default + private Boolean deleted = false; } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java index a609ec3..5f75a89 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java @@ -1,4 +1,42 @@ package com.assu.server.domain.chat.repository; -public class ChatRepository { +import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; +import com.assu.server.domain.chat.entity.ChattingRoom; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ChatRepository extends JpaRepository { + + @Query(""" + SELECT new com.assu.server.domain.chat.dto.ChatRoomListResultDTO ( + r.id, + (SELECT m.message + FROM Message m + WHERE m.chattingRoom.id = r.id + AND m.sendTime = ( + SELECT MAX(m2.sendTime) + FROM Message m2 + WHERE m2.chattingRoom.id = r.id + ) + ), + (SELECT MAX(m.sendTime) + FROM Message m + WHERE m.chattingRoom.id = r.id + ), + (SELECT COUNT(m) + FROM Message m + WHERE m.chattingRoom.id = r.id + AND m.receiver.id = :memberId + AND m.isRead = false), + CASE WHEN r.partner.member.id = :memberId THEN r.admin.member.id ELSE r.partner.member.id END, + CASE WHEN r.partner.member.id = :memberId THEN r.admin.name ELSE r.partner.name END, + CASE WHEN r.partner.member.id = :memberId THEN r.admin.member.profileUrl ELSE r.partner.member.profileUrl END + ) + FROM ChattingRoom r + WHERE r.partner.member.id = :memberId OR r.admin.member.id = :memberId + """) + List findChattingRoomByMember(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index e3499a1..b588ec8 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -1,4 +1,8 @@ package com.assu.server.domain.chat.service; +import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; +import java.util.List; + public interface ChatService { + List getChatRoomList(); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 4f30c99..08ebd6e 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -1,4 +1,26 @@ package com.assu.server.domain.chat.service; -public class ChatServiceImpl { +import com.assu.server.domain.chat.converter.ChatConverter; +import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; +import com.assu.server.domain.chat.repository.ChatRepository; +import com.assu.server.domain.common.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class ChatServiceImpl implements ChatService { + private final ChatRepository chatRepository; + private final MemberRepository memberRepository; + + @Override + public List getChatRoomList() { +// Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 1L; + + List chatRoomList = chatRepository.findChattingRoomByMember(memberId); + return ChatConverter.toChatRoomListResultDTO(chatRoomList); + } } diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java index 4687dbc..161e5cc 100644 --- a/src/main/java/com/assu/server/domain/common/entity/Member.java +++ b/src/main/java/com/assu/server/domain/common/entity/Member.java @@ -12,7 +12,7 @@ @Getter @Entity -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -24,6 +24,8 @@ public class Member { private LocalDateTime phoneVerifiedAt; + private String profileUrl; + @Enumerated(EnumType.STRING) private UserRole role; // User, ADMIN, PARTNER diff --git a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java new file mode 100644 index 0000000..207a939 --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java @@ -0,0 +1,9 @@ +package com.assu.server.domain.common.repository; + +import com.assu.server.domain.common.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + + + +public interface MemberRepository extends JpaRepository { +} 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 2384c4d..24a8930 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -12,6 +12,7 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth + .requestMatchers("/chat/rooms").permitAll() .requestMatchers( "/ws/**", "/v3/api-docs/**", From 7781d52ec5d4c773ce6c449db3bb16614fe681f9 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Mon, 28 Jul 2025 18:17:52 +0900 Subject: [PATCH 013/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20=ED=98=84=EC=9E=AC=EB=8A=94=20admin,=20partner?= =?UTF-8?q?=20ID=20=EB=AA=A8=EB=91=90=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9D=B4=EC=A7=80=EB=A7=8C,=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20=ED=9B=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/repository/AdminRepository.java | 4 --- .../chat/controller/ChatController.java | 11 +++++++ .../domain/chat/converter/ChatConverter.java | 14 +++++++-- .../domain/chat/dto/ChatRequestDTO.java | 7 +++++ .../domain/chat/dto/ChatResponseDTO.java | 22 +++++--------- .../domain/chat/service/ChatService.java | 2 ++ .../domain/chat/service/ChatServiceImpl.java | 30 +++++++++++++++++++ .../partner/repository/PartnerRepository.java | 4 --- .../apiPayload/code/status/ErrorStatus.java | 2 ++ .../server/global/config/SecurityConfig.java | 2 +- 10 files changed, 71 insertions(+), 27 deletions(-) delete mode 100644 src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java delete mode 100644 src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java deleted file mode 100644 index 4e6b1fa..0000000 --- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.admin.repository; - -public class AdminRepository { -} diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 97f7461..ccf97c8 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -1,5 +1,7 @@ package com.assu.server.domain.chat.controller; +import com.assu.server.domain.chat.dto.ChatRequestDTO; +import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.service.ChatService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; @@ -23,4 +25,13 @@ public class ChatController { public BaseResponse> getChatRoomList() { return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList()); } + + @Operation( + summary = "채팅방을 생성하는 API 입니다.", + description = "상대방의 id를 request body에 입력해 주세요" + ) + @PostMapping("/create/rooms") + public BaseResponse createChatRoom(@RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) { + return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request)); + } } diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 9464b13..2e6017b 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -1,8 +1,9 @@ package com.assu.server.domain.chat.converter; -import com.assu.server.domain.chat.dto.ChatResponseDTO; +import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; +import com.assu.server.domain.partner.entity.Partner; import java.util.List; import java.util.stream.Collectors; @@ -23,9 +24,16 @@ public static ChatRoomListResultDTO toChatRoomResultDTO(ChatRoomListResultDTO re } // 리스트 변환 - public static List toChatRoomListResultDTO(List dtos) { - return dtos.stream() + public static List toChatRoomListResultDTO(List dto) { + return dto.stream() .map(ChatConverter::toChatRoomResultDTO) .collect(Collectors.toList()); } + + public static ChattingRoom toCreateChattingRoom(Admin admin, Partner partner) { + return ChattingRoom.builder() + .admin(admin) + .partner(partner) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index e4e172b..abc4505 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -1,4 +1,11 @@ package com.assu.server.domain.chat.dto; +import lombok.Getter; + public class ChatRequestDTO { + @Getter + public static class CreateChatRoomRequestDTO { + private Long adminId; + private Long partnerId; + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 78611e6..5d20fe0 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -5,20 +5,12 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - public class ChatResponseDTO { -// @Getter -// @Builder -// @AllArgsConstructor -// @NoArgsConstructor -// public static class ChatRoomListResultDTO { -// private Long roomId; -// private String lastMessage; -// private LocalDateTime lastMessageTime; -// private Long unreadMessagesCount; -// private Long opponentId; -// private String opponentName; -// private String opponentProfileImage; -// } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CreateChatRoomResponseDTO { + private Long roomId; + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index b588ec8..2358760 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -1,8 +1,10 @@ package com.assu.server.domain.chat.service; +import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import java.util.List; public interface ChatService { List getChatRoomList(); + Long createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 08ebd6e..f42d522 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -1,9 +1,17 @@ package com.assu.server.domain.chat.service; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; import com.assu.server.domain.chat.converter.ChatConverter; +import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; +import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.chat.repository.ChatRepository; import com.assu.server.domain.common.repository.MemberRepository; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partner.repository.PartnerRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; @@ -14,6 +22,8 @@ public class ChatServiceImpl implements ChatService { private final ChatRepository chatRepository; private final MemberRepository memberRepository; + private final PartnerRepository partnerRepository; + private final AdminRepository adminRepository; @Override public List getChatRoomList() { @@ -23,4 +33,24 @@ public List getChatRoomList() { List chatRoomList = chatRepository.findChattingRoomByMember(memberId); return ChatConverter.toChatRoomListResultDTO(chatRoomList); } + + @Override + public Long createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request) { +// Long memberId = SecurityUtil.getCurrentUserId; +// Long opponentId = request.getOpponentId(); + + Long adminId = request.getAdminId(); + Long partnerId = request.getPartnerId(); + + Admin admin = adminRepository.findById(adminId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + Partner partner = partnerRepository.findById(partnerId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + + ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner); + + ChattingRoom savedRoom = chatRepository.save(room); + + return savedRoom.getId(); + } } diff --git a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java deleted file mode 100644 index f6f0ce4..0000000 --- a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partner.repository; - -public class PartnerRepository { -} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index ddefa9e..a304aea 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -20,6 +20,8 @@ public enum ErrorStatus implements BaseErrorCode { // 멤버 에러 NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."), + NO_SUCH_ADMIN(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 admin ID 입니다."), + NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 partner ID 입니다.") ; 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 24a8930..37ec7ce 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -12,7 +12,7 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - .requestMatchers("/chat/rooms").permitAll() + .requestMatchers("/chat/**").permitAll() .requestMatchers( "/ws/**", "/v3/api-docs/**", From a73859115062d6b3b52b87fe2bcf0f5a94522bb1 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Mon, 28 Jul 2025 18:17:59 +0900 Subject: [PATCH 014/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20=ED=98=84=EC=9E=AC=EB=8A=94=20admin,=20partner?= =?UTF-8?q?=20ID=20=EB=AA=A8=EB=91=90=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9D=B4=EC=A7=80=EB=A7=8C,=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20=ED=9B=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/admin/repository/AdminRepository.java | 7 +++++++ .../domain/partner/repository/PartnerRepository.java | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java create mode 100644 src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java new file mode 100644 index 0000000..4fd442a --- /dev/null +++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java @@ -0,0 +1,7 @@ +package com.assu.server.domain.admin.repository; + +import com.assu.server.domain.admin.entity.Admin; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminRepository extends JpaRepository { +} diff --git a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java new file mode 100644 index 0000000..ae4ef46 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java @@ -0,0 +1,7 @@ +package com.assu.server.domain.partner.repository; + +import com.assu.server.domain.partner.entity.Partner; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PartnerRepository extends JpaRepository { +} From e2467eca395b156369fc630f81df84606401ad4b Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Mon, 28 Jul 2025 20:45:07 +0900 Subject: [PATCH 015/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=9D=B4=EB=A6=84=20=EC=84=9C=EB=A1=9C?= =?UTF-8?q?=EC=97=90=EA=B2=8C=20=EC=95=8C=EB=A7=9E=EA=B2=8C=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EA=B2=8C=20=EC=84=A4=EC=A0=95=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20active=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/controller/ChatController.java | 2 +- .../domain/chat/converter/ChatConverter.java | 5 +++++ .../server/domain/chat/dto/ChatResponseDTO.java | 1 + .../server/domain/chat/entity/ChattingRoom.java | 12 ++++++++++++ .../server/domain/chat/service/ChatService.java | 3 ++- .../domain/chat/service/ChatServiceImpl.java | 15 ++++++++++++--- 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index ccf97c8..f58c9b1 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -31,7 +31,7 @@ public BaseResponse> description = "상대방의 id를 request body에 입력해 주세요" ) @PostMapping("/create/rooms") - public BaseResponse createChatRoom(@RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) { + public BaseResponse createChatRoom(@RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) { return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request)); } } diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 2e6017b..e5356b5 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -1,6 +1,7 @@ package com.assu.server.domain.chat.converter; import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.partner.entity.Partner; @@ -36,4 +37,8 @@ public static ChattingRoom toCreateChattingRoom(Admin admin, Partner partner) { .partner(partner) .build(); } + + public static ChatResponseDTO.CreateChatRoomResponseDTO toCreateChatRoomIdDTO(ChattingRoom room) { + return new ChatResponseDTO.CreateChatRoomResponseDTO(room.getId()); + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 5d20fe0..d64657f 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; public class ChatResponseDTO { + @Getter @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java index 0d7488e..c81975a 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java +++ b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java @@ -39,4 +39,16 @@ public class ChattingRoom extends BaseEntity { @OneToMany(fetch = FetchType.LAZY) @JoinColumn(name = "room_id") private List messages; + + private String adminViewName; + private String partnerViewName; + + public void updateStatus(ActivationStatus status) { + this.status = status; + } + + public void updateName(String adminViewName, String partnerViewName) { + this.adminViewName = adminViewName; + this.partnerViewName = partnerViewName; + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index 2358760..43c716a 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -1,10 +1,11 @@ package com.assu.server.domain.chat.service; import com.assu.server.domain.chat.dto.ChatRequestDTO; +import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import java.util.List; public interface ChatService { List getChatRoomList(); - Long createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request); + ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index f42d522..f039456 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -4,9 +4,11 @@ import com.assu.server.domain.admin.repository.AdminRepository; import com.assu.server.domain.chat.converter.ChatConverter; import com.assu.server.domain.chat.dto.ChatRequestDTO; +import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.chat.repository.ChatRepository; +import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.common.repository.MemberRepository; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.partner.repository.PartnerRepository; @@ -16,7 +18,6 @@ import org.springframework.stereotype.Service; import java.util.List; - @Service @RequiredArgsConstructor public class ChatServiceImpl implements ChatService { @@ -35,7 +36,7 @@ public List getChatRoomList() { } @Override - public Long createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request) { + public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request) { // Long memberId = SecurityUtil.getCurrentUserId; // Long opponentId = request.getOpponentId(); @@ -49,8 +50,16 @@ public Long createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request) { ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner); + room.updateStatus(ActivationStatus.ACTIVE); + + room.updateName( + partner.getName(), + admin.getName() + ); ChattingRoom savedRoom = chatRepository.save(room); - return savedRoom.getId(); + + + return ChatConverter.toCreateChatRoomIdDTO(savedRoom); } } From 8aba0b99732319a00eb8fab496a4f53d061cfb6f Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Tue, 29 Jul 2025 00:39:58 +0900 Subject: [PATCH 016/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EA=B5=AC=ED=98=84=20-=20=EC=9D=B4=EA=B1=B4=20HTTP?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=EA=B0=80=20=EC=95=84=EB=8B=88?= =?UTF-8?q?=EB=9D=BC,=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=EC=97=90=20=EC=95=88?= =?UTF-8?q?=EB=82=98=EC=99=80=EC=9A=94.=20=EC=A0=80=EC=9E=A5=EB=90=9C=20ht?= =?UTF-8?q?ml=EC=8B=A4=ED=96=89=ED=95=B4=EB=B3=B4=EC=8B=9C=EB=A9=B4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=8F=84=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/config/WebSocketConfig.java | 25 ++++--- .../chat/controller/ChatController.java | 15 +++++ .../domain/chat/converter/ChatConverter.java | 20 ++++++ .../domain/chat/dto/ChatRequestDTO.java | 6 ++ .../domain/chat/dto/ChatResponseDTO.java | 10 +++ .../chat/repository/MessageRepository.java | 7 ++ .../domain/chat/service/ChatService.java | 1 + .../domain/chat/service/ChatServiceImpl.java | 19 ++++++ .../apiPayload/code/status/ErrorStatus.java | 5 +- .../server/global/config/SecurityConfig.java | 4 +- src/main/resources/chattinttest.html | 67 +++++++++++++++++++ 11 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java create mode 100644 src/main/resources/chattinttest.html diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java index f7d137e..0f9920b 100644 --- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java +++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java @@ -1,24 +1,23 @@ package com.assu.server.domain.chat.config; -import com.assu.server.domain.chat.handler.ChatWebSocketHandler; import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.*; @Configuration -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - private final ChatWebSocketHandler chatWebSocketHandler; - - public WebSocketConfig (ChatWebSocketHandler chatWebSocketHandler) { - this.chatWebSocketHandler = chatWebSocketHandler; + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws/chat") // 클라이언트 WebSocket 연결 지점 + .setAllowedOriginPatterns("http://localhost:63342") + .withSockJS(); // fallback for old browsers } @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(chatWebSocketHandler, "/ws/chat") - .setAllowedOrigins("*"); + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix + registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix } } diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index f58c9b1..8b4b300 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -6,8 +6,11 @@ import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; import org.springframework.web.bind.annotation.*; import com.assu.server.global.apiPayload.BaseResponse; +import org.springframework.messaging.simp.SimpMessagingTemplate; import java.util.List; @@ -16,6 +19,7 @@ @RequestMapping("/chat") public class ChatController { private final ChatService chatService; + private final SimpMessagingTemplate simpMessagingTemplate; @Operation( summary = "채팅방 목록을 조회하는 API 입니다.", @@ -34,4 +38,15 @@ public BaseResponse> public BaseResponse createChatRoom(@RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) { return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request)); } + + @Operation( + summary = "채팅 API 입니다.", + description = "roomId, senderId, message를 입력해 주세요" + ) + @MessageMapping("/send") + public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { + ChatResponseDTO.ChatMessageResponseDTO response = chatService.handleMessage(request); + + simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), response); + } } diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index e5356b5..6764273 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -1,9 +1,12 @@ package com.assu.server.domain.chat.converter; import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; +import com.assu.server.domain.chat.entity.Message; +import com.assu.server.domain.common.entity.Member; import com.assu.server.domain.partner.entity.Partner; import java.util.List; @@ -41,4 +44,21 @@ public static ChattingRoom toCreateChattingRoom(Admin admin, Partner partner) { public static ChatResponseDTO.CreateChatRoomResponseDTO toCreateChatRoomIdDTO(ChattingRoom room) { return new ChatResponseDTO.CreateChatRoomResponseDTO(room.getId()); } + + public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender) { + return Message.builder() + .chattingRoom(room) + .sender(sender) + .message(request.message()) + .build(); + } + + public static ChatResponseDTO.ChatMessageResponseDTO toChatMessageDTO(Message message) { + return ChatResponseDTO.ChatMessageResponseDTO.builder() + .roomId(message.getChattingRoom().getId()) + .senderId(message.getSender().getId()) + .message(message.getMessage()) + .sentAt(message.getCreatedAt()) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index abc4505..4cb0e4e 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -8,4 +8,10 @@ public static class CreateChatRoomRequestDTO { private Long adminId; private Long partnerId; } + + public record ChatMessageRequestDTO( + Long roomId, + Long senderId, + String message + ) {} } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index d64657f..277c0f8 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -5,6 +5,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + public class ChatResponseDTO { @Getter @@ -14,4 +16,12 @@ public class ChatResponseDTO { public static class CreateChatRoomResponseDTO { private Long roomId; } + + @Builder + public record ChatMessageResponseDTO( + Long roomId, + Long senderId, + String message, + LocalDateTime sentAt + ) {} } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java new file mode 100644 index 0000000..aa4c5f3 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -0,0 +1,7 @@ +package com.assu.server.domain.chat.repository; + +import com.assu.server.domain.chat.entity.Message; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MessageRepository extends JpaRepository { +} diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index 43c716a..da3f3c1 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -8,4 +8,5 @@ public interface ChatService { List getChatRoomList(); ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request); + ChatResponseDTO.ChatMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index f039456..c7ab003 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -7,7 +7,10 @@ import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; +import com.assu.server.domain.chat.entity.Message; import com.assu.server.domain.chat.repository.ChatRepository; +import com.assu.server.domain.chat.repository.MessageRepository; +import com.assu.server.domain.common.entity.Member; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.common.repository.MemberRepository; import com.assu.server.domain.partner.entity.Partner; @@ -25,6 +28,8 @@ public class ChatServiceImpl implements ChatService { private final MemberRepository memberRepository; private final PartnerRepository partnerRepository; private final AdminRepository adminRepository; + private final MessageRepository messageRepository; + @Override public List getChatRoomList() { @@ -62,4 +67,18 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C return ChatConverter.toCreateChatRoomIdDTO(savedRoom); } + + @Override + public ChatResponseDTO.ChatMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { + // 유효성 검사 + ChattingRoom room = chatRepository.findById(request.roomId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); + Member sender = memberRepository.findById(request.senderId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + + Message message = ChatConverter.toMessageEntity(request, room, sender); + messageRepository.save(message); + + return ChatConverter.toChatMessageDTO(message); + } } diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index a304aea..6d5b4fb 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -21,7 +21,10 @@ public enum ErrorStatus implements BaseErrorCode { // 멤버 에러 NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."), NO_SUCH_ADMIN(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 admin ID 입니다."), - NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 partner ID 입니다.") + NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 partner ID 입니다."), + + // 채팅 에러 + NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다.") ; 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 37ec7ce..6aa66aa 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -12,9 +12,11 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - .requestMatchers("/chat/**").permitAll() .requestMatchers( + "/chat/**", "/ws/**", + "/pub/**", // STOMP 메시지 전송 + "/sub/**", // STOMP 메시지 구독 "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", diff --git a/src/main/resources/chattinttest.html b/src/main/resources/chattinttest.html new file mode 100644 index 0000000..746e8bf --- /dev/null +++ b/src/main/resources/chattinttest.html @@ -0,0 +1,67 @@ + + + + + WebSocket 테스트 + + + + +

🔌 WebSocket 테스트

+ +
+
+
+
+ + +
+ +
+

+
+
+
+
\ No newline at end of file

From 0fdea412bb8e134ca86863242af7dd4282a74362 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Wed, 30 Jul 2025 22:54:58 +0900
Subject: [PATCH 017/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?=
 =?UTF-8?q?=ED=8C=85=20=EC=9D=BD=EC=9D=8C=20=ED=99=95=EC=9D=B8=20API=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84=20-=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?=
 =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=8B=9C=20receiverId=EB=8F=84=20?=
 =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../chat/controller/ChatController.java       | 13 +++++++++++
 .../domain/chat/converter/ChatConverter.java  |  3 ++-
 .../domain/chat/dto/ChatRequestDTO.java       |  6 +++++
 .../domain/chat/dto/ChatResponseDTO.java      |  7 +++++-
 .../server/domain/chat/entity/Message.java    | 22 +++++++------------
 .../chat/repository/MessageRepository.java    | 10 +++++++++
 .../domain/chat/service/ChatService.java      |  1 +
 .../domain/chat/service/ChatServiceImpl.java  | 18 ++++++++++++++-
 src/main/resources/chattinttest.html          |  3 +++
 9 files changed, 66 insertions(+), 17 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
index 8b4b300..028e68c 100644
--- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
+++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
@@ -3,11 +3,13 @@
 import com.assu.server.domain.chat.dto.ChatRequestDTO;
 import com.assu.server.domain.chat.dto.ChatResponseDTO;
 import com.assu.server.domain.chat.service.ChatService;
+import com.assu.server.domain.common.entity.Member;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
 import org.springframework.messaging.handler.annotation.MessageMapping;
 import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 import com.assu.server.global.apiPayload.BaseResponse;
 import org.springframework.messaging.simp.SimpMessagingTemplate;
@@ -49,4 +51,15 @@ public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request)
 
         simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), response);
     }
+
+    @Operation(
+            summary = "메시지 읽음 처리 API 입니다.",
+            description = "roomId를 입력해 주세요."
+    )
+    @PatchMapping("rooms/{roomId}/read")
+    public BaseResponse readMessage(
+            @PathVariable Long roomId) {
+        ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId);
+        return BaseResponse.onSuccess(SuccessStatus._OK, response);
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
index 6764273..b18f5f2 100644
--- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
+++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
@@ -45,10 +45,11 @@ public static ChatResponseDTO.CreateChatRoomResponseDTO toCreateChatRoomIdDTO(Ch
         return new ChatResponseDTO.CreateChatRoomResponseDTO(room.getId());
     }
 
-    public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender) {
+    public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender, Member receiver) {
         return Message.builder()
                 .chattingRoom(room)
                 .sender(sender)
+                .receiver(receiver)
                 .message(request.message())
                 .build();
     }
diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
index 4cb0e4e..796b9de 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.chat.dto;
 
+import com.assu.server.domain.common.entity.Member;
 import lombok.Getter;
 
 public class ChatRequestDTO {
@@ -12,6 +13,11 @@ public static class CreateChatRoomRequestDTO {
     public record ChatMessageRequestDTO(
         Long roomId,
         Long senderId,
+        Long receiverId,
         String message
         ) {}
+
+    public record ReadMessageRequestDTO(
+        Long roomId
+        ) {}
 }
diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
index 277c0f8..b0a7735 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
@@ -1,10 +1,10 @@
 package com.assu.server.domain.chat.dto;
 
+import jakarta.transaction.Transactional;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
-
 import java.time.LocalDateTime;
 
 public class ChatResponseDTO {
@@ -24,4 +24,9 @@ public record ChatMessageResponseDTO(
             String message,
             LocalDateTime sentAt
     ) {}
+
+    public record ReadMessageResponseDTO(
+            Long roomId,
+            int readCount
+    ) {}
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java
index 7204e20..9d84726 100644
--- a/src/main/java/com/assu/server/domain/chat/entity/Message.java
+++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java
@@ -5,19 +5,8 @@
 
 import com.assu.server.domain.common.entity.BaseEntity;
 import com.assu.server.domain.common.entity.Member;
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import jakarta.persistence.*;
+import lombok.*;
 
 @Entity
 @Getter
@@ -50,8 +39,13 @@ public class Message extends BaseEntity {
 	private LocalDateTime sendTime;
 	private LocalDateTime readTime;
 
-	private Boolean isRead;
+    @Column(nullable = false)
+	private boolean isRead = false;
 
     @Builder.Default
     private Boolean deleted = false;
+
+    public void markAsRead() {
+        this.isRead = true;
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java
index aa4c5f3..0a2b25b 100644
--- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java
+++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java
@@ -2,6 +2,16 @@
 
 import com.assu.server.domain.chat.entity.Message;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
 
 public interface MessageRepository extends JpaRepository {
+    @Query("""
+        SELECT m FROM Message m
+        WHERE m.chattingRoom.id = :roomId
+        AND m.receiver.id = :receiverId
+        AND m.isRead = false
+""")
+    List findUnreadMessagesByRoomAndReceiver(Long roomId, Long receiverId);
 }
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java
index da3f3c1..9cfaf51 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java
@@ -9,4 +9,5 @@ public interface ChatService {
     List getChatRoomList();
     ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request);
     ChatResponseDTO.ChatMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request);
+    ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId);
 }
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index c7ab003..77b4387 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -17,6 +17,7 @@
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.exception.DatabaseException;
+import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import java.util.List;
@@ -75,10 +76,25 @@ public ChatResponseDTO.ChatMessageResponseDTO handleMessage(ChatRequestDTO.ChatM
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM));
         Member sender = memberRepository.findById(request.senderId())
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
+        Member receiver = memberRepository.findById(request.receiverId())
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
 
-        Message message = ChatConverter.toMessageEntity(request, room, sender);
+        Message message = ChatConverter.toMessageEntity(request, room, sender, receiver);
         messageRepository.save(message);
 
         return ChatConverter.toChatMessageDTO(message);
     }
+
+    @Transactional
+    @Override
+    public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) {
+//        Long memberId = SecurityUtil.getCurrentUserId();
+        Long memberId = 2L;
+
+        List unreadMessages = messageRepository.findUnreadMessagesByRoomAndReceiver(roomId, memberId);
+
+        unreadMessages.forEach(Message::markAsRead);
+
+        return new ChatResponseDTO.ReadMessageResponseDTO(roomId, unreadMessages.size());
+    }
 }
diff --git a/src/main/resources/chattinttest.html b/src/main/resources/chattinttest.html
index 746e8bf..a93f1c8 100644
--- a/src/main/resources/chattinttest.html
+++ b/src/main/resources/chattinttest.html
@@ -12,6 +12,7 @@ 

🔌 WebSocket 테스트



+

@@ -46,6 +47,7 @@

🔌 WebSocket 테스트

function sendMessage() { const roomId = document.getElementById("roomId").value; const senderId = document.getElementById("senderId").value; + const receiverId = document.getElementById("receiverId").value; const message = document.getElementById("message").value; if (!stompClient || !stompClient.connected) { @@ -56,6 +58,7 @@

🔌 WebSocket 테스트

const payload = { roomId: parseInt(roomId), senderId: parseInt(senderId), + receiverId: parseInt(receiverId), message: message }; From cbf6284a5d1f5e340d62651ef7bee4f81d45cea0 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sun, 3 Aug 2025 22:44:25 +0900 Subject: [PATCH 018/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EB=93=A4=EC=96=B4=EA=B0=94=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84=20-=20?= =?UTF-8?q?=EA=B0=81=20=EB=A9=94=EC=8B=9C=EC=A7=80=EC=99=80=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=EA=B0=84,?= =?UTF-8?q?=20=EB=88=84=EA=B0=80=20=EB=B3=B4=EB=82=B8=EA=B1=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=20=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 14 +++++++-- .../domain/chat/converter/ChatConverter.java | 27 +++++++++++++++-- .../domain/chat/dto/ChatMessageDTO.java | 26 ++++++++++++++++ .../domain/chat/dto/ChatRequestDTO.java | 5 ---- .../domain/chat/dto/ChatResponseDTO.java | 30 ++++++++++++++----- .../chat/repository/MessageRepository.java | 24 +++++++++++++++ .../domain/chat/service/ChatService.java | 3 +- .../domain/chat/service/ChatServiceImpl.java | 15 ++++++++-- 8 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 028e68c..80b7a13 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -3,13 +3,11 @@ import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.service.ChatService; -import com.assu.server.domain.common.entity.Member; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import com.assu.server.global.apiPayload.BaseResponse; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -47,7 +45,7 @@ public BaseResponse createChatRoom(@R ) @MessageMapping("/send") public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { - ChatResponseDTO.ChatMessageResponseDTO response = chatService.handleMessage(request); + ChatResponseDTO.SendMessageResponseDTO response = chatService.handleMessage(request); simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), response); } @@ -62,4 +60,14 @@ public BaseResponse readMessage( ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId); return BaseResponse.onSuccess(SuccessStatus._OK, response); } + + @Operation( + summary = "채팅방 상세 조회 API 입니다.", + description = "roomId를 입력해 주세요." + ) + @GetMapping("rooms/{roomId}/messages") + public BaseResponse getChatHistory(@PathVariable Long roomId) { + ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId); + return BaseResponse.onSuccess(SuccessStatus._OK, response); + } } diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index b18f5f2..5ff150b 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -1,6 +1,7 @@ package com.assu.server.domain.chat.converter; import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.chat.dto.ChatMessageDTO; import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; @@ -54,12 +55,32 @@ public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO reque .build(); } - public static ChatResponseDTO.ChatMessageResponseDTO toChatMessageDTO(Message message) { - return ChatResponseDTO.ChatMessageResponseDTO.builder() + public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message message) { + return ChatResponseDTO.SendMessageResponseDTO.builder() .roomId(message.getChattingRoom().getId()) .senderId(message.getSender().getId()) .message(message.getMessage()) - .sentAt(message.getCreatedAt()) + .sentAt(message.getSendTime()) + .build(); + } + +// public static ChatMessageDTO toChatMessageDTO(Message message, Long currentUserId) { +// return ChatMessageDTO.builder() +// .messageId(message.getId()) +// .message(message.getMessage()) +// .sendTime(message.getCreatedAt()) +// .isRead(message.isRead()) +// .isMyMessage(message.getSender().getId().equals(currentUserId)) +// .build(); +// } + + public static ChatResponseDTO.ChatHistoryResponseDTO toChatHistoryDTO( + List messages) { + + // ③ 최종 DTO 빌드 + return ChatResponseDTO.ChatHistoryResponseDTO.builder() + .roomId(messages.get(0).getRoomId()) + .messages(messages) .build(); } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java new file mode 100644 index 0000000..628af1d --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java @@ -0,0 +1,26 @@ +package com.assu.server.domain.chat.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ChatMessageDTO { + @JsonIgnore + private Long roomId; + // 메시지 삭제 시 사용 가능 + private Long messageId; + + private String message; + private LocalDateTime sendTime; + + private boolean isRead; + private boolean isMyMessage; +} diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index 796b9de..87d3298 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -1,6 +1,5 @@ package com.assu.server.domain.chat.dto; -import com.assu.server.domain.common.entity.Member; import lombok.Getter; public class ChatRequestDTO { @@ -16,8 +15,4 @@ public record ChatMessageRequestDTO( Long receiverId, String message ) {} - - public record ReadMessageRequestDTO( - Long roomId - ) {} } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index b0a7735..227adee 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -1,14 +1,16 @@ package com.assu.server.domain.chat.dto; -import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; public class ChatResponseDTO { + // 채팅방 목록 조회 @Getter @NoArgsConstructor @AllArgsConstructor @@ -17,16 +19,28 @@ public static class CreateChatRoomResponseDTO { private Long roomId; } + // 메시지 전송 @Builder - public record ChatMessageResponseDTO( - Long roomId, - Long senderId, - String message, - LocalDateTime sentAt + public record SendMessageResponseDTO( + Long roomId, + Long senderId, + String message, + LocalDateTime sentAt ) {} + // 메시지 읽음 처리 public record ReadMessageResponseDTO( - Long roomId, - int readCount + Long roomId, + int readCount ) {} + + // 채팅방 들어갔을 때 조회 + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ChatHistoryResponseDTO { + private Long roomId; + private List messages; + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java index 0a2b25b..60850f8 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -1,8 +1,10 @@ package com.assu.server.domain.chat.repository; +import com.assu.server.domain.chat.dto.ChatMessageDTO; import com.assu.server.domain.chat.entity.Message; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; @@ -14,4 +16,26 @@ public interface MessageRepository extends JpaRepository { AND m.isRead = false """) List findUnreadMessagesByRoomAndReceiver(Long roomId, Long receiverId); + + + @Query(""" + SELECT new com.assu.server.domain.chat.dto.ChatMessageDTO ( + m.chattingRoom.id, + m.id, + m.message, + m.sendTime, + m.isRead, + CASE WHEN m.sender.id = :memberId THEN true + ELSE false + END + ) + FROM Message m + WHERE m.chattingRoom.id = :roomId + AND (m.sender.id = :memberId OR m.receiver.id = :memberId) + ORDER BY m.sendTime ASC +""") + List findAllMessagesByRoomAndMemberId( + @Param("roomId") Long roomId, + @Param("memberId") Long memberId + ); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index 9cfaf51..936571f 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -8,6 +8,7 @@ public interface ChatService { List getChatRoomList(); ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request); - ChatResponseDTO.ChatMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); + ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId); + ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 77b4387..0c861df 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -3,6 +3,7 @@ import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.admin.repository.AdminRepository; import com.assu.server.domain.chat.converter.ChatConverter; +import com.assu.server.domain.chat.dto.ChatMessageDTO; import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; @@ -70,7 +71,7 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C } @Override - public ChatResponseDTO.ChatMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { + public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { // 유효성 검사 ChattingRoom room = chatRepository.findById(request.roomId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); @@ -82,7 +83,7 @@ public ChatResponseDTO.ChatMessageResponseDTO handleMessage(ChatRequestDTO.ChatM Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); messageRepository.save(message); - return ChatConverter.toChatMessageDTO(message); + return ChatConverter.toSendMessageDTO(message); } @Transactional @@ -97,4 +98,14 @@ public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) { return new ChatResponseDTO.ReadMessageResponseDTO(roomId, unreadMessages.size()); } + + @Override + public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId) { + // Long memberId = SecurityUtil.getCurrentUserId(); + Long memberId = 2L; + + List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(roomId, memberId); + + return ChatConverter.toChatHistoryDTO(allMessages); + } } From 32a3e4835c9723babcfa5c960649751456b709f5 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Tue, 5 Aug 2025 15:10:51 +0900 Subject: [PATCH 019/270] =?UTF-8?q?feat/#16-chatting=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EB=82=98=EA=B0=80=EA=B8=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B5=AC=ED=98=84=20-=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=EA=B0=80=201=EB=AA=85=EB=A7=8C=20=EB=82=98?= =?UTF-8?q?=EA=B0=80=EB=A9=B4=20=EC=B1=84=ED=8C=85=EB=B0=A9=EC=9D=80=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80,=20=ED=95=B4=EB=8B=B9=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EC=9D=98=20=EB=AA=A9=EB=A1=9D=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=A7=8C=20=EC=82=AD=EC=A0=9C=20-=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EB=AA=A8=EB=91=90=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EB=A9=B4=20=EC=B1=84=ED=8C=85=EB=B0=A9,=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A8=EB=91=90=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/admin/entity/Admin.java | 1 + .../chat/controller/ChatController.java | 12 +++++ .../domain/chat/dto/ChatRequestDTO.java | 2 +- .../domain/chat/dto/ChatResponseDTO.java | 14 ++++- .../domain/chat/entity/ChattingRoom.java | 16 +++++- .../chat/repository/MessageRepository.java | 1 + .../domain/chat/service/ChatService.java | 1 + .../domain/chat/service/ChatServiceImpl.java | 52 ++++++++++++++++++- .../server/domain/partner/entity/Partner.java | 1 + .../apiPayload/code/status/ErrorStatus.java | 5 +- .../server/global/config/SecurityConfig.java | 2 + src/main/resources/application.yml | 15 +++++- 12 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java index 271200b..6356573 100644 --- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java +++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java @@ -15,6 +15,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder +@Setter public class Admin { @Id diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 80b7a13..8d38776 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -70,4 +70,16 @@ public BaseResponse getChatHistory(@Path ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId); return BaseResponse.onSuccess(SuccessStatus._OK, response); } + + @Operation( + summary = "채팅방을 나가는 API 입니다." + + "참여자가 2명이면 채팅방이 살아있지만, 이미 한 명이 나갔다면 채팅방이 삭제됩니다.", + description = "roomId를 입력해 주세요." + ) + @DeleteMapping("rooms/{roomId}/leave") + public BaseResponse leaveChattingRoom( + @PathVariable Long roomId + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId)); + } } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index 87d3298..90798fc 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -15,4 +15,4 @@ public record ChatMessageRequestDTO( Long receiverId, String message ) {} -} +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 227adee..8c4790a 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; public class ChatResponseDTO { @@ -43,4 +42,15 @@ public static class ChatHistoryResponseDTO { private Long roomId; private List messages; } -} \ No newline at end of file + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class LeaveChattingRoomResponseDTO { + private Long roomId; + private boolean isLeftSuccessfully; + private boolean isRoomDeleted; + } +} + diff --git a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java index c81975a..c209025 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java +++ b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java @@ -36,13 +36,15 @@ public class ChattingRoom extends BaseEntity { @JoinColumn(name = "partner_id") private Partner partner; - @OneToMany(fetch = FetchType.LAZY) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name = "room_id") private List messages; private String adminViewName; private String partnerViewName; + private int memberCount; + public void updateStatus(ActivationStatus status) { this.status = status; } @@ -51,4 +53,16 @@ public void updateName(String adminViewName, String partnerViewName) { this.adminViewName = adminViewName; this.partnerViewName = partnerViewName; } + + public void updateMemberCount(int memberCount) { + this.memberCount = memberCount; + } + + public void setAdmin(Admin admin) { + this.admin = admin; + } + + public void setPartner(Partner partner) { + this.partner = partner; + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java index 60850f8..31f5367 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -38,4 +38,5 @@ List findAllMessagesByRoomAndMemberId( @Param("roomId") Long roomId, @Param("memberId") Long memberId ); + } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index 936571f..bc44c56 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -11,4 +11,5 @@ public interface ChatService { ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId); ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId); + ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 0c861df..eea5495 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -20,6 +20,7 @@ import com.assu.server.global.exception.exception.DatabaseException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; import org.springframework.stereotype.Service; import java.util.List; @@ -36,7 +37,7 @@ public class ChatServiceImpl implements ChatService { @Override public List getChatRoomList() { // Long memberId = SecurityUtil.getCurrentUserId; - Long memberId = 1L; + Long memberId = 2L; List chatRoomList = chatRepository.findChattingRoomByMember(memberId); return ChatConverter.toChatRoomListResultDTO(chatRoomList); @@ -59,6 +60,8 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C room.updateStatus(ActivationStatus.ACTIVE); + room.updateMemberCount(2); + room.updateName( partner.getName(), admin.getName() @@ -101,11 +104,56 @@ public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) { @Override public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId) { - // Long memberId = SecurityUtil.getCurrentUserId(); +// Long memberId = SecurityUtil.getCurrentUserId(); Long memberId = 2L; List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(roomId, memberId); return ChatConverter.toChatHistoryDTO(allMessages); } + + @Override + public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId) { +// Long memberId = SecurityUtil.getCurrentUserId(); + + Long memberId = 2L; + + // 멤버 조회 + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + + // 채팅방 조회 + ChattingRoom chattingRoom = chatRepository.findById(roomId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_MEMBER_IN_THE_ROOM)); + + boolean isAdmin = chattingRoom.getAdmin() != null && + chattingRoom.getAdmin().getMember().getId().equals(member.getId()); + boolean isPartner = chattingRoom.getPartner() != null && + chattingRoom.getPartner().getMember().getId().equals(member.getId()); + + int memberCount = chattingRoom.getMemberCount(); + boolean isRoomDeleted = false; + boolean isLeftSuccessfully = false; + + if(memberCount == 2) { + if (isAdmin) { + chattingRoom.setAdmin(null); + } else if (isPartner) { + chattingRoom.setPartner(null); + } else { + throw new DatabaseException(ErrorStatus.NO_SUCH_MEMBER); + } + chattingRoom.updateMemberCount(1); + isLeftSuccessfully = true; + chatRepository.save(chattingRoom); + } else if(memberCount == 1) { + isRoomDeleted = true; + isLeftSuccessfully = true; + chatRepository.delete(chattingRoom); + + } else if(memberCount == 0) { + throw new DatabaseException(ErrorStatus.NO_MEMBER); + } + return new ChatResponseDTO.LeaveChattingRoomResponseDTO(roomId, isLeftSuccessfully,isRoomDeleted); + } } diff --git a/src/main/java/com/assu/server/domain/partner/entity/Partner.java b/src/main/java/com/assu/server/domain/partner/entity/Partner.java index ddeec3f..efc423c 100644 --- a/src/main/java/com/assu/server/domain/partner/entity/Partner.java +++ b/src/main/java/com/assu/server/domain/partner/entity/Partner.java @@ -15,6 +15,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder +@Setter public class Partner { @Id diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index 6d5b4fb..f569a0d 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -2,6 +2,7 @@ import com.assu.server.global.apiPayload.code.BaseErrorCode; import com.assu.server.global.apiPayload.code.ErrorReasonDTO; +import com.sun.net.httpserver.HttpsServer; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -24,7 +25,9 @@ public enum ErrorStatus implements BaseErrorCode { NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 partner ID 입니다."), // 채팅 에러 - NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다.") + NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."), + NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."), + NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다.") ; 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 6aa66aa..c50ebd9 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -14,6 +14,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(auth -> auth .requestMatchers( "/chat/**", + "/suggestion/**", + "/review/**", "/ws/**", "/pub/**", // STOMP 메시지 전송 "/sub/**", // STOMP 메시지 구독 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a383e9d..c17a186 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,22 @@ spring: + + batch: + jdbc: + initialize-schema: never + job: + enabled: false + + datasource: + url: jdbc:mariadb://localhost:3306/assu_maria_db?allowPublicKeyRetrieval=true&useSSL=false + username: root + password: ${MARIA_DB_PASSWORD} + driver-class-name: org.mariadb.jdbc.Driver + profiles: active: local # 여기에 local, blue, green 셋중 하나로 입력 config: import: -# - classpath:application-secret.yml + - classpath:application-secret.yml jpa: hibernate: ddl-auto: update # 여기 From f9469dcf83e4e51ecb2c7ce5fb0afdec8115140f Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Tue, 5 Aug 2025 18:50:55 +0900 Subject: [PATCH 020/270] =?UTF-8?q?feat/#13-review=20=20-=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=9E=91=EC=84=B1=20api=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../server/domain/common/entity/Member.java | 2 +- .../partner/repository/PartnerRepository.java | 5 +- .../review/controller/ReviewController.java | 24 +++++++++ .../review/converter/ReviewConverter.java | 30 +++++++++++ .../domain/review/dto/ReviewRequestDTO.java | 16 ++++++ .../domain/review/dto/ReviewResponseDTO.java | 19 +++++++ .../review/repository/ReviewRepository.java | 5 +- .../domain/review/service/ReviewService.java | 6 +++ .../review/service/ReviewServiceImpl.java | 50 ++++++++++++++++++- .../store/repository/StoreRepository.java | 6 ++- .../user/repository/StudentRepository.java | 5 +- .../apiPayload/code/status/ErrorStatus.java | 7 ++- .../server/global/config/SecurityConfig.java | 23 +++++++++ 14 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/assu/server/global/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index 29f8d3b..d8fab7c 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' // Swagger 3 (SpringDoc OpenAPI) - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4' // maria db runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java index 4687dbc..14bf9e7 100644 --- a/src/main/java/com/assu/server/domain/common/entity/Member.java +++ b/src/main/java/com/assu/server/domain/common/entity/Member.java @@ -12,7 +12,7 @@ @Getter @Entity -public class Member { +public class Member extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java index f6f0ce4..2e47953 100644 --- a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java +++ b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java @@ -1,4 +1,7 @@ package com.assu.server.domain.partner.repository; -public class PartnerRepository { +import com.assu.server.domain.partner.entity.Partner; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PartnerRepository extends JpaRepository { } diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java index de40587..96392cf 100644 --- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java +++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java @@ -1,4 +1,28 @@ package com.assu.server.domain.review.controller; +import com.assu.server.domain.review.dto.ReviewRequestDTO; +import com.assu.server.domain.review.dto.ReviewResponseDTO; +import com.assu.server.domain.review.service.ReviewService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/review") public class ReviewController { + private final ReviewService reviewService; + @Operation( + summary = "리뷰 작성 API입니다.", + description = "리뷰 내용과 별점, 리뷰 이미지를 입력해주세요." + ) + @PostMapping() + public BaseResponse writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO writeReviewRequestDTO) { + return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(writeReviewRequestDTO)); + } } diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java index ea023d0..6f0d695 100644 --- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java @@ -1,4 +1,34 @@ package com.assu.server.domain.review.converter; +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.review.dto.ReviewRequestDTO; +import com.assu.server.domain.review.dto.ReviewResponseDTO; +import com.assu.server.domain.review.entity.Review; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.user.entity.Student; + public class ReviewConverter { + public static ReviewResponseDTO.WriteReviewResponseDTO writeReviewResultDTO(Review review){ + //enti -> dto + return ReviewResponseDTO.WriteReviewResponseDTO.builder() + .reviewId(review.getId())// 리스폰스 dto로 아이디를 바꿔줄거다. + .rate(review.getRate()) + .content(review.getContent()) +// .memberId(review.getStudent().getId()) + .createdAt(review.getCreatedAt()) + //한 리뷰 여러개 사진 but 하나로 묶임 추가 고려해보기 --추후에 !! + .build(); //리스폰스 리턴 + } + public static Review toReviewEntity(ReviewRequestDTO.WriteReviewRequestDTO request, Store store, Partner partner, Student student){ + //request + return Review.builder() + .rate(request.getRate()) + .content(request.getContent()) + .store(store) + .partner(partner) + .student(student) + // .imageList(request.getReviewImage()) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java index 0d1d258..791512a 100644 --- a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java +++ b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java @@ -1,4 +1,20 @@ package com.assu.server.domain.review.dto; +import com.assu.server.domain.review.entity.ReviewPhoto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + public class ReviewRequestDTO { + @Getter + public static class WriteReviewRequestDTO { + private String content; + private Integer rate; + //private List reviewImage; + private Long storeId; + private Long partnerId; + } } diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java index 3604229..91a74a6 100644 --- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java +++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java @@ -1,4 +1,23 @@ package com.assu.server.domain.review.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + public class ReviewResponseDTO { + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class WriteReviewResponseDTO { + private Long reviewId; //entity 보고 형 맞추기 + private String content; + private Integer rate; + private LocalDateTime createdAt; + private Long memberId; + } + } diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java index 0207d17..70c6e24 100644 --- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java @@ -1,4 +1,7 @@ package com.assu.server.domain.review.repository; -public class ReviewRepository { +import com.assu.server.domain.review.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository { } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java index 3686da3..9a8ac5d 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java @@ -1,4 +1,10 @@ package com.assu.server.domain.review.service; +import com.assu.server.domain.review.dto.ReviewRequestDTO; +import com.assu.server.domain.review.dto.ReviewResponseDTO; +import org.springframework.web.bind.annotation.RequestBody; + public interface ReviewService { + ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request); + } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index 12d2f73..37a4b7c 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -1,4 +1,52 @@ package com.assu.server.domain.review.service; -public class ReviewServiceImpl { +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partner.repository.PartnerRepository; +import com.assu.server.domain.review.converter.ReviewConverter; +import com.assu.server.domain.review.dto.ReviewRequestDTO; +import com.assu.server.domain.review.dto.ReviewResponseDTO; +import com.assu.server.domain.review.entity.Review; +import com.assu.server.domain.review.repository.ReviewRepository; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.store.repository.StoreRepository; +import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ReviewServiceImpl implements ReviewService { + private final ReviewRepository reviewRepository; + private final StoreRepository storeRepository; + private final PartnerRepository partnerRepository; + private final StudentRepository studentRepository; + + + + @Override + public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request) { + //Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 1L; + Long storeId = request.getStoreId(); //변수 선언 + //존재여부 검증 + Store store = storeRepository.findById(storeId) + .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); //없을 경우!! + Partner partner = partnerRepository.findById(request.getPartnerId()) //파라미터 변수 선언 없이 바로 받기 + .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + Student student = studentRepository.findById(Math.toIntExact(memberId)) + .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT)); + + Review review = ReviewConverter.toReviewEntity(request, store, partner, student); + + + reviewRepository.save(review);//rep에서 데이터 상하차 저장 + //잘 저장 됏어요!! + return ReviewConverter.writeReviewResultDTO(review);//객체를 dto로 바꿔서 사용자에게 보여줌 -> controller + } + + } diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java index 5b7f958..ca95b1c 100644 --- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java @@ -1,4 +1,8 @@ package com.assu.server.domain.store.repository; -public class StoreRepository { +import com.assu.server.domain.store.entity.Store; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StoreRepository extends JpaRepository { + } diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java index e042199..128ab16 100644 --- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java @@ -1,4 +1,7 @@ package com.assu.server.domain.user.repository; -public class StudentRepository { +import com.assu.server.domain.user.entity.Student; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StudentRepository extends JpaRepository { } diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index ddefa9e..7180da8 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -20,7 +20,12 @@ public enum ErrorStatus implements BaseErrorCode { // 멤버 에러 NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."), - + //스토어 에러 + NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_5001", "존재하지 않는 스토어 ID입니다."), + //파트너 에러 + NO_SUCH_PARTNER(HttpStatus.NOT_FOUND, "PARTNER_4002", "존재하지 않는 파트너 ID입니다."), + //스투던트 에러 + NO_SUCH_STUDENT(HttpStatus.NOT_FOUND, "STUDENT_4003", "존재하지 않는 학생 ID입니다.") ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java new file mode 100644 index 0000000..dc03cbd --- /dev/null +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -0,0 +1,23 @@ +package com.assu.server.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // ⭐ 모든 요청 허용 + ) + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .formLogin(login -> login.disable()) + .httpBasic(basic -> basic.disable()); + + return http.build(); + } +} \ No newline at end of file From 9faab14ba824fc2a46cf0958e846d1cf4ba3824e Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Tue, 5 Aug 2025 18:55:58 +0900 Subject: [PATCH 021/270] =?UTF-8?q?[Feat/#14]=20=20-=20=EC=A0=9C=ED=9C=B4?= =?UTF-8?q?=20=EA=B1=B4=EC=9D=98=20API=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../admin/repository/AdminRepository.java | 5 ++- .../server/domain/common/entity/Member.java | 2 +- .../server/domain/review/entity/Review.java | 2 +- .../store/repository/StoreRepository.java | 5 ++- .../controller/SuggestionController.java | 28 +++++++++++++ .../converter/SuggestionConverter.java | 28 +++++++++++++ .../suggestion/dto/SuggestionRequestDTO.java | 10 +++++ .../suggestion/dto/SuggestionResponseDTO.java | 19 +++++++++ .../domain/suggestion/entity/Suggestion.java | 2 +- .../repository/SuggestionRepository.java | 6 ++- .../suggestion/service/SuggestionService.java | 10 +++++ .../service/SuggestionServiceImpl.java | 39 ++++++++++++++++++- .../server/domain/user/entity/Student.java | 2 + .../user/repository/StudentRepository.java | 5 ++- .../apiPayload/code/status/ErrorStatus.java | 3 ++ .../server/global/config/SecurityConfig.java | 23 +++++++++++ 17 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/assu/server/global/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index 29f8d3b..d8fab7c 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' // Swagger 3 (SpringDoc OpenAPI) - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4' // maria db runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java index 4e6b1fa..4fd442a 100644 --- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java +++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java @@ -1,4 +1,7 @@ package com.assu.server.domain.admin.repository; -public class AdminRepository { +import com.assu.server.domain.admin.entity.Admin; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminRepository extends JpaRepository { } diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java index 4687dbc..857de7b 100644 --- a/src/main/java/com/assu/server/domain/common/entity/Member.java +++ b/src/main/java/com/assu/server/domain/common/entity/Member.java @@ -12,7 +12,7 @@ @Getter @Entity -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/assu/server/domain/review/entity/Review.java b/src/main/java/com/assu/server/domain/review/entity/Review.java index 0045706..97b6af0 100644 --- a/src/main/java/com/assu/server/domain/review/entity/Review.java +++ b/src/main/java/com/assu/server/domain/review/entity/Review.java @@ -45,7 +45,7 @@ public class Review extends BaseEntity { private Store store; @OneToMany(mappedBy= "review", cascade = CascadeType.ALL) - private List imageList = new ArrayList<>(); + private final List imageList = new ArrayList<>(); private Integer rate; private String content; diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java index 5b7f958..fb3611f 100644 --- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java @@ -1,4 +1,7 @@ package com.assu.server.domain.store.repository; -public class StoreRepository { +import com.assu.server.domain.store.entity.Store; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StoreRepository extends JpaRepository { } diff --git a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java index e65d553..50ccb9c 100644 --- a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java +++ b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java @@ -1,4 +1,32 @@ package com.assu.server.domain.suggestion.controller; +import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO; +import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO; +import com.assu.server.domain.suggestion.service.SuggestionService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +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; + +@RestController +@RequiredArgsConstructor // 파라미터가 있어야만 하는 생성자 +@RequestMapping("/suggestion") // suggestion 아래에서 시작 public class SuggestionController { + + private final SuggestionService suggestionService; + + @Operation( + summary = "제휴 건의를 하는 API 입니다.", + description = "건의대상, 제휴 희망 가게, 희망 혜택을 입력해주세요." + ) + @PostMapping() + public BaseResponse writeSuggestion( + @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO + ){ + return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO)); + } } diff --git a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java index c13cabf..bb53db5 100644 --- a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java +++ b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java @@ -1,4 +1,32 @@ package com.assu.server.domain.suggestion.converter; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO; +import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO; +import com.assu.server.domain.suggestion.entity.Suggestion; +import com.assu.server.domain.user.entity.Student; + public class SuggestionConverter { + + public static SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestionResultDTO(Suggestion suggestion){ + return SuggestionResponseDTO.WriteSuggestionResponseDTO.builder() + .suggestionId(suggestion.getId()) + .memberId(suggestion.getStudent().getId()) + .studentNumber(suggestion.getStudent().getStudentNumber()) + .suggestionSubjectId(suggestion.getAdmin().getId()) + .suggestionStore(suggestion.getStoreName()) + .suggestionBenefit(suggestion.getContent()) + .build(); + } + + public static Suggestion toSuggestionEntity(SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO, Admin admin, Student student){ +// 여기서 뭘 할거냐면 사용자에게서 데이터를 받았으면 걔를 return 하면서 entity로 + return Suggestion.builder() + .admin(admin) + .student(student) + .storeName(suggestionRequestDTO.getStoreName()) + .content(suggestionRequestDTO.getBenefit()) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java index a17553a..bdf11eb 100644 --- a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java +++ b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java @@ -1,4 +1,14 @@ package com.assu.server.domain.suggestion.dto; +import lombok.Getter; + public class SuggestionRequestDTO { + + @Getter + public static class WriteSuggestionRequestDTO{ + private Long adminId; // 건의 대상 + private Long studentId; // 건의자 아이디 + private String storeName; // 희망 가게 + private String benefit; // 희망 혜택 + } } diff --git a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java index 83efa3d..08f47cc 100644 --- a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java +++ b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java @@ -1,4 +1,23 @@ package com.assu.server.domain.suggestion.dto; +import com.assu.server.domain.admin.entity.Admin; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + public class SuggestionResponseDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class WriteSuggestionResponseDTO { + private Long suggestionId; // 제휴 번호 + private Long memberId; // 제안인 아이디 + private Long studentNumber; // 제안인 학번 + private Long suggestionSubjectId; // 건의 대상 아이디 + private String suggestionStore; // 희망 가게 이름 + private String suggestionBenefit; // 희망 혜택 + } } diff --git a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java index 7a22608..e3e6685 100644 --- a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java +++ b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java @@ -35,6 +35,6 @@ public class Suggestion extends BaseEntity { @JoinColumn(name = "student_id") private Student student; - private String shopName; + private String storeName; private String content; } diff --git a/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java b/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java index 747cf92..9239b54 100644 --- a/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java +++ b/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java @@ -1,4 +1,8 @@ package com.assu.server.domain.suggestion.repository; -public class SuggestionRepository { +import com.assu.server.domain.suggestion.entity.Suggestion; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SuggestionRepository extends JpaRepository { + } diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java index 15b27d9..188ccfe 100644 --- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java +++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java @@ -1,4 +1,14 @@ package com.assu.server.domain.suggestion.service; +import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO; +import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO; +import com.assu.server.domain.suggestion.entity.Suggestion; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + public interface SuggestionService { + + SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion( + @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO request + ); } diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java index 912d087..c2eac45 100644 --- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java +++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java @@ -1,4 +1,41 @@ package com.assu.server.domain.suggestion.service; -public class SuggestionServiceImpl { +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.suggestion.converter.SuggestionConverter; +import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO; +import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO; +import com.assu.server.domain.suggestion.entity.Suggestion; +import com.assu.server.domain.suggestion.repository.SuggestionRepository; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SuggestionServiceImpl implements SuggestionService { + + private final SuggestionRepository suggestionRepository; + private final AdminRepository adminRepository; + private final StudentRepository studentRepository; + + @Override + public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(SuggestionRequestDTO.WriteSuggestionRequestDTO request) { +// Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 1L; + Admin admin = adminRepository.findById(request.getAdminId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + + Student student = studentRepository.findById(memberId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + + Suggestion suggestion = SuggestionConverter.toSuggestionEntity(request, admin, student); + suggestionRepository.save(suggestion); + + return SuggestionConverter.writeSuggestionResultDTO(suggestion); + } } diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java index 89544b0..a73258b 100644 --- a/src/main/java/com/assu/server/domain/user/entity/Student.java +++ b/src/main/java/com/assu/server/domain/user/entity/Student.java @@ -31,6 +31,8 @@ public class Student { private int stamp; + private Long studentNumber; + @Enumerated(EnumType.STRING) private Major major; } diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java index e042199..9a4c6ce 100644 --- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java @@ -1,4 +1,7 @@ package com.assu.server.domain.user.repository; -public class StudentRepository { +import com.assu.server.domain.user.entity.Student; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StudentRepository extends JpaRepository { } diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index ddefa9e..c7b4ed5 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -21,6 +21,9 @@ public enum ErrorStatus implements BaseErrorCode { // 멤버 에러 NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."), + // 어드민 에러 + NO_SUCH_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_5001", "존재하지 않는 학생회입니다."), + ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java new file mode 100644 index 0000000..9ba2024 --- /dev/null +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -0,0 +1,23 @@ +package com.assu.server.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // ⭐ 모든 요청 허용 + ) + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .formLogin(login -> login.disable()) + .httpBasic(basic -> basic.disable()); + + return http.build(); + } +} From 54239831cdba74bf7c892d7c84c9732808fb75dc Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Tue, 5 Aug 2025 19:54:36 +0900 Subject: [PATCH 022/270] =?UTF-8?q?feat/#13-review=20=20-=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 16 ++++++++++++---- .../review/converter/ReviewConverter.java | 17 +++++++++++++++++ .../domain/review/dto/ReviewResponseDTO.java | 13 +++++++++++++ .../review/repository/ReviewRepository.java | 11 +++++++++++ .../domain/review/service/ReviewService.java | 5 ++++- .../review/service/ReviewServiceImpl.java | 9 +++++++++ 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java index 96392cf..368db7d 100644 --- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java +++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java @@ -7,10 +7,9 @@ import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -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 org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController @RequiredArgsConstructor @@ -25,4 +24,13 @@ public class ReviewController { public BaseResponse writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO writeReviewRequestDTO) { return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(writeReviewRequestDTO)); } + + @Operation( + summary = "내가 쓴 리뷰 조회 API입니다.", + description = "Autorization 후에 사용해주세요." + ) + @GetMapping("/{studentId}") + public BaseResponse> checkStudent() { + return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview()); + } } diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java index 6f0d695..31f8522 100644 --- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java @@ -8,6 +8,9 @@ import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.user.entity.Student; +import java.util.List; +import java.util.stream.Collectors; + public class ReviewConverter { public static ReviewResponseDTO.WriteReviewResponseDTO writeReviewResultDTO(Review review){ //enti -> dto @@ -31,4 +34,18 @@ public static Review toReviewEntity(ReviewRequestDTO.WriteReviewRequestDTO requ // .imageList(request.getReviewImage()) .build(); } + public static ReviewResponseDTO.CheckStudentReviewResponseDTO checkStudentReviewResultDTO(Review review){ + return ReviewResponseDTO.CheckStudentReviewResponseDTO.builder() + .reviewId(review.getId()) + .rate(review.getRate()) + .content(review.getContent()) + .createdAt(review.getCreatedAt()) + .storeId(review.getStore().getId()) + .build(); + } + public static List checkStudentReviewResultDTO(List reviews){ + return reviews.stream() + .map(ReviewConverter::checkStudentReviewResultDTO) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java index 91a74a6..7a760a4 100644 --- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java +++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java @@ -19,5 +19,18 @@ public static class WriteReviewResponseDTO { private LocalDateTime createdAt; private Long memberId; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CheckStudentReviewResponseDTO { //내가 작성한 리뷰 + private Long reviewId; + private Long storeId; + private String content; + private Integer rate; + private LocalDateTime createdAt; + //private List reviewImage; + } + } diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java index 70c6e24..365e2d6 100644 --- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java @@ -2,6 +2,17 @@ import com.assu.server.domain.review.entity.Review; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface ReviewRepository extends JpaRepository { + @Query(""" + SELECT r + FROM Review r + WHERE r.student.id = :memberId + ORDER BY r.createdAt DESC +""") + List findByMemberId(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java index 9a8ac5d..6d6966a 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java @@ -2,9 +2,12 @@ import com.assu.server.domain.review.dto.ReviewRequestDTO; import com.assu.server.domain.review.dto.ReviewResponseDTO; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import java.util.List; + public interface ReviewService { ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request); - + List checkStudentReview(); } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index 37a4b7c..f8f2e80 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -17,6 +17,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class ReviewServiceImpl implements ReviewService { @@ -48,5 +50,12 @@ public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.Wri return ReviewConverter.writeReviewResultDTO(review);//객체를 dto로 바꿔서 사용자에게 보여줌 -> controller } + @Override + public List checkStudentReview() { + //Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 1L; + List reviews = reviewRepository.findByMemberId(memberId); + return ReviewConverter.checkStudentReviewResultDTO(reviews); + } } From f998faea95dd070bba466e2905361a46b9b91274 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Tue, 5 Aug 2025 20:18:29 +0900 Subject: [PATCH 023/270] =?UTF-8?q?feat/#13-review=20=20-=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=82=AD=EC=A0=9C=20api=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/controller/ReviewController.java | 10 ++++++++++ .../domain/review/converter/ReviewConverter.java | 6 +++++- .../server/domain/review/dto/ReviewResponseDTO.java | 8 +++++++- .../server/domain/review/service/ReviewService.java | 1 + .../domain/review/service/ReviewServiceImpl.java | 8 ++++++++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java index 368db7d..8eb5259 100644 --- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java +++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java @@ -33,4 +33,14 @@ public BaseResponse writeReview(@Reque public BaseResponse> checkStudent() { return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview()); } + + @Operation( + summary = "내가 쓴 리뷰 삭제 API입니다.", + description = "삭제할 리뷰 ID를 입력해주세요." + ) + @DeleteMapping("/{reviewId}") + public BaseResponse deleteReview(@PathVariable Long reviewId) { + return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.deleteReview(reviewId)); + } + } diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java index 31f8522..6181650 100644 --- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java @@ -1,6 +1,5 @@ package com.assu.server.domain.review.converter; -import com.assu.server.domain.common.entity.Member; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.review.dto.ReviewRequestDTO; import com.assu.server.domain.review.dto.ReviewResponseDTO; @@ -48,4 +47,9 @@ public static List checkStudent .map(ReviewConverter::checkStudentReviewResultDTO) .collect(Collectors.toList()); } + public static ReviewResponseDTO.DeleteReviewResponseDTO deleteReviewResultDTO(Long reviewId){ + return ReviewResponseDTO.DeleteReviewResponseDTO.builder() + .reviewId(reviewId) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java index 7a760a4..58b7c04 100644 --- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java +++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java @@ -31,6 +31,12 @@ public static class CheckStudentReviewResponseDTO { //내가 작성한 리뷰 private LocalDateTime createdAt; //private List reviewImage; } - + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class DeleteReviewResponseDTO { + private Long reviewId; + } } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java index 6d6966a..d15252d 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java @@ -10,4 +10,5 @@ public interface ReviewService { ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request); List checkStudentReview(); + ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(@PathVariable Long reviewId); } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index f8f2e80..f3c9f49 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -14,6 +14,7 @@ import com.assu.server.domain.user.repository.StudentRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.exception.DatabaseException; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -58,4 +59,11 @@ public List checkStudentReview( return ReviewConverter.checkStudentReviewResultDTO(reviews); } + + @Override + @Transactional + public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) { + reviewRepository.deleteById(reviewId); + return ReviewConverter.deleteReviewResultDTO(reviewId); + } } From d4232d72ec296629689b42730db725710ca12c3a Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 6 Aug 2025 11:20:13 +0900 Subject: [PATCH 024/270] =?UTF-8?q?[FEAT/#15]=20=EA=B3=B5=EB=8F=99?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../certification/SessionTimeoutManager.java | 47 ++++++ .../CertificationSessionManager.java | 32 ++++ .../config/CertifyWebSocketConfig.java | 23 +++ .../controller/CertificationController.java | 80 ++++++++++ .../converter/CertificationConverter.java | 23 +++ .../dto/CertificationRequestDTO.java | 17 ++ .../dto/CertificationResponseDTO.java | 13 ++ .../certification/dto/CurrentProgress.java | 27 ++++ .../entity/AssociateCertification.java | 16 ++ .../certification/entity/QRCertification.java | 4 + .../entity/enums/SessionStatus.java | 5 + .../AssociateCertificationRepository.java | 9 ++ .../repository/QRCertificationRepository.java | 8 + .../service/CertificationService.java | 8 + .../service/CertificationServiceImpl.java | 146 +++++++++++++++++- .../apiPayload/code/status/ErrorStatus.java | 15 ++ .../apiPayload/code/status/SuccessStatus.java | 9 +- 17 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/certification/SessionTimeoutManager.java create mode 100644 src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java create mode 100644 src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java create mode 100644 src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java create mode 100644 src/main/java/com/assu/server/domain/certification/entity/enums/SessionStatus.java create mode 100644 src/main/java/com/assu/server/domain/certification/repository/AssociateCertificationRepository.java create mode 100644 src/main/java/com/assu/server/domain/certification/repository/QRCertificationRepository.java diff --git a/src/main/java/com/assu/server/domain/certification/SessionTimeoutManager.java b/src/main/java/com/assu/server/domain/certification/SessionTimeoutManager.java new file mode 100644 index 0000000..2ec76d7 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/SessionTimeoutManager.java @@ -0,0 +1,47 @@ +package com.assu.server.domain.certification; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.assu.server.domain.certification.component.CertificationSessionManager; +import com.assu.server.domain.certification.entity.AssociateCertification; +import com.assu.server.domain.certification.entity.enums.SessionStatus; +import com.assu.server.domain.certification.repository.AssociateCertificationRepository; + +@Component +public class SessionTimeoutManager { + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + @Autowired + private AssociateCertificationRepository certificationRepository; + + @Autowired + private CertificationSessionManager sessionManager; + + public void scheduleTimeout(Long sessionId, Duration timeout) { + scheduler.schedule(() -> { + closeSession(sessionId); + }, timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + private void closeSession(Long sessionId) { + Optional certOpt = certificationRepository.findById(sessionId); + certOpt.ifPresent(cert -> { + if (cert.getStatus() == SessionStatus.OPENED) { + cert.setStatus(SessionStatus.EXPIRED); + certificationRepository.save(cert); + } + }); + // 이러면 인증 전에 만료되는 것은 EXPIRED로, 시간안에 인증 된 세션은 COMPLETED로 남음 + + // 메모리에서도 세션 제거 + sessionManager.removeSession(sessionId); + } +} diff --git a/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java new file mode 100644 index 0000000..c1332b8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java @@ -0,0 +1,32 @@ +package com.assu.server.domain.certification.component; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; + +@Component +public class CertificationSessionManager { + private final Map> sessionUserMap = new ConcurrentHashMap<>(); + + public void openSession(Long sessionId) { + sessionUserMap.put(sessionId, ConcurrentHashMap.newKeySet()); + } + + public void addUserToSession(Long sessionId, Long userId) { + sessionUserMap.getOrDefault(sessionId, ConcurrentHashMap.newKeySet()).add(userId); + } + + public int getCurrentUserCount(Long sessionId) { + return sessionUserMap.getOrDefault(sessionId, Set.of()).size(); + } + + public boolean hasUser(Long sessionId, Long userId) { + return sessionUserMap.getOrDefault(sessionId, Set.of()).contains(userId); + } + + public void removeSession(Long sessionId) { + sessionUserMap.remove(sessionId); + } +} diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java new file mode 100644 index 0000000..8706bb7 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java @@ -0,0 +1,23 @@ +package com.assu.server.domain.certification.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@EnableWebSocketMessageBroker +@Configuration +public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer { + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/certification/progress"); // 인증현황을 받아보기 위한 구독 주소 + config.setApplicationDestinationPrefixes("/certification"); // 클라이언트가 인증 요청을 보내는 주소 + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") // 클라이언트 WebSocket 연결 주소 + .setAllowedOriginPatterns("*"); // CORS 허용 + } +} diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java index 25a1bc6..5a147d4 100644 --- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java +++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java @@ -1,4 +1,84 @@ package com.assu.server.domain.certification.controller; +import java.time.LocalDateTime; + +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.assu.server.domain.certification.dto.CertificationRequestDTO; +import com.assu.server.domain.certification.dto.CertificationResponseDTO; +import com.assu.server.domain.certification.service.CertificationService; +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.entity.enums.EnrollmentStatus; +import com.assu.server.domain.user.entity.enums.Major; +import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.util.PrincipalDetails; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "제휴 인증 api", description = "qr인증과 관련된 api 입니다.") +@RequiredArgsConstructor public class CertificationController { + + private final CertificationService certificationService; + + @PostMapping("/certification/session") + @Operation(summary = "세션 정보를 요청하는 api", description = "인원 수 기준이 요구되는 제휴일 때 세션을 만들고, 대표자 QR에 담을 정보를 요청하는 api 입니다.") + public ResponseEntity> getSessionId( + @AuthenticationPrincipal PrincipalDetails userDetails, + @RequestBody CertificationRequestDTO.groupRequest dto + ) { + + Member member = userDetails.getMember(); + CertificationResponseDTO.getSessionIdResponse result = certificationService.getSessionId(dto, member); + + return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_SESSION_CREATE, result)); + } + + @MessageMapping("/certify") + @Operation(summary = "그룹 세션 인증 api", description = "그룹에 대한 세션 인증 요청을 보냅니다.") + public ResponseEntity certifyGroup( + CertificationRequestDTO.groupSessionRequest dto // 나중에 여기에 Security + WebSocket 설정 완료한 후 + // @AuthenticationPrincipal 넣어주기 + + ) { + // 일단 더미 유저로 + Member member = new Member(); + member.setId(1L); + member.setIsActivated(ActivationStatus.ACTIVE); + member.setRole(UserRole.USER); + member.setIsPhoneVerified(true); + member.setPhoneNum("01012345678"); + member.setPhoneVerifiedAt(LocalDateTime.now()); + + Student dummyStudent = Student.builder() + .member(member) + .department("IT대학") + .enrollmentStatus(EnrollmentStatus.ENROLLED) + .yearSemester("2025-1") + .university("숭실대학교") + .stamp(0) + .major(Major.COM) + .build(); + + // Member와 StudentProfile 연결 (양방향인 경우) + member.setStudentProfile(dummyStudent); + + certificationService.handleCertification(dto, member); + + return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_CERTIFICATION_SUCCESS, null)); + } + } diff --git a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java index 1a293d1..581c4aa 100644 --- a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java +++ b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java @@ -1,4 +1,27 @@ package com.assu.server.domain.certification.converter; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.certification.dto.CertificationRequestDTO; +import com.assu.server.domain.certification.dto.CertificationResponseDTO; +import com.assu.server.domain.certification.entity.AssociateCertification; +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.store.entity.Store; + public class CertificationConverter { + public static AssociateCertification toAssociateCertification(CertificationRequestDTO.groupRequest dto, Store store, Member member) { + return AssociateCertification.builder() + .store(store) + .partner(store.getPartner()) + .isCertified(false) + .peopleNumber(dto.getPeople()) + .tableNumber(dto.getTableNumber()) + .student(member.getStudentProfile()) + .build(); + } + + public static CertificationResponseDTO.getSessionIdResponse toSessionIdResponse(Long sessionId, Long adminId){ + return CertificationResponseDTO.getSessionIdResponse.builder() + .sessionId(sessionId).adminId(adminId) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java index c9efcf7..8bdab6e 100644 --- a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java +++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java @@ -1,4 +1,21 @@ package com.assu.server.domain.certification.dto; +import lombok.Getter; + + public class CertificationRequestDTO { + + @Getter + public static class groupRequest{ + Integer people; + String storeName; + String adminName; + Integer tableNumber; + } + + @Getter + public static class groupSessionRequest{ + Long adminId; + Long sessionId; + } } diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java index 9ebea4d..b63966f 100644 --- a/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java +++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java @@ -1,4 +1,17 @@ package com.assu.server.domain.certification.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + public class CertificationResponseDTO { + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class getSessionIdResponse { + Long sessionId; + Long adminId; + } } diff --git a/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java b/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java new file mode 100644 index 0000000..2c8a35b --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java @@ -0,0 +1,27 @@ +package com.assu.server.domain.certification.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@AllArgsConstructor +public class CurrentProgress { + private int count; + + + public static class CertificationNumber{ + public CertificationNumber(int count){ + this.count= count; + } + int count; + } + + public static class CompletedNotification{ + public CompletedNotification(String message){ + this.message= message; + } + String message; + } + +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java b/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java index 7befda2..24d261e 100644 --- a/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java +++ b/src/main/java/com/assu/server/domain/certification/entity/AssociateCertification.java @@ -1,10 +1,13 @@ package com.assu.server.domain.certification.entity; +import com.assu.server.domain.certification.entity.enums.SessionStatus; import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.user.entity.Student; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -29,6 +32,10 @@ public class AssociateCertification extends BaseEntity { private Integer tableNumber; private Boolean isCertified; + private Integer peopleNumber; + + @Enumerated(EnumType.STRING) + private SessionStatus status; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "store_id") @@ -41,4 +48,13 @@ public class AssociateCertification extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "student_id") private Student student; + + public void setStatus(SessionStatus status) { + this.status = status; + } + public void setIsCertified(Boolean isCertified) { + this.isCertified = isCertified; + } + + } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java b/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java index 588f380..89f3df9 100644 --- a/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java +++ b/src/main/java/com/assu/server/domain/certification/entity/QRCertification.java @@ -32,6 +32,10 @@ public class QRCertification extends BaseEntity { @JoinColumn(name = "member_id") private Student student; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "certification_id") + private AssociateCertification certification; + private Boolean isVerified; private LocalDateTime verifiedTime; } diff --git a/src/main/java/com/assu/server/domain/certification/entity/enums/SessionStatus.java b/src/main/java/com/assu/server/domain/certification/entity/enums/SessionStatus.java new file mode 100644 index 0000000..3a7091e --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/entity/enums/SessionStatus.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.certification.entity.enums; + +public enum SessionStatus { + OPENED, COMPLETED, EXPIRED +} diff --git a/src/main/java/com/assu/server/domain/certification/repository/AssociateCertificationRepository.java b/src/main/java/com/assu/server/domain/certification/repository/AssociateCertificationRepository.java new file mode 100644 index 0000000..945cedc --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/repository/AssociateCertificationRepository.java @@ -0,0 +1,9 @@ +package com.assu.server.domain.certification.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.assu.server.domain.certification.entity.AssociateCertification; + +public interface AssociateCertificationRepository extends JpaRepository { + +} diff --git a/src/main/java/com/assu/server/domain/certification/repository/QRCertificationRepository.java b/src/main/java/com/assu/server/domain/certification/repository/QRCertificationRepository.java new file mode 100644 index 0000000..ff7029f --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/repository/QRCertificationRepository.java @@ -0,0 +1,8 @@ +package com.assu.server.domain.certification.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.assu.server.domain.certification.entity.QRCertification; + +public interface QRCertificationRepository extends JpaRepository { +} diff --git a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java index ffb49a8..e9403c8 100644 --- a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java +++ b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java @@ -1,4 +1,12 @@ package com.assu.server.domain.certification.service; +import com.assu.server.domain.certification.dto.CertificationRequestDTO; +import com.assu.server.domain.certification.dto.CertificationResponseDTO; +import com.assu.server.domain.common.entity.Member; + public interface CertificationService { + + CertificationResponseDTO.getSessionIdResponse getSessionId(CertificationRequestDTO.groupRequest dto, Member member); + + void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member); } 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 c5fc2fd..bb7b44f 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 @@ -1,4 +1,148 @@ package com.assu.server.domain.certification.service; -public class CertificationServiceImpl { +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + + +import org.springframework.stereotype.Service; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.admin.service.AdminService; +import com.assu.server.domain.certification.SessionTimeoutManager; +import com.assu.server.domain.certification.component.CertificationSessionManager; +import com.assu.server.domain.certification.converter.CertificationConverter; +import com.assu.server.domain.certification.dto.CertificationRequestDTO; +import com.assu.server.domain.certification.dto.CertificationResponseDTO; +import com.assu.server.domain.certification.dto.CurrentProgress; +import com.assu.server.domain.certification.entity.AssociateCertification; +import com.assu.server.domain.certification.entity.QRCertification; +import com.assu.server.domain.certification.entity.enums.SessionStatus; +import com.assu.server.domain.certification.repository.AssociateCertificationRepository; +import com.assu.server.domain.certification.repository.QRCertificationRepository; +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.store.repository.StoreRepository; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.GeneralException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +// AdminService 참조, 순환 참조 문제 주의 +@Transactional +@Service +@RequiredArgsConstructor +public class CertificationServiceImpl implements CertificationService { + private final AdminRepository adminRepository; + private final StoreRepository storeRepository; + private final AssociateCertificationRepository associateCertificationRepository; + private final QRCertificationRepository qrRepository; + + // 세션 메니저 + private final CertificationSessionManager sessionManager; + private final SessionTimeoutManager timeoutManager; + + // AdminService 참조 + private final AdminService adminService; + private final SimpMessagingTemplate messagingTemplate; + + + + @Override + public CertificationResponseDTO.getSessionIdResponse getSessionId( + CertificationRequestDTO.groupRequest dto, Member member){ + Long userId = member.getId(); + + // admin id 추출 + Admin admin = adminRepository.findByName(dto.getAdminName()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_ADMIN) + ); + + // store id 추출 + Store store = storeRepository.findByName(dto.getStoreName()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) + ); + + + // 세션 생성 및 구독 로직 + AssociateCertification ownerCertification = associateCertificationRepository.save( + CertificationConverter.toAssociateCertification(dto, store, member)); + Long sessionId = ownerCertification.getId(); + + sessionManager.openSession(sessionId); + // 세션 생성 직후 만료 시간을 5분으로 설정 + timeoutManager.scheduleTimeout(sessionId, Duration.ofMinutes(5)); + + // 세션 여는 대표자는 제일 먼저 인증 + sessionManager.addUserToSession(sessionId, userId); + + return CertificationConverter.toSessionIdResponse(sessionId, admin.getId()); + + } + + @Override + public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member) { + + Long userId = member.getId(); + + // 제휴 대상인지 확인하기 + Long adminId = dto.getAdminId(); + Student student = member.getStudentProfile(); + List admins = adminService.findMatchingAdmins(student.getUniversity(), student.getDepartment(), student.getMajor()); + + boolean matched = admins.stream() + .anyMatch(admin -> admin.getId().equals(adminId)); + + if (!matched) { + throw new IllegalArgumentException("관리자 정보가 일치하지 않습니다."); + } + + + // session 존재 여부 확인 + Long sessionId = dto.getSessionId(); + AssociateCertification session = associateCertificationRepository.findById(sessionId).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_SESSION) + ); + + // 세션 활성화 여부 확인 + if(session.getStatus() != SessionStatus.OPENED) + throw new GeneralException(ErrorStatus.SESSION_NOT_OPENED); + + boolean isDoubledUser= sessionManager.hasUser(sessionId, userId); + if(isDoubledUser) + throw new GeneralException(ErrorStatus.DOUBLE_CERTIFIED_USER); + + sessionManager.addUserToSession(sessionId, userId); + int currentCertifiedNumber = sessionManager.getCurrentUserCount(sessionId); + + messagingTemplate.convertAndSend("/certification/progress"+sessionId, + new CurrentProgress.CertificationNumber(currentCertifiedNumber)); + + if(currentCertifiedNumber >= session.getPeopleNumber()){ + session.setIsCertified(true); + session.setStatus(SessionStatus.COMPLETED); + associateCertificationRepository.save(session); + + + messagingTemplate.convertAndSend("certification/progress"+sessionId, + new CurrentProgress.CompletedNotification("인증이 완료되었습니다.") + ); + } + + // 인증 정보를 QRCertification 에 삽입 + QRCertification qrCertification = new QRCertification(); + qrCertification.builder() + .certification(session) + .verifiedTime(LocalDateTime.now()) + .isVerified(true) + .build(); + qrRepository.save(qrCertification); + + } + + + } diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index ddefa9e..c59ea17 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -20,6 +20,21 @@ public enum ErrorStatus implements BaseErrorCode { // 멤버 에러 NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."), + NO_STUENT_TYPE(HttpStatus.BAD_REQUEST, "MEMBER4002", "학생 타입이 아닌 멤버입니다."), + + + // 관리자 에러 + NO_SUCH_ADMIN(HttpStatus.NOT_FOUND, "ADMIN4001", "존재하지 않는 관리자 ID입니다."), + + + // store 에러 + NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE4001", "존재하지 않은 store ID입니다."), + + + // session 에러 + NO_SUCH_SESSION(HttpStatus.NOT_FOUND, "SESSION4001", "존재하지 않는 session ID입니다."), + SESSION_NOT_OPENED(HttpStatus.BAD_REQUEST, "SESSION4002", "만료되었거나 인증이 완료된 session ID입니다."), + DOUBLE_CERTIFIED_USER(HttpStatus.BAD_REQUEST, "SESSION4003", "이미 인증된 유저입니다.") ; diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java index 0ca43cf..38fb93d 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java @@ -95,8 +95,15 @@ public enum SuccessStatus implements BaseCode { REPORT_PROFILE_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "계정 신고의 정보가 성공적으로 조회되었습니다."), REPORT_PROFILE_SUCCESS(HttpStatus.OK, "REPORT_201", "계정을 성공적으로 신고했습니다."), REPORT_ADMIN_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200","관리자용 신고 기록이 성공적으로 조회되었습니다."), - REPORT_ADMIN_PROCESSED(HttpStatus.OK,"REPORT_204","신고가 성공적으로 처리되었습니다.") + REPORT_ADMIN_PROCESSED(HttpStatus.OK,"REPORT_204","신고가 성공적으로 처리되었습니다."), + // 제휴 성공 + PAPER_HISTORY_SUCCESS(HttpStatus.OK, "PAPER201", "가게 별 제휴 내용이 성공적으로 조회되었습니다."), + + + // 그룹 인증 + GROUP_SESSION_CREATE(HttpStatus.OK, "GROUP201", "인증 세션 생성 및 대표자 구독이 완료되었습니다."), + GROUP_CERTIFICATION_SUCCESS(HttpStatus.OK, "GROUP202", "그룹 인증 세션에 대한 인증이 완료되었습니다.") ; private final HttpStatus httpStatus; From e893d2bf54fc67f7e2f1cbdb84277481e9cdaaac Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 6 Aug 2025 11:22:08 +0900 Subject: [PATCH 025/270] =?UTF-8?q?[FEAT/#15]=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EC=99=80=20=EC=9D=BC=EC=B9=98=ED=95=98?= =?UTF-8?q?=EB=8A=94=20admin=20=EC=B0=BE=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 10 +++++++ .../admin/repository/AdminRepository.java | 23 ++++++++++++++- .../domain/admin/service/AdminService.java | 7 +++++ .../admin/service/AdminServiceImpl.java | 28 ++++++++++++++++++- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 29f8d3b..cbfbe06 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,16 @@ dependencies { // h2 db (test) runtimeOnly 'com.h2database:h2' + + // WebSocket 기본 기능 + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework:spring-messaging' + + // STOMP 메세징 지원 + implementation 'org.springframework.boot:spring-boot-starter-web' + + // JSON 처리 + implementation 'com.fasterxml.jackson.core:jackson-databind' } tasks.named('test') { diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java index 4e6b1fa..79a9a62 100644 --- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java +++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java @@ -1,4 +1,25 @@ package com.assu.server.domain.admin.repository; -public class AdminRepository { +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.user.entity.enums.Major; + +public interface AdminRepository extends JpaRepository { + + @Query("SELECT a FROM Admin a WHERE " + + "a.name LIKE %:university% OR " + + "a.name LIKE %:department% OR " + + "a.major= :major") + List findMatchingAdmins(@Param("university") String university, + @Param("department") String department, + @Param("major") Major major); + + Optional findByName(String name); + } diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminService.java b/src/main/java/com/assu/server/domain/admin/service/AdminService.java index c01f598..68add7f 100644 --- a/src/main/java/com/assu/server/domain/admin/service/AdminService.java +++ b/src/main/java/com/assu/server/domain/admin/service/AdminService.java @@ -1,4 +1,11 @@ package com.assu.server.domain.admin.service; +import java.util.List; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.user.entity.enums.Major; + +// PaperQueryServiceImpl 이 AdminService 참조 중 -> 순환참조 문제 발생하지 않도록 주의 public interface AdminService { + List findMatchingAdmins(String university, String department, Major major); } diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java index 3c3eb41..c7d6c9b 100644 --- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java @@ -1,4 +1,30 @@ package com.assu.server.domain.admin.service; -public class AdminServiceImpl { +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.user.entity.enums.Major; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Transactional +@Service +@RequiredArgsConstructor +public class AdminServiceImpl implements AdminService { + + private final AdminRepository adminRepository; + + // 유저의 정보와 맞는 admin 을 찾기 + @Override + public List findMatchingAdmins(String university, String department, Major major){ + + + List adminList = adminRepository.findMatchingAdmins(university, department,major); + + return adminList; + } } From 9d3fad80c3174de0b85aff3fce3973370bee59da Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 6 Aug 2025 11:22:24 +0900 Subject: [PATCH 026/270] =?UTF-8?q?[FEAT/#15]=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20set=20method=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/admin/entity/Admin.java | 8 +++ .../server/domain/common/entity/Member.java | 51 +++++++++++++++++++ .../server/domain/partner/entity/Partner.java | 4 ++ .../server/domain/user/entity/Student.java | 4 ++ 4 files changed, 67 insertions(+) diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java index 271200b..234ab4a 100644 --- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java +++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java @@ -1,6 +1,8 @@ package com.assu.server.domain.admin.entity; import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.user.entity.enums.Major; + import jakarta.persistence.Entity; import jakarta.persistence.JoinColumn; import jakarta.persistence.MapsId; @@ -36,4 +38,10 @@ public class Admin { private Boolean isSignVerified; private LocalDateTime signVerifiedAt; + + private Major major; + + public void setMember(Member member) { + this.member = member; + } } diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java index 4687dbc..84d3ecd 100644 --- a/src/main/java/com/assu/server/domain/common/entity/Member.java +++ b/src/main/java/com/assu/server/domain/common/entity/Member.java @@ -41,5 +41,56 @@ public class Member { private Partner partnerProfile; // 편의 메서드 및 Builder 등 생략 + + public void setPhoneNum(String phoneNum) { + this.phoneNum = phoneNum; + } + + public void setIsPhoneVerified(Boolean isPhoneVerified) { + this.isPhoneVerified = isPhoneVerified; + } + + public void setPhoneVerifiedAt(LocalDateTime phoneVerifiedAt) { + this.phoneVerifiedAt = phoneVerifiedAt; + } + + public void setRole(UserRole role) { + this.role = role; + } + + public void setIsActivated(ActivationStatus isActivated) { + this.isActivated = isActivated; + } + + + // 하드코딩시에만 사용 -> 원격에 올리기 전 주석 처리 + public void setId(Long id){ + this.id = id; + } + + // 연관관계 편의 메서드 + + public void setStudentProfile(Student studentProfile) { + this.studentProfile = studentProfile; + if (studentProfile.getMember() != this) { + studentProfile.setMember(this); + } + } + + public void setAdminProfile(Admin adminProfile) { + this.adminProfile = adminProfile; + if (adminProfile.getMember() != this) { + adminProfile.setMember(this); + } + } + + public void setPartnerProfile(Partner partnerProfile) { + this.partnerProfile = partnerProfile; + if (partnerProfile.getMember() != this) { + partnerProfile.setMember(this); + } + } + + } diff --git a/src/main/java/com/assu/server/domain/partner/entity/Partner.java b/src/main/java/com/assu/server/domain/partner/entity/Partner.java index ddeec3f..8fadbc8 100644 --- a/src/main/java/com/assu/server/domain/partner/entity/Partner.java +++ b/src/main/java/com/assu/server/domain/partner/entity/Partner.java @@ -36,4 +36,8 @@ public class Partner { private Boolean isLicenseVerified; private LocalDateTime licenseVerifiedAt; + + public void setMember(Member member) { + this.member = member; + } } diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java index 89544b0..48f793f 100644 --- a/src/main/java/com/assu/server/domain/user/entity/Student.java +++ b/src/main/java/com/assu/server/domain/user/entity/Student.java @@ -33,4 +33,8 @@ public class Student { @Enumerated(EnumType.STRING) private Major major; + + public void setMember(Member member) { + this.member = member; + } } From 291f01b2b330e2af4f0e439586566be980c711a8 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 6 Aug 2025 12:41:26 +0900 Subject: [PATCH 027/270] =?UTF-8?q?[FEAT/#15]=20(=EC=B5=9C=EC=A2=85)=20?= =?UTF-8?q?=EC=A0=9C=ED=9C=B4=20=EC=9A=94=EC=B2=AD=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PatnershipController.java | 4 --- .../converter/PartnershipConverter.java | 20 +++++++++++++ .../converter/PatnershipConverter.java | 4 --- .../dto/PartnershipRequestDTO.java | 13 +++++++++ ...seDTO.java => PartnershipResponseDTO.java} | 2 +- .../service/PartnershipService.java | 8 ++++++ .../service/PartnershipServiceImpl.java | 28 +++++++++++++++++++ .../service/PatnershipService.java | 4 --- .../service/PatnershipServiceImpl.java | 4 --- .../PartnershipUsageRepository.java | 9 ++++++ .../global/apiPayload/BaseResponse.java | 9 ++++++ .../apiPayload/code/status/SuccessStatus.java | 5 +++- 12 files changed, 92 insertions(+), 18 deletions(-) delete mode 100644 src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java create mode 100644 src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java create mode 100644 src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java rename src/main/java/com/assu/server/domain/partnership/dto/{PatnershipResponseDTO.java => PartnershipResponseDTO.java} (57%) create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java create mode 100644 src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java b/src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java deleted file mode 100644 index 117e59c..0000000 --- a/src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.controller; - -public class PatnershipController { -} diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java new file mode 100644 index 0000000..5920669 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -0,0 +1,20 @@ +package com.assu.server.domain.partnership.converter; + +import java.time.LocalDate; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.user.entity.PartnershipUsage; + +public class PartnershipConverter { + + public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ + return PartnershipUsage.builder() + .date(LocalDate.now()) + .place(dto.getPlaceName()) + .student(member.getStudentProfile()) + .isReviewed(false) + .partnershipContent(dto.getPartnershipContent()) + .build(); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java deleted file mode 100644 index 9f9cbc5..0000000 --- a/src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.converter; - -public class PatnershipConverter { -} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java new file mode 100644 index 0000000..6d0f9b4 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java @@ -0,0 +1,13 @@ +package com.assu.server.domain.partnership.dto; + +import lombok.Getter; + +public class PartnershipRequestDTO { + + @Getter + public static class finalRequest{ + String placeName; + String partnershipContent; + Long discount; + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java similarity index 57% rename from src/main/java/com/assu/server/domain/partnership/dto/PatnershipResponseDTO.java rename to src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java index 4c6f5e0..11b5a4f 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java @@ -1,4 +1,4 @@ package com.assu.server.domain.partnership.dto; -public class PatnershipResponseDTO { +public class PartnershipResponseDTO { } 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 new file mode 100644 index 0000000..3b6a623 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java @@ -0,0 +1,8 @@ +package com.assu.server.domain.partnership.service; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; + +public interface PartnershipService { + void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member); +} 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 new file mode 100644 index 0000000..c8d25af --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java @@ -0,0 +1,28 @@ +package com.assu.server.domain.partnership.service; + +import org.springframework.stereotype.Service; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partnership.converter.PartnershipConverter; +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.user.entity.PartnershipUsage; +import com.assu.server.domain.user.repository.PartnershipUsageRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Service +@Transactional +@RequiredArgsConstructor +public class PartnershipServiceImpl implements PartnershipService { + + private final PartnershipUsageRepository partnershipUsageRepository; + + public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ + + PartnershipUsage usage = PartnershipConverter.toPartnershipUsage(dto, member); + partnershipUsageRepository.save(usage); + + + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java b/src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java deleted file mode 100644 index 1437bad..0000000 --- a/src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.service; - -public interface PatnershipService { -} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java deleted file mode 100644 index 80c89ab..0000000 --- a/src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.service; - -public class PatnershipServiceImpl { -} diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java new file mode 100644 index 0000000..d38de37 --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java @@ -0,0 +1,9 @@ +package com.assu.server.domain.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.assu.server.domain.user.entity.PartnershipUsage; + +public interface PartnershipUsageRepository extends JpaRepository { + +} diff --git a/src/main/java/com/assu/server/global/apiPayload/BaseResponse.java b/src/main/java/com/assu/server/global/apiPayload/BaseResponse.java index 9be21a3..329da61 100644 --- a/src/main/java/com/assu/server/global/apiPayload/BaseResponse.java +++ b/src/main/java/com/assu/server/global/apiPayload/BaseResponse.java @@ -43,4 +43,13 @@ public static BaseResponse onFailure(BaseErrorCode code, T result) { public static BaseResponse onFailure(String code, String message, T data) { return new BaseResponse<>(false, code, message, data); } + + public static BaseResponse onSuccessWithoutData(BaseCode code) { + return new BaseResponse<>( + true, + code.getReasonHttpStatus().getCode(), + code.getReasonHttpStatus().getMessage(), + null + ); + } } diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java index 38fb93d..4318b3a 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java @@ -4,6 +4,8 @@ import com.assu.server.global.apiPayload.code.ReasonDTO; import lombok.AllArgsConstructor; import lombok.Getter; + +import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; import org.springframework.http.HttpStatus; @Getter @@ -98,7 +100,8 @@ public enum SuccessStatus implements BaseCode { REPORT_ADMIN_PROCESSED(HttpStatus.OK,"REPORT_204","신고가 성공적으로 처리되었습니다."), // 제휴 성공 - PAPER_HISTORY_SUCCESS(HttpStatus.OK, "PAPER201", "가게 별 제휴 내용이 성공적으로 조회되었습니다."), + PAPER_STORE_HISTORY_SUCCESS(HttpStatus.OK, "PAPER201", "가게 별 제휴 내용이 성공적으로 조회되었습니다."), + USER_PAPER_REQUEST_SUCCESS(HttpStatus.OK, "PAPER202", "제휴 요청이 성공적으로 처리되었습니다."), // 그룹 인증 From 1cdc8efbf2ad1a0043e8138012bec6e324d21d17 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 6 Aug 2025 12:41:46 +0900 Subject: [PATCH 028/270] =?UTF-8?q?[FEAT/#15]=20(=EC=B5=9C=EC=A2=85)=20?= =?UTF-8?q?=EC=A0=9C=ED=9C=B4=20=EC=9A=94=EC=B2=AD=20api=20-=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PartnershipController.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java 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 new file mode 100644 index 0000000..9e97c39 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java @@ -0,0 +1,41 @@ +package com.assu.server.domain.partnership.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partnership.dto.PaperResponseDTO; +import com.assu.server.domain.partnership.service.PartnershipService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.util.PrincipalDetails; +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "제휴 요청 api", description = "최종적으로 @@ 제휴를 요청할때 사용하는 api ") +@RequiredArgsConstructor +public class PartnershipController { + + private final PartnershipService partnershipService; + + + @PostMapping("/parntership/usage") + @Operation(summary= "유저의 인증 후 최종적으로 호출", description = "인증완료 화면 전에 바로 호출되어 유저의 제휴 내역에 데이터가 들어가게 됩니다.") + public ResponseEntity> finalPartnershipRequest( + @AuthenticationPrincipal PrincipalDetails userDetails,@RequestBody PartnershipRequestDTO.finalRequest dto + ) { + Member member = userDetails.getMember(); + + partnershipService.recordPartnershipUsage(dto, member); + + return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.USER_PAPER_REQUEST_SUCCESS)); + } + + +} From af27452676d8fdf1ca06f24eb0197535bf866785 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Thu, 7 Aug 2025 17:39:18 +0900 Subject: [PATCH 029/270] =?UTF-8?q?feat/#16-chatting=20-=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?->=20createdAt=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20-=20?= =?UTF-8?q?=EC=83=81=EB=8C=80=EB=B0=A9=EC=9D=B4=20=EB=82=98=EA=B0=84=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EC=A1=B0=ED=9A=8C=20=EC=95=88=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradlew | 0 .../domain/chat/converter/ChatConverter.java | 5 ++- .../domain/chat/entity/ChattingRoom.java | 3 +- .../server/domain/chat/entity/Message.java | 4 +- .../chat/repository/ChatRepository.java | 43 +++++++++++++------ .../chat/repository/MessageRepository.java | 9 ++-- .../domain/chat/service/ChatServiceImpl.java | 12 +++--- .../apiPayload/code/status/ErrorStatus.java | 4 +- 8 files changed, 52 insertions(+), 28 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 5ff150b..0e89da0 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -60,7 +60,7 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me .roomId(message.getChattingRoom().getId()) .senderId(message.getSender().getId()) .message(message.getMessage()) - .sentAt(message.getSendTime()) + .sentAt(message.getCreatedAt()) .build(); } @@ -75,11 +75,12 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me // } public static ChatResponseDTO.ChatHistoryResponseDTO toChatHistoryDTO( + Long roomId, List messages) { // ③ 최종 DTO 빌드 return ChatResponseDTO.ChatHistoryResponseDTO.builder() - .roomId(messages.get(0).getRoomId()) + .roomId(roomId) .messages(messages) .build(); } diff --git a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java index c209025..e88c405 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java +++ b/src/main/java/com/assu/server/domain/chat/entity/ChattingRoom.java @@ -36,8 +36,7 @@ public class ChattingRoom extends BaseEntity { @JoinColumn(name = "partner_id") private Partner partner; - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") + @OneToMany(mappedBy = "chattingRoom", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List messages; private String adminViewName; diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java index 9d84726..f45fb77 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/Message.java +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -36,8 +36,8 @@ public class Message extends BaseEntity { private String message; - private LocalDateTime sendTime; - private LocalDateTime readTime; +// private LocalDateTime sendTime; +// private LocalDateTime readTime; @Column(nullable = false) private boolean isRead = false; diff --git a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java index 5f75a89..c3270c4 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java @@ -5,24 +5,22 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; - import java.util.List; public interface ChatRepository extends JpaRepository { - @Query(""" SELECT new com.assu.server.domain.chat.dto.ChatRoomListResultDTO ( r.id, (SELECT m.message FROM Message m WHERE m.chattingRoom.id = r.id - AND m.sendTime = ( - SELECT MAX(m2.sendTime) + AND m.createdAt = ( + SELECT MAX(m2.createdAt) FROM Message m2 WHERE m2.chattingRoom.id = r.id ) ), - (SELECT MAX(m.sendTime) + (SELECT MAX(m.createdAt) FROM Message m WHERE m.chattingRoom.id = r.id ), @@ -31,12 +29,33 @@ SELECT MAX(m2.sendTime) WHERE m.chattingRoom.id = r.id AND m.receiver.id = :memberId AND m.isRead = false), - CASE WHEN r.partner.member.id = :memberId THEN r.admin.member.id ELSE r.partner.member.id END, - CASE WHEN r.partner.member.id = :memberId THEN r.admin.name ELSE r.partner.name END, - CASE WHEN r.partner.member.id = :memberId THEN r.admin.member.profileUrl ELSE r.partner.member.profileUrl END - ) - FROM ChattingRoom r - WHERE r.partner.member.id = :memberId OR r.admin.member.id = :memberId + CASE + WHEN pm.id IS NULL AND am.id = :memberId THEN NULL + WHEN am.id IS NULL AND pm.id = :memberId THEN NULL + WHEN pm.id = :memberId THEN a.id + ELSE p.id + END, + CASE + WHEN pm.id IS NULL AND am.id = :memberId THEN NULL + WHEN am.id IS NULL AND pm.id = :memberId THEN NULL + WHEN pm.id = :memberId THEN a.name + ELSE p.name + END, + CASE + WHEN pm.id IS NULL AND am.id = :memberId THEN NULL + WHEN am.id IS NULL AND pm.id = :memberId THEN NULL + WHEN pm.id = :memberId THEN am.profileUrl + ELSE pm.profileUrl + END + ) + FROM ChattingRoom r + LEFT JOIN r.partner p + LEFT JOIN p.member pm + LEFT JOIN r.admin a + LEFT JOIN a.member am + WHERE pm.id = :memberId + OR am.id = :memberId """) - List findChattingRoomByMember(@Param("memberId") Long memberId); + List findChattingRoomsByMemberId(@Param("memberId") Long memberId); + } diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java index 31f5367..8d5a316 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; public interface MessageRepository extends JpaRepository { @Query(""" @@ -23,16 +24,16 @@ public interface MessageRepository extends JpaRepository { m.chattingRoom.id, m.id, m.message, - m.sendTime, + m.createdAt, m.isRead, - CASE WHEN m.sender.id = :memberId THEN true - ELSE false + CASE WHEN m.sender.id = :memberId THEN true + ELSE false END ) FROM Message m WHERE m.chattingRoom.id = :roomId AND (m.sender.id = :memberId OR m.receiver.id = :memberId) - ORDER BY m.sendTime ASC + ORDER BY m.createdAt ASC """) List findAllMessagesByRoomAndMemberId( @Param("roomId") Long roomId, diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index eea5495..c662852 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -20,7 +20,6 @@ import com.assu.server.global.exception.exception.DatabaseException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import lombok.extern.java.Log; import org.springframework.stereotype.Service; import java.util.List; @@ -37,9 +36,9 @@ public class ChatServiceImpl implements ChatService { @Override public List getChatRoomList() { // Long memberId = SecurityUtil.getCurrentUserId; - Long memberId = 2L; + Long memberId = 1L; - List chatRoomList = chatRepository.findChattingRoomByMember(memberId); + List chatRoomList = chatRepository.findChattingRoomsByMemberId(memberId); return ChatConverter.toChatRoomListResultDTO(chatRoomList); } @@ -105,11 +104,14 @@ public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) { @Override public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId) { // Long memberId = SecurityUtil.getCurrentUserId(); - Long memberId = 2L; + Long memberId = 1L; + + ChattingRoom room = chatRepository.findById(roomId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(roomId, memberId); - return ChatConverter.toChatHistoryDTO(allMessages); + return ChatConverter.toChatHistoryDTO(roomId, allMessages); } @Override diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index f569a0d..8bf2f46 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -27,7 +27,9 @@ public enum ErrorStatus implements BaseErrorCode { // 채팅 에러 NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."), NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."), - NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다.") + NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."), + NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."), + ; From 4006345794d2e52a85f2131a7e032ffebbd6cb18 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:34:27 +1000 Subject: [PATCH 030/270] =?UTF-8?q?[FIX/#20]=20=ED=8C=8C=EC=9D=BC=EB=AA=85?= =?UTF-8?q?=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +++ .../controller/NotificationController.java | 4 -- .../converter/NotificationConverter.java | 4 -- .../dto/NotificaitonRequestDTO.java | 4 -- .../dto/NotificaitonResponseDTO.java | 4 -- .../repository/NotificationRepository.java | 4 -- .../service/NotificationService.java | 4 -- .../service/NotificationServiceImpl.java | 4 -- .../controller/NotificationController.java | 4 ++ .../converter/NotificationConverter.java | 4 ++ .../dto/NotificationRequestDTO.java | 4 ++ .../dto/NotificationResponseDTO.java | 4 ++ .../entity/Notification.java | 2 +- .../repository/NotificationRepository.java | 4 ++ .../service/NotificationService.java | 4 ++ .../service/NotificationServiceImpl.java | 4 ++ .../server/global/config/FirebaseConfig.java | 42 +++++++++++++++++++ 17 files changed, 77 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/com/assu/server/domain/notificaiton/controller/NotificationController.java delete mode 100644 src/main/java/com/assu/server/domain/notificaiton/converter/NotificationConverter.java delete mode 100644 src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonRequestDTO.java delete mode 100644 src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonResponseDTO.java delete mode 100644 src/main/java/com/assu/server/domain/notificaiton/repository/NotificationRepository.java delete mode 100644 src/main/java/com/assu/server/domain/notificaiton/service/NotificationService.java delete mode 100644 src/main/java/com/assu/server/domain/notificaiton/service/NotificationServiceImpl.java create mode 100644 src/main/java/com/assu/server/domain/notification/controller/NotificationController.java create mode 100644 src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java create mode 100644 src/main/java/com/assu/server/domain/notification/dto/NotificationRequestDTO.java create mode 100644 src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java rename src/main/java/com/assu/server/domain/{notificaiton => notification}/entity/Notification.java (95%) create mode 100644 src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationService.java create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java create mode 100644 src/main/java/com/assu/server/global/config/FirebaseConfig.java diff --git a/build.gradle b/build.gradle index 29f8d3b..fb47a01 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,12 @@ dependencies { // h2 db (test) runtimeOnly 'com.h2database:h2' + + // fcm + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.google.auth:google-auth-library-oauth2-http:1.18.0' + implementation 'com.google.firebase:firebase-admin:9.2.0' } tasks.named('test') { diff --git a/src/main/java/com/assu/server/domain/notificaiton/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notificaiton/controller/NotificationController.java deleted file mode 100644 index f292f8a..0000000 --- a/src/main/java/com/assu/server/domain/notificaiton/controller/NotificationController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notificaiton.controller; - -public class NotificationController { -} diff --git a/src/main/java/com/assu/server/domain/notificaiton/converter/NotificationConverter.java b/src/main/java/com/assu/server/domain/notificaiton/converter/NotificationConverter.java deleted file mode 100644 index c332734..0000000 --- a/src/main/java/com/assu/server/domain/notificaiton/converter/NotificationConverter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notificaiton.converter; - -public class NotificationConverter { -} diff --git a/src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonRequestDTO.java b/src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonRequestDTO.java deleted file mode 100644 index 8306a48..0000000 --- a/src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonRequestDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notificaiton.dto; - -public class NotificaitonRequestDTO { -} diff --git a/src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonResponseDTO.java b/src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonResponseDTO.java deleted file mode 100644 index af3579d..0000000 --- a/src/main/java/com/assu/server/domain/notificaiton/dto/NotificaitonResponseDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notificaiton.dto; - -public class NotificaitonResponseDTO { -} diff --git a/src/main/java/com/assu/server/domain/notificaiton/repository/NotificationRepository.java b/src/main/java/com/assu/server/domain/notificaiton/repository/NotificationRepository.java deleted file mode 100644 index fe4cf1a..0000000 --- a/src/main/java/com/assu/server/domain/notificaiton/repository/NotificationRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notificaiton.repository; - -public class NotificationRepository { -} diff --git a/src/main/java/com/assu/server/domain/notificaiton/service/NotificationService.java b/src/main/java/com/assu/server/domain/notificaiton/service/NotificationService.java deleted file mode 100644 index 1a00b11..0000000 --- a/src/main/java/com/assu/server/domain/notificaiton/service/NotificationService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notificaiton.service; - -public interface NotificationService { -} diff --git a/src/main/java/com/assu/server/domain/notificaiton/service/NotificationServiceImpl.java b/src/main/java/com/assu/server/domain/notificaiton/service/NotificationServiceImpl.java deleted file mode 100644 index ae73fca..0000000 --- a/src/main/java/com/assu/server/domain/notificaiton/service/NotificationServiceImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notificaiton.service; - -public class NotificationServiceImpl { -} diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java new file mode 100644 index 0000000..6d5fe77 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.notification.controller; + +public class NotificationController { +} diff --git a/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java b/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java new file mode 100644 index 0000000..f87a43a --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.notification.converter; + +public class NotificationConverter { +} diff --git a/src/main/java/com/assu/server/domain/notification/dto/NotificationRequestDTO.java b/src/main/java/com/assu/server/domain/notification/dto/NotificationRequestDTO.java new file mode 100644 index 0000000..7c21b4a --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/dto/NotificationRequestDTO.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.notification.dto; + +public class NotificationRequestDTO { +} diff --git a/src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java b/src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java new file mode 100644 index 0000000..2f30739 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.notification.dto; + +public class NotificationResponseDTO { +} diff --git a/src/main/java/com/assu/server/domain/notificaiton/entity/Notification.java b/src/main/java/com/assu/server/domain/notification/entity/Notification.java similarity index 95% rename from src/main/java/com/assu/server/domain/notificaiton/entity/Notification.java rename to src/main/java/com/assu/server/domain/notification/entity/Notification.java index cded40c..5ac04a3 100644 --- a/src/main/java/com/assu/server/domain/notificaiton/entity/Notification.java +++ b/src/main/java/com/assu/server/domain/notification/entity/Notification.java @@ -1,4 +1,4 @@ -package com.assu.server.domain.notificaiton.entity; +package com.assu.server.domain.notification.entity; import com.assu.server.domain.certification.entity.QRCertification; import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.partner.entity.Partner; diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java new file mode 100644 index 0000000..592687f --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.notification.repository; + +public class NotificationRepository { +} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationService.java new file mode 100644 index 0000000..a3fe03b --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationService.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.notification.service; + +public interface NotificationService { +} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java new file mode 100644 index 0000000..8d68840 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java @@ -0,0 +1,4 @@ +package com.assu.server.domain.notification.service; + +public class NotificationServiceImpl { +} diff --git a/src/main/java/com/assu/server/global/config/FirebaseConfig.java b/src/main/java/com/assu/server/global/config/FirebaseConfig.java new file mode 100644 index 0000000..fc2e0a7 --- /dev/null +++ b/src/main/java/com/assu/server/global/config/FirebaseConfig.java @@ -0,0 +1,42 @@ +package com.assu.server.global.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import java.io.InputStream; + +@Configuration +public class FirebaseConfig { + + @Value("${firebase.project-id}") + private String projectId; + + @Value("${firebase.credentials.path}") + private Resource serviceAccount; + + @Bean + public FirebaseApp firebaseApp() throws Exception { + if (!FirebaseApp.getApps().isEmpty()) { + return FirebaseApp.getInstance(); + } + + try (InputStream is = serviceAccount.getInputStream()) { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(is)) + .setProjectId(projectId) + .build(); + return FirebaseApp.initializeApp(options); + } + } + + @Bean + public FirebaseMessaging firebaseMessaging(FirebaseApp app) { + return FirebaseMessaging.getInstance(app); + } +} From 7f06da2658d84296369d729d52bcd1cd9164963f Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:50:14 +1000 Subject: [PATCH 031/270] =?UTF-8?q?[MOD/#20]=20fcm=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++++- src/main/resources/application.yml | 7 +++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9503f1d..8c0869d 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ out/ ### Secret ### src/main/resources/application-secret.yml src/test/resources/application-test.yml -src/test/resources/application-secret.yml \ No newline at end of file +src/test/resources/application-secret.yml + +### Firebase ### +src/main/resources/firebase/ \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 692f20a..68e093c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,12 +1,11 @@ spring: - profiles: - active: local # 여기에 local, blue, green 셋중 하나로 입력 config: import: - - classpath:application-secret.yml + - optional:classpath:application-secret.yml + - optional:file:/app/config/application-secret.yml jpa: hibernate: - ddl-auto: update # 여기 + ddl-auto: update properties: hibernate: jdbc: From a06875fa6209bc1b82658a6ba3e5a5045daeeefe Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sun, 10 Aug 2025 14:13:50 +0900 Subject: [PATCH 032/270] =?UTF-8?q?[FEAT/#15]=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EC=97=90=EA=B2=8C=20=ED=95=B4=EB=8B=B9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B0=80=EA=B2=8C=20=EB=B3=84=20=EC=A0=9C=ED=9C=B4=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PaperController.java | 48 +++++++++ .../converter/PartnershipConverter.java | 88 +++++++++++++++++ ...stDTO.java => PaperContentRequestDTO.java} | 2 +- .../dto/PaperContentResponseDTO.java | 22 +++++ .../partnership/dto/PaperResponseDTO.java | 19 ++++ .../dto/PartnershipRequestDTO.java | 1 + .../domain/partnership/entity/Goods.java | 32 ++++++ .../domain/partnership/entity/Paper.java | 4 +- .../partnership/entity/PaperContent.java | 23 +++-- ...perContentType.java => CriterionType.java} | 4 +- .../partnership/entity/enums/OptionType.java | 5 + .../repository/PaperContentRepository.java | 11 +++ .../repository/PaperRepository.java | 22 +++++ .../service/PaperContentService.java | 9 ++ .../service/PaperContentServiceImpl.java | 12 +++ .../service/PaperQueryService.java | 8 ++ .../service/PaperQueryServiceImpl.java | 97 +++++++++++++++++++ .../store/repository/StoreRepository.java | 12 ++- 18 files changed, 406 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/partnership/controller/PaperController.java rename src/main/java/com/assu/server/domain/partnership/dto/{PatnershipRequestDTO.java => PaperContentRequestDTO.java} (57%) create mode 100644 src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/partnership/dto/PaperResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/Goods.java rename src/main/java/com/assu/server/domain/partnership/entity/enums/{PaperContentType.java => CriterionType.java} (51%) create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PaperContentService.java create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PaperContentServiceImpl.java create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PaperQueryService.java create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java new file mode 100644 index 0000000..639b029 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java @@ -0,0 +1,48 @@ +package com.assu.server.domain.partnership.controller; + +import java.security.Principal; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import org.springframework.web.bind.annotation.RestController; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partnership.dto.PaperResponseDTO; +import com.assu.server.domain.partnership.service.PaperQueryService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.util.PrincipalDetails; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "제휴 관련 내용 '조회' api", description = "상세 설명") +@RequiredArgsConstructor +public class PaperController { + + private final PaperQueryService paperQueryService; + + @GetMapping("/store/{storeId}/papers") + @Operation(summary = "유저에게 적용 가능한 제휴 컨텐츠 조회", description = "유저가 속한 단과대, 학부 admin_id과 store_id 를 가진 제휴 컨텐츠 제공") + @Parameters({ + @Parameter(name = "storeId", description = "QR에서 추출한 storeId를 입력해주세요") + }) + public ResponseEntity> getStorePaperContent(@PathVariable Long storeId, + @AuthenticationPrincipal PrincipalDetails userDetails + ) { + Member member = userDetails.getMember(); + + PaperResponseDTO.partnershipContent result = paperQueryService.getStorePaperContent(storeId, member); + + return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PAPER_STORE_HISTORY_SUCCESS, result)); + } + +} diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java index 5920669..e6af0b1 100644 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -1,9 +1,15 @@ package com.assu.server.domain.partnership.converter; import java.time.LocalDate; +import java.util.List; import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partnership.dto.PaperContentResponseDTO; import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.partnership.entity.Goods; +import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; import com.assu.server.domain.user.entity.PartnershipUsage; public class PartnershipConverter { @@ -17,4 +23,86 @@ public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalReq .partnershipContent(dto.getPartnershipContent()) .build(); } + + + + public static List toContentResponseList(List contents) { + return contents.stream() + .map(PartnershipConverter::toContentResponse) + .toList(); + } + + public static PaperContentResponseDTO.storePaperContentResponse toContentResponse(PaperContent content) { + List goodsList = extractGoods(content); + Integer peopleValue = extractPeople(content); + String paperContentText = buildPaperContentText(content, goodsList, peopleValue); + + return PaperContentResponseDTO.storePaperContentResponse.builder() + .adminName(content.getPaper().getAdmin().getName()) + .paperContent(paperContentText) + .contentId(content.getId()) + .goods(goodsList) + .people(peopleValue) + .build(); + } + + private static List extractGoods(PaperContent content) { + if (content.getOptionType() == OptionType.SERVICE && content.getCategory() != null) { + return content.getGoods().stream() + .map(Goods::getBelonging) + .toList(); + } + return null; + } + + private static Integer extractPeople(PaperContent content) { + if (content.getCriterionType() == CriterionType.HEADCOUNT) { + return content.getPeople(); + } + return null; + } + + private static String buildPaperContentText(PaperContent content, List goodsList, Integer peopleValue) { + String result = ""; + + boolean isGoodsSingle = goodsList != null && goodsList.size() == 1; + boolean isGoodsMultiple = goodsList != null && goodsList.size() > 1; + + // 1. HEADCOUNT + SERVICE + 여러 개 goods + if (content.getCriterionType() == CriterionType.HEADCOUNT && + content.getOptionType() == OptionType.SERVICE && + isGoodsMultiple) { + result = peopleValue + "명 이상 식사 시 " + content.getCategory() + " 제공"; + } + // 2. HEADCOUNT + SERVICE + 단일 goods + else if (content.getCriterionType() == CriterionType.HEADCOUNT && + content.getOptionType() == OptionType.SERVICE && + isGoodsSingle) { + result = peopleValue + "명 이상 식사 시 " + goodsList.get(0) + " 제공"; + } + // 3. HEADCOUNT + DISCOUNT + else if (content.getCriterionType() == CriterionType.HEADCOUNT && + content.getOptionType() == OptionType.DISCOUNT) { + result = peopleValue + "명 이상 식사 시 " + content.getDiscount() + "% 할인"; + } + // 4. PRICE + SERVICE + 여러 개 goods + else if (content.getCriterionType() == CriterionType.PRICE && + content.getOptionType() == OptionType.SERVICE && + isGoodsMultiple) { + result = content.getCost() + " 이상 주문 시 " + content.getCategory() + " 제공"; + } + // 5. PRICE + SERVICE + 단일 goods + else if (content.getCriterionType() == CriterionType.PRICE && + content.getOptionType() == OptionType.SERVICE && + isGoodsSingle) { + result = content.getCost() + " 이상 주문 시 " + goodsList.get(0) + " 제공"; + } + // 6. PRICE + DISCOUNT + else if (content.getCriterionType() == CriterionType.PRICE && + content.getOptionType() == OptionType.DISCOUNT) { + result = content.getCost() + " 이상 주문 시 " + content.getDiscount() + "% 할인"; + } + + return result; + } } diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentRequestDTO.java similarity index 57% rename from src/main/java/com/assu/server/domain/partnership/dto/PatnershipRequestDTO.java rename to src/main/java/com/assu/server/domain/partnership/dto/PaperContentRequestDTO.java index 99e4c2b..c759663 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipRequestDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentRequestDTO.java @@ -1,4 +1,4 @@ package com.assu.server.domain.partnership.dto; -public class PatnershipRequestDTO { +public class PaperContentRequestDTO { } diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java new file mode 100644 index 0000000..60d9355 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java @@ -0,0 +1,22 @@ +package com.assu.server.domain.partnership.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class PaperContentResponseDTO { + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class storePaperContentResponse{ + String adminName; + String paperContent; + Long contentId; + List goods; + Integer people; + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PaperResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PaperResponseDTO.java new file mode 100644 index 0000000..85427c9 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PaperResponseDTO.java @@ -0,0 +1,19 @@ +package com.assu.server.domain.partnership.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class PaperResponseDTO { + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class partnershipContent{ + String storeName; + Long storeId; + List contents; + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java index 6d0f9b4..8f330ba 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java @@ -8,6 +8,7 @@ public class PartnershipRequestDTO { public static class finalRequest{ String placeName; String partnershipContent; + Long contentId; Long discount; } } diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Goods.java b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java new file mode 100644 index 0000000..2618438 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java @@ -0,0 +1,32 @@ +package com.assu.server.domain.partnership.entity; + +import com.assu.server.domain.partnership.repository.PaperContentRepository; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Goods { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id") + private PaperContent content; + + private String belonging; +} diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java index faeb633..2d4cd9e 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java @@ -1,4 +1,6 @@ package com.assu.server.domain.partnership.entity; +import java.time.LocalDate; + import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.common.enums.ActivationStatus; @@ -29,7 +31,7 @@ public class Paper extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String partnershipPeriod; // 이게 뭘로 들어오는거지. 그냥 LocalDate 로 하는게 낫지 않나? + private LocalDate partnershipPeriod; // 이게 뭘로 들어오는거지. 그냥 LocalDate 로 하는게 낫지 않나? @Enumerated(EnumType.STRING) private ActivationStatus isActivated; diff --git a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java index 29e3195..7b2881b 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java @@ -1,7 +1,7 @@ package com.assu.server.domain.partnership.entity; import com.assu.server.domain.common.entity.BaseEntity; -import com.assu.server.domain.partnership.entity.enums.PaperContentType; -import com.assu.server.domain.user.entity.enums.Major; +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -12,11 +12,15 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @@ -33,19 +37,22 @@ public class PaperContent extends BaseEntity { private Paper paper; @Enumerated(EnumType.STRING) - private PaperContentType type; + private CriterionType criterionType; + + @Enumerated(EnumType.STRING) + private OptionType optionType; + + private Integer people; - private String belonging; + private String category; private Long cost; private Long discount; - private String goods; - - @Enumerated(EnumType.STRING) - private Major major; + @OneToMany(mappedBy = "content") + private List goods = new ArrayList<>(); } diff --git a/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java b/src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java similarity index 51% rename from src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java rename to src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java index 80f7023..f90adde 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java @@ -1,5 +1,5 @@ package com.assu.server.domain.partnership.entity.enums; -public enum PaperContentType{ - PEOPLE, BELONGING, COST +public enum CriterionType { + PRICE, HEADCOUNT } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java b/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java new file mode 100644 index 0000000..cd5db5f --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.partnership.entity.enums; + +public enum OptionType { + SERVICE, DISCOUNT +} diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java new file mode 100644 index 0000000..ec62e73 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java @@ -0,0 +1,11 @@ +package com.assu.server.domain.partnership.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.assu.server.domain.partnership.entity.PaperContent; + +public interface PaperContentRepository extends JpaRepository { + List findByPaperId(Long paperId); +} diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java new file mode 100644 index 0000000..6329b86 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -0,0 +1,22 @@ +package com.assu.server.domain.partnership.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partnership.entity.Paper; + +public interface PaperRepository extends JpaRepository { + + @Query("SELECT p FROM Paper p " + + "WHERE p.store.id = :storeId " + + "AND p.admin.id = :adminId " + + "AND p.isActivated = :status") + List findByStoreIdAndAdminIdAndStatus( + @Param("storeId")Long storeId, + @Param("adminId")Long adminId, + @Param("status")ActivationStatus status); +} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperContentService.java b/src/main/java/com/assu/server/domain/partnership/service/PaperContentService.java new file mode 100644 index 0000000..2c2051a --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PaperContentService.java @@ -0,0 +1,9 @@ +package com.assu.server.domain.partnership.service; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.assu.server.domain.partnership.entity.PaperContent; + +public interface PaperContentService { + +} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperContentServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PaperContentServiceImpl.java new file mode 100644 index 0000000..38720e5 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PaperContentServiceImpl.java @@ -0,0 +1,12 @@ +package com.assu.server.domain.partnership.service; + +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + + +@Service +@RequiredArgsConstructor +public class PaperContentServiceImpl implements PaperContentService { +} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryService.java b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryService.java new file mode 100644 index 0000000..ec44371 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryService.java @@ -0,0 +1,8 @@ +package com.assu.server.domain.partnership.service; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.partnership.dto.PaperResponseDTO; + +public interface PaperQueryService { + PaperResponseDTO.partnershipContent getStorePaperContent(Long storeId, Member member); +} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java new file mode 100644 index 0000000..a783b3c --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java @@ -0,0 +1,97 @@ +package com.assu.server.domain.partnership.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.service.AdminService; +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.partnership.converter.PartnershipConverter; +import com.assu.server.domain.partnership.dto.PaperContentResponseDTO; +import com.assu.server.domain.partnership.dto.PaperResponseDTO; +import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.repository.PaperContentRepository; +import com.assu.server.domain.partnership.repository.PaperRepository; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.store.repository.StoreRepository; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.entity.enums.Major; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.GeneralException; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class PaperQueryServiceImpl implements PaperQueryService { + + private final AdminService adminService; + private final PaperRepository paperRepository; + private final PaperContentRepository contentRepository; + private final StoreRepository storeRepository; + + @Override + public PaperResponseDTO.partnershipContent getStorePaperContent(Long storeId, Member member){ + + // 역할이 학생이 아닌 경우 : 이미 type별로 ui를 분기 시켜놔서 그럴일 없을 것 같긴 하지만 혹시 몰라서 처리함 + if(member.getRole() != UserRole.USER) + throw new GeneralException(ErrorStatus.NO_STUENT_TYPE); + + Student student = member.getStudentProfile(); + + // 유저의 학교, 단과대, 학부 정보를 조회하여 일치하는 admin을 찾습니다. + List adminList = adminService.findMatchingAdmins( + student.getUniversity(), + student.getDepartment(), + student.getMajor()); + + // 한번 더 거르기 위해서 + List filteredAdmin = adminList.stream() + .filter(admin -> { + String name = admin.getName(); + Major major = admin.getMajor(); + return name.contains(student.getUniversity()) + || name.contains(student.getDepartment()) + || major.equals(student.getMajor()); + }).toList(); + + + // 추출한 admin, store와 일치하는 paperId 를 추출합니다. + List paperList = filteredAdmin.stream() + .flatMap(admin -> + paperRepository.findByStoreIdAndAdminIdAndStatus(storeId, admin.getId(), ActivationStatus.ACTIVE) + .stream()).toList(); + + //paperId로 paperContent 를 조회 + List contentList = paperList.stream() + .flatMap(paper-> + contentRepository.findByPaperId(paper.getId()).stream() + ).toList(); + + + + Store store = storeRepository.findById(storeId).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) + ); + + // dto 변환 + List contents = + PartnershipConverter.toContentResponseList(contentList); + + // partnershipContent DTO 생성 + return PaperResponseDTO.partnershipContent.builder() + .storeName(store.getName()) + .storeId(storeId) + .contents(contents) + .build(); + + + } + +} diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java index 5b7f958..b1af3f6 100644 --- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java @@ -1,4 +1,14 @@ package com.assu.server.domain.store.repository; -public class StoreRepository { +import java.util.Optional; + +import org.hibernate.validator.internal.engine.resolver.JPATraversableResolver; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.assu.server.domain.store.entity.Store; + +public interface StoreRepository extends JpaRepository { + Optional findByName(String name); + Optional findById(Long id); + } From ed663cca9ff0d74c4f40914ecbc440385b73cb29 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sun, 10 Aug 2025 15:01:24 +0900 Subject: [PATCH 033/270] =?UTF-8?q?[FEAT/#15]=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EB=93=A4=EA=B9=8C=EC=A7=80=20par?= =?UTF-8?q?tnershipUsage=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CertificationSessionManager.java | 4 ++++ .../certification/dto/CurrentProgress.java | 10 ++++++++- .../service/CertificationServiceImpl.java | 21 +++++++++---------- .../converter/PartnershipConverter.java | 6 ++++-- .../dto/PartnershipRequestDTO.java | 3 +++ .../service/PartnershipServiceImpl.java | 20 ++++++++++++++++-- .../domain/user/entity/PartnershipUsage.java | 1 + .../user/repository/StudentRepository.java | 6 +++++- 8 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java index c1332b8..624d4e0 100644 --- a/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java +++ b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java @@ -1,5 +1,6 @@ package com.assu.server.domain.certification.component; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -25,6 +26,9 @@ public int getCurrentUserCount(Long sessionId) { public boolean hasUser(Long sessionId, Long userId) { return sessionUserMap.getOrDefault(sessionId, Set.of()).contains(userId); } + public List snapshotUserIds(Long sessionId) { + return List.copyOf(sessionUserMap.getOrDefault(sessionId, Set.of())); + } public void removeSession(Long sessionId) { sessionUserMap.remove(sessionId); diff --git a/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java b/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java index 2c8a35b..919e946 100644 --- a/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java +++ b/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java @@ -1,5 +1,7 @@ package com.assu.server.domain.certification.dto; +import java.util.List; + import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -10,18 +12,24 @@ public class CurrentProgress { private int count; + @Getter public static class CertificationNumber{ public CertificationNumber(int count){ + this.count= count; } int count; } + @Getter public static class CompletedNotification{ - public CompletedNotification(String message){ + public CompletedNotification(String message, List userIds){ + this.message= message; + this.userIds= userIds; } String message; + List userIds; } } \ No newline at end of file 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 bb7b44f..fe5e2a3 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 @@ -39,7 +39,6 @@ public class CertificationServiceImpl implements CertificationService { private final AdminRepository adminRepository; private final StoreRepository storeRepository; private final AssociateCertificationRepository associateCertificationRepository; - private final QRCertificationRepository qrRepository; // 세션 메니저 private final CertificationSessionManager sessionManager; @@ -127,19 +126,19 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, associateCertificationRepository.save(session); - messagingTemplate.convertAndSend("certification/progress"+sessionId, - new CurrentProgress.CompletedNotification("인증이 완료되었습니다.") + messagingTemplate.convertAndSend("/certification/progress"+sessionId, + new CurrentProgress.CompletedNotification("인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId)) ); } - // 인증 정보를 QRCertification 에 삽입 - QRCertification qrCertification = new QRCertification(); - qrCertification.builder() - .certification(session) - .verifiedTime(LocalDateTime.now()) - .isVerified(true) - .build(); - qrRepository.save(qrCertification); + // // 인증 정보를 QRCertification 에 삽입 + // QRCertification qrCertification = new QRCertification(); + // qrCertification.builder() + // .certification(session) + // .verifiedTime(LocalDateTime.now()) + // .isVerified(true) + // .build(); + // qrRepository.save(qrCertification); } diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java index e6af0b1..73caacb 100644 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -11,15 +11,17 @@ import com.assu.server.domain.partnership.entity.enums.CriterionType; import com.assu.server.domain.partnership.entity.enums.OptionType; import com.assu.server.domain.user.entity.PartnershipUsage; +import com.assu.server.domain.user.entity.Student; public class PartnershipConverter { - public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ + public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Student student) { return PartnershipUsage.builder() .date(LocalDate.now()) .place(dto.getPlaceName()) - .student(member.getStudentProfile()) + .student(student) .isReviewed(false) + .contentId(dto.getContentId()) .partnershipContent(dto.getPartnershipContent()) .build(); } diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java index 8f330ba..c3a922b 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java @@ -1,5 +1,7 @@ package com.assu.server.domain.partnership.dto; +import java.util.List; + import lombok.Getter; public class PartnershipRequestDTO { @@ -10,5 +12,6 @@ public static class finalRequest{ String partnershipContent; Long contentId; Long discount; + List userIds; } } 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 c8d25af..9922d6c 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 @@ -1,12 +1,17 @@ package com.assu.server.domain.partnership.service; +import java.util.ArrayList; +import java.util.List; + import org.springframework.stereotype.Service; import com.assu.server.domain.common.entity.Member; import com.assu.server.domain.partnership.converter.PartnershipConverter; import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; import com.assu.server.domain.user.entity.PartnershipUsage; +import com.assu.server.domain.user.entity.Student; import com.assu.server.domain.user.repository.PartnershipUsageRepository; +import com.assu.server.domain.user.repository.StudentRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -17,12 +22,23 @@ public class PartnershipServiceImpl implements PartnershipService { private final PartnershipUsageRepository partnershipUsageRepository; + private final StudentRepository studentRepository; public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ - PartnershipUsage usage = PartnershipConverter.toPartnershipUsage(dto, member); - partnershipUsageRepository.save(usage); + List usages = new ArrayList<>(); + + // 1) 요청한 member 본인 + usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile())); + + // 2) dto의 userIds에 있는 다른 사용자들 + for (Long userId : dto.getUserIds()) { + Student student = studentRepository.getReferenceById(userId); + usages.add(PartnershipConverter.toPartnershipUsage(dto, student)); + } + + partnershipUsageRepository.saveAll(usages); } } diff --git a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java index 85959cf..da21c38 100644 --- a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java +++ b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java @@ -35,6 +35,7 @@ public class PartnershipUsage extends BaseEntity { private String partnershipContent; private Boolean isReviewed; private Integer discount; + private Long contentId; } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java index e042199..3762339 100644 --- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java @@ -1,4 +1,8 @@ package com.assu.server.domain.user.repository; -public class StudentRepository { +import org.springframework.data.jpa.repository.JpaRepository; + +import com.assu.server.domain.user.entity.Student; + +public interface StudentRepository extends JpaRepository { } From ce83ff7dfcccbc69b3ea915a4f1d3007c3228fbe Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sun, 10 Aug 2025 15:04:35 +0900 Subject: [PATCH 034/270] =?UTF-8?q?[FEAT/#15]=20NPE=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/partnership/service/PartnershipServiceImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 9922d6c..5754333 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 @@ -1,7 +1,9 @@ package com.assu.server.domain.partnership.service; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; import org.springframework.stereotype.Service; @@ -32,8 +34,9 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe // 1) 요청한 member 본인 usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile())); + List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList()); // 2) dto의 userIds에 있는 다른 사용자들 - for (Long userId : dto.getUserIds()) { + for (Long userId : userIds) { Student student = studentRepository.getReferenceById(userId); usages.add(PartnershipConverter.toPartnershipUsage(dto, student)); } From d60a6e68455cea7896c562205464373c96767ea7 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Tue, 12 Aug 2025 00:22:04 +0900 Subject: [PATCH 035/270] =?UTF-8?q?feat/#13-review=20=20-=20=EB=82=B4=20?= =?UTF-8?q?=EA=B0=80=EA=B2=8C=20=EB=A6=AC=EB=B7=B0=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 12 ++++++++++-- .../review/converter/ReviewConverter.java | 16 ++++++++++++++++ .../domain/review/dto/ReviewResponseDTO.java | 17 +++++++++++++++++ .../review/repository/ReviewRepository.java | 3 +++ .../domain/review/service/ReviewService.java | 1 + .../review/service/ReviewServiceImpl.java | 17 +++++++++++++++++ .../assu/server/domain/store/entity/Store.java | 1 - .../store/repository/StoreRepository.java | 6 +++++- 8 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java index 8eb5259..76a708a 100644 --- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java +++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java @@ -13,7 +13,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/review") +@RequestMapping("/reviews") public class ReviewController { private final ReviewService reviewService; @Operation( @@ -29,7 +29,7 @@ public BaseResponse writeReview(@Reque summary = "내가 쓴 리뷰 조회 API입니다.", description = "Autorization 후에 사용해주세요." ) - @GetMapping("/{studentId}") + @GetMapping("/student") public BaseResponse> checkStudent() { return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview()); } @@ -43,4 +43,12 @@ public BaseResponse deleteReview(@Pat return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.deleteReview(reviewId)); } + @Operation( + summary = "내 가게 리뷰 조회 API입니다.", + description = "내 가게 ID를 입력해주세요." + ) + @GetMapping("/partner") + public BaseResponse> checkPartnerReview(){ + return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview()); + } } diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java index 6181650..74117f0 100644 --- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java @@ -47,6 +47,22 @@ public static List checkStudent .map(ReviewConverter::checkStudentReviewResultDTO) .collect(Collectors.toList()); } + public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReviewResultDTO(Review review){ + return ReviewResponseDTO.CheckPartnerReviewResponseDTO.builder() + .reviewId(review.getId()) + .storeId(review.getStore().getId()) + .reviewerId(review.getStudent().getId()) + .content(review.getContent()) + .rate(review.getRate()) + .createdAt(review.getCreatedAt()) + .build(); + + } + public static List checkPartnerReviewResultDTO(List reviews){ + return reviews.stream() + .map(ReviewConverter::checkPartnerReviewResultDTO) + .collect(Collectors.toList()); + } public static ReviewResponseDTO.DeleteReviewResponseDTO deleteReviewResultDTO(Long reviewId){ return ReviewResponseDTO.DeleteReviewResponseDTO.builder() .reviewId(reviewId) diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java index 58b7c04..6118065 100644 --- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java +++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.List; public class ReviewResponseDTO { @Getter @@ -31,6 +32,22 @@ public static class CheckStudentReviewResponseDTO { //내가 작성한 리뷰 private LocalDateTime createdAt; //private List reviewImage; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CheckPartnerReviewResponseDTO {//partner의 리뷰 확인 + private Long reviewId; + private Long storeId; //현재 파트너의 가게 아이디 + private Long reviewerId; + private String content; + private Integer rate; + private LocalDateTime createdAt; + private String sortBy; // 정렬기준 -> 최신, 별점, 오래된 순 + //private List reviewImage; + } + + @Getter @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java index 365e2d6..bfc1ecd 100644 --- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java @@ -15,4 +15,7 @@ public interface ReviewRepository extends JpaRepository { ORDER BY r.createdAt DESC """) List findByMemberId(@Param("memberId") Long memberId); + List findByStoreId(Long storeId); + + List findByStoreIdOrderByCreatedAtDesc(Long id);//최신순 정렬 } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java index d15252d..006fbdc 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java @@ -10,5 +10,6 @@ public interface ReviewService { ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request); List checkStudentReview(); + List checkPartnerReview(); ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(@PathVariable Long reviewId); } diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index f3c9f49..8a7866a 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -18,6 +18,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.sql.SQLOutput; import java.util.List; @Service @@ -60,6 +61,22 @@ public List checkStudentReview( return ReviewConverter.checkStudentReviewResultDTO(reviews); } + @Override + @Transactional + public List checkPartnerReview() { + //Long memberId = SecurityUtil.getCurrentUserId; + Long partnerId = 2L; //ID 하드코딩 한 것 + + Partner partner = partnerRepository.findById(partnerId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + System.out.println("파트너 id는 "+partner.getId()); + Store store = storeRepository.findByPartner(partner) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); + + //List reviews = reviewRepository.findByStoreId(store.getId()); + List reviews = reviewRepository.findByStoreIdOrderByCreatedAtDesc(store.getId()); //최신순 정렬 + return ReviewConverter.checkPartnerReviewResultDTO(reviews); + } @Override @Transactional public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) { diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java index f808887..97bc362 100644 --- a/src/main/java/com/assu/server/domain/store/entity/Store.java +++ b/src/main/java/com/assu/server/domain/store/entity/Store.java @@ -33,7 +33,6 @@ public class Store extends BaseEntity { private Partner partner; private Integer rate; - @Enumerated(EnumType.STRING) private ActivationStatus isActivate; diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java index ca95b1c..fc7b590 100644 --- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java @@ -1,8 +1,12 @@ package com.assu.server.domain.store.repository; +import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.store.entity.Store; import org.springframework.data.jpa.repository.JpaRepository; -public interface StoreRepository extends JpaRepository { +import java.util.List; +import java.util.Optional; +public interface StoreRepository extends JpaRepository { + Optional findByPartner(Partner partner); } From 0a561ff0610f1c4808c486f0339fd1b2a5f934e9 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Tue, 12 Aug 2025 15:42:58 +0900 Subject: [PATCH 036/270] =?UTF-8?q?[FEAT/#15]=20stamp=EC=A6=9D=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/partnership/service/PartnershipServiceImpl.java | 1 + .../java/com/assu/server/domain/user/entity/Student.java | 7 +++++++ 2 files changed, 8 insertions(+) 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 5754333..fe27da6 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 @@ -39,6 +39,7 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe for (Long userId : userIds) { Student student = studentRepository.getReferenceById(userId); usages.add(PartnershipConverter.toPartnershipUsage(dto, student)); + student.setStamp(); } partnershipUsageRepository.saveAll(usages); diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java index 48f793f..8faf1f3 100644 --- a/src/main/java/com/assu/server/domain/user/entity/Student.java +++ b/src/main/java/com/assu/server/domain/user/entity/Student.java @@ -37,4 +37,11 @@ public class Student { public void setMember(Member member) { this.member = member; } + + public void setStamp() { + if(this.stamp ==10) + this.stamp=1; + else + this.stamp++; + } } From 76725dca9c2c17fa3ad090cbdf726731ee8982f9 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Wed, 13 Aug 2025 00:05:42 +0900 Subject: [PATCH 037/270] =?UTF-8?q?feat/#13-review=20=20-=20=EB=82=B4=20?= =?UTF-8?q?=EC=8A=A4=ED=83=AC=ED=94=84=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 4 +-- .../review/service/ReviewServiceImpl.java | 2 +- .../user/controller/StudentController.java | 23 ++++++++++++- .../user/converter/StudentConverter.java | 10 ++++++ .../domain/user/dto/StudentRequestDTO.java | 4 +++ .../domain/user/dto/StudentResponseDTO.java | 33 +++++++++++++++++++ .../server/domain/user/entity/Student.java | 6 ++++ .../domain/user/service/StudentService.java | 3 ++ .../user/service/StudentServiceImpl.java | 25 +++++++++++++- 9 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java index 76a708a..599016f 100644 --- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java +++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java @@ -27,7 +27,7 @@ public BaseResponse writeReview(@Reque @Operation( summary = "내가 쓴 리뷰 조회 API입니다.", - description = "Autorization 후에 사용해주세요." + description = "Authorization 후에 사용해주세요." ) @GetMapping("/student") public BaseResponse> checkStudent() { @@ -45,7 +45,7 @@ public BaseResponse deleteReview(@Pat @Operation( summary = "내 가게 리뷰 조회 API입니다.", - description = "내 가게 ID를 입력해주세요." + description = "Authorization 후에 사용해주세요." ) @GetMapping("/partner") public BaseResponse> checkPartnerReview(){ diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index 8a7866a..ad7aeb0 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -64,7 +64,7 @@ public List checkStudentReview( @Override @Transactional public List checkPartnerReview() { - //Long memberId = SecurityUtil.getCurrentUserId; + //Long partnerId = SecurityUtil.getCurrentUserId; Long partnerId = 2L; //ID 하드코딩 한 것 Partner partner = partnerRepository.findById(partnerId) diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java index 303f234..d060c35 100644 --- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java +++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java @@ -1,4 +1,25 @@ package com.assu.server.domain.user.controller; +import com.assu.server.domain.user.dto.StudentResponseDTO; +import com.assu.server.domain.user.service.StudentService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/user") public class StudentController { -} + private final StudentService studentService; + + @Operation( + summary = "스탬프 조회", + description = "Authorization 후에 사용해주세요." + ) + @GetMapping("/stamp") + public BaseResponse getStamp() { + return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getStamp()); + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/user/converter/StudentConverter.java b/src/main/java/com/assu/server/domain/user/converter/StudentConverter.java index 8a937ab..90f34ed 100644 --- a/src/main/java/com/assu/server/domain/user/converter/StudentConverter.java +++ b/src/main/java/com/assu/server/domain/user/converter/StudentConverter.java @@ -1,4 +1,14 @@ package com.assu.server.domain.user.converter; +import com.assu.server.domain.user.dto.StudentResponseDTO; +import com.assu.server.domain.user.entity.Student; + public class StudentConverter { + public static StudentResponseDTO.CheckStampResponseDTO checkStampResponseDTO(Student student, String message) { + return StudentResponseDTO.CheckStampResponseDTO.builder() + .userId(student.getId()) + .stamp(student.getStamp()) + .message(message) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentRequestDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentRequestDTO.java index 7507793..2c9043a 100644 --- a/src/main/java/com/assu/server/domain/user/dto/StudentRequestDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentRequestDTO.java @@ -1,4 +1,8 @@ package com.assu.server.domain.user.dto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + public class StudentRequestDTO { } diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java index 1ebaae2..acd2718 100644 --- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java @@ -1,4 +1,37 @@ package com.assu.server.domain.user.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + public class StudentResponseDTO { + /* @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CheckPartnershipUsageResponseDTO { + private Long id; + private String place; + private LocalDate date; + private String partnershipContent; + private Boolean isReviewed; //리뷰 작성하기 버튼 활성화 ? + private Integer discount; //가격? 비율 + private LocalDateTime createdAt; + } + */ + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CheckStampResponseDTO { + private Long userId; + private int stamp; + private String message; + } + } diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java index 89544b0..8013ac1 100644 --- a/src/main/java/com/assu/server/domain/user/entity/Student.java +++ b/src/main/java/com/assu/server/domain/user/entity/Student.java @@ -1,11 +1,15 @@ package com.assu.server.domain.user.entity; +import com.assu.server.domain.common.entity.AdminUser; import com.assu.server.domain.common.entity.Member; import com.assu.server.domain.user.entity.enums.EnrollmentStatus; import com.assu.server.domain.user.entity.enums.Major; import jakarta.persistence.*; import lombok.*; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -33,4 +37,6 @@ public class Student { @Enumerated(EnumType.STRING) private Major major; + //@OneToMany(mappedBy = "user") + //private List adminUsers = new ArrayList<>(); } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java index 84c57a1..514ffac 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentService.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java @@ -1,4 +1,7 @@ package com.assu.server.domain.user.service; +import com.assu.server.domain.user.dto.StudentResponseDTO; + public interface StudentService { + StudentResponseDTO.CheckStampResponseDTO getStamp();//조회 } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java index 61a060f..7b08755 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java @@ -1,4 +1,27 @@ package com.assu.server.domain.user.service; -public class StudentServiceImpl { +import com.assu.server.domain.user.converter.StudentConverter; +import com.assu.server.domain.user.dto.StudentResponseDTO; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class StudentServiceImpl implements StudentService { + private final StudentRepository studentRepository; + @Override + @Transactional + public StudentResponseDTO.CheckStampResponseDTO getStamp() { + //Long studentId = SecurityUtil.getCurrentUserId; + Long studentId = 1L; + Student student = studentRepository.findById(Math.toIntExact(studentId)) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT)); + + return StudentConverter.checkStampResponseDTO(student, "스탬프 조회 성공"); + } } From 587b9d9c5bd12827b08d7d1fed911722608ac478 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 13 Aug 2025 11:44:10 +0900 Subject: [PATCH 038/270] =?UTF-8?q?[FEAT/#15]=20=EA=B0=9C=EC=9D=B8?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CertificationController.java | 15 +++++++++++++++ .../converter/CertificationConverter.java | 11 +++++++++++ .../dto/CertificationRequestDTO.java | 7 +++++++ .../service/CertificationService.java | 2 ++ .../service/CertificationServiceImpl.java | 12 ++++++++++++ .../controller/PartnershipController.java | 2 +- .../apiPayload/code/status/SuccessStatus.java | 6 +++++- 7 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java index 5a147d4..71c8028 100644 --- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java +++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java @@ -22,6 +22,7 @@ import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import com.assu.server.global.util.PrincipalDetails; +import com.fasterxml.jackson.databind.ser.Serializers; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -81,4 +82,18 @@ public ResponseEntity certifyGroup( return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_CERTIFICATION_SUCCESS, null)); } + @PostMapping("/certification/personal") + @Operation(summary = "개인 인증 api", description = "사실 크게 필요없는데, 제휴 내역 통계를 위해 데이터를 post하는 api 입니다. " + + "가게 별 제휴를 조회하고 people값이 null 인 제휴를 선택한 경우 그룹 인증 대신 요청하는 api 입니다.") + public ResponseEntity> personalCertification( + @AuthenticationPrincipal PrincipalDetails userDetails, + @RequestBody CertificationRequestDTO.personalRequest dto + ) { + + Member member = userDetails.getMember(); + certificationService.certificatePersonal(dto, member); + + return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.PERSONAL_CERTIFICATION_SUCCESS)); + } + } diff --git a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java index 581c4aa..236c58a 100644 --- a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java +++ b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java @@ -24,4 +24,15 @@ public static CertificationResponseDTO.getSessionIdResponse toSessionIdResponse( .sessionId(sessionId).adminId(adminId) .build(); } + + public static AssociateCertification toPersonalCertification(CertificationRequestDTO.personalRequest dto, Store store, Member member) { + return AssociateCertification.builder() + .store(store) + .partner(store.getPartner()) + .isCertified(true) + .tableNumber(dto.getTableNumber()) + .peopleNumber(1) + .student(member.getStudentProfile()) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java index 8bdab6e..7e3d2fa 100644 --- a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java +++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java @@ -13,6 +13,13 @@ public static class groupRequest{ Integer tableNumber; } + @Getter + public static class personalRequest{ + String storeName; + String adminName; + Integer tableNumber; + } + @Getter public static class groupSessionRequest{ Long adminId; diff --git a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java index e9403c8..65c00af 100644 --- a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java +++ b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java @@ -9,4 +9,6 @@ public interface CertificationService { CertificationResponseDTO.getSessionIdResponse getSessionId(CertificationRequestDTO.groupRequest dto, Member member); void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member); + + void certificatePersonal(CertificationRequestDTO.personalRequest dto, Member member); } 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 fe5e2a3..7ea8a2e 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 @@ -142,6 +142,18 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, } + @Override + public void certificatePersonal(CertificationRequestDTO.personalRequest dto, Member member){ + // store id 추출 + Store store = storeRepository.findByName(dto.getStoreName()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) + ); + + AssociateCertification personalCertificationData = CertificationConverter.toPersonalCertification(dto, store, member); + associateCertificationRepository.save(personalCertificationData); + + } + } 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 9e97c39..3377910 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 @@ -26,7 +26,7 @@ public class PartnershipController { @PostMapping("/parntership/usage") - @Operation(summary= "유저의 인증 후 최종적으로 호출", description = "인증완료 화면 전에 바로 호출되어 유저의 제휴 내역에 데이터가 들어가게 됩니다.") + @Operation(summary= "유저의 인증 후 최종적으로 호출", description = "인증완료 화면 전에 바로 호출되어 유저의 제휴 내역에 데이터가 들어가게 됩니다. (개인 인증인 경우도 포함됩니다.)") public ResponseEntity> finalPartnershipRequest( @AuthenticationPrincipal PrincipalDetails userDetails,@RequestBody PartnershipRequestDTO.finalRequest dto ) { diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java index 4318b3a..46a3521 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java @@ -106,7 +106,11 @@ public enum SuccessStatus implements BaseCode { // 그룹 인증 GROUP_SESSION_CREATE(HttpStatus.OK, "GROUP201", "인증 세션 생성 및 대표자 구독이 완료되었습니다."), - GROUP_CERTIFICATION_SUCCESS(HttpStatus.OK, "GROUP202", "그룹 인증 세션에 대한 인증이 완료되었습니다.") + GROUP_CERTIFICATION_SUCCESS(HttpStatus.OK, "GROUP202", "그룹 인증 세션에 대한 인증이 완료되었습니다."), + + + // 개인 인증 + PERSONAL_CERTIFICATION_SUCCESS(HttpStatus.OK, "PERSONAL201", "개인 인증이 완료 되었습니다.") ; private final HttpStatus httpStatus; From 3e54b4e874cd41488c7c9c82214f6a9a61235ec0 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:57:16 +1000 Subject: [PATCH 039/270] =?UTF-8?q?[FEAT/#20]=20DeviceToken=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=09-=20deviceToken=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=B6=94=EA=B0=80=20=09-=20deviceToken=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D/=EC=82=AD=EC=A0=9C=20API=20=EC=B6=94=EA=B0=80=20=09-?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API,=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20API=20=EA=B5=AC=ED=98=84=20=09-=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=09-=20FCM=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=EC=9D=84=20=EC=9C=84=ED=95=9C=20Factory=20=EB=B0=8F?= =?UTF-8?q?=20Dispatcher=20=EA=B5=AC=ED=98=84=20=09-=20=EB=8B=A4=EC=96=91?= =?UTF-8?q?=ED=95=9C=20=EC=95=8C=EB=A6=BC=20=ED=83=80=EC=9E=85=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20NotificationManager?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/common/entity/Member.java | 2 +- .../controller/DeviceTokenController.java | 27 +++++++ .../deviceToken/entity/DeviceToken.java | 27 +++++++ .../repository/DeviceTokenRepository.java | 16 ++++ .../service/DeviceTokenService.java | 8 ++ .../service/DeviceTokenServiceImpl.java | 32 ++++++++ .../controller/NotificationController.java | 35 +++++++++ .../converter/NotificationConverter.java | 40 ++++++++++ .../dto/NotificationResponseDTO.java | 21 ++++++ .../notification/entity/Notification.java | 58 ++++++++++----- .../entity/NotificationOutbox.java | 34 +++++++++ .../notification/entity/NotificationType.java | 21 ++++++ .../NotificationOutboxRepository.java | 10 +++ .../repository/NotificationRepository.java | 19 ++++- .../service/NotificationCommandService.java | 12 +++ .../NotificationCommandServiceImpl.java | 46 ++++++++++++ .../service/NotificationDispatcher.java | 37 ++++++++++ .../service/NotificationQueryService.java | 9 +++ .../service/NotificationQueryServiceImpl.java | 33 +++++++++ .../service/NotificationService.java | 4 - .../service/NotificationServiceImpl.java | 4 - .../user/repository/StudentRepository.java | 4 - .../java/com/assu/server/infra/FcmClient.java | 49 +++++++++++++ .../server/infra/NotificationFactory.java | 73 +++++++++++++++++++ 24 files changed, 587 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java create mode 100644 src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java create mode 100644 src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java create mode 100644 src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java create mode 100644 src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java create mode 100644 src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java create mode 100644 src/main/java/com/assu/server/domain/notification/entity/NotificationType.java create mode 100644 src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java delete mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationService.java delete mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java delete mode 100644 src/main/java/com/assu/server/domain/user/repository/StudentRepository.java create mode 100644 src/main/java/com/assu/server/infra/FcmClient.java create mode 100644 src/main/java/com/assu/server/infra/NotificationFactory.java diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java index 4687dbc..857de7b 100644 --- a/src/main/java/com/assu/server/domain/common/entity/Member.java +++ b/src/main/java/com/assu/server/domain/common/entity/Member.java @@ -12,7 +12,7 @@ @Getter @Entity -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java new file mode 100644 index 0000000..f51b642 --- /dev/null +++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java @@ -0,0 +1,27 @@ +package com.assu.server.domain.deviceToken.controller; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.deviceToken.service.DeviceTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("deviceTokens") +@RequiredArgsConstructor +public class DeviceTokenController { + private final DeviceTokenService service; + + public record RegisterTokenReq(String token) {} + + @PostMapping("/register") + public void register(@RequestBody RegisterTokenReq req, + @RequestParam Long MemberId) + { + service.register(req.token(), MemberId); + } + + @DeleteMapping("/unregister/{token_id}") + public void unregister(@PathVariable String token_id){ + service.unregister(token_id); + } +} diff --git a/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java new file mode 100644 index 0000000..03994a8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java @@ -0,0 +1,27 @@ +package com.assu.server.domain.deviceToken.entity; + +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DeviceToken extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="member_id", nullable=false) + private Member member; + + @Column(nullable=false, length=200, unique=true) + private String token; + + @Setter + @Column(nullable=false) + private boolean active; +} diff --git a/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java new file mode 100644 index 0000000..32365b9 --- /dev/null +++ b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java @@ -0,0 +1,16 @@ +package com.assu.server.domain.deviceToken.repository; + +import com.assu.server.domain.deviceToken.entity.DeviceToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface DeviceTokenRepository extends JpaRepository { + @Query("select dt.token from DeviceToken dt where dt.member.id=:memberId and dt.active=true") + List findActiveTokensByMemberId(@Param("memberId") Long memberId); + + Optional findByToken(String token); +} diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java new file mode 100644 index 0000000..cb4172a --- /dev/null +++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java @@ -0,0 +1,8 @@ +package com.assu.server.domain.deviceToken.service; + +import com.assu.server.domain.common.entity.Member; + +public interface DeviceTokenService { + void register(String token, Long memberId); + void unregister(String token); +} diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java new file mode 100644 index 0000000..5aedc84 --- /dev/null +++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java @@ -0,0 +1,32 @@ +package com.assu.server.domain.deviceToken.service; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.deviceToken.entity.DeviceToken; +import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DeviceTokenServiceImpl implements DeviceTokenService { + private final DeviceTokenRepository deviceTokenRepository; + //private final MemberRepository memberRepository; + + @Transactional + @Override + public void register(String token, Long memberId) { + Member member = memberRepository.findById(memberId); + + DeviceToken dt = deviceTokenRepository.findByToken(token) + .map(deviceToken -> { deviceToken.setActive(true); return deviceToken; }) + .orElse(DeviceToken.builder().member(member).token(token).active(true).build()); + deviceTokenRepository.save(dt); + } + + @Override + @Transactional + public void unregister(String token) { + deviceTokenRepository.findByToken(token).ifPresent(deviceToken -> deviceToken.setActive(false)); + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java index 6d5fe77..03a17f1 100644 --- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java @@ -1,4 +1,39 @@ package com.assu.server.domain.notification.controller; +import com.assu.server.domain.notification.dto.NotificationResponseDTO; +import com.assu.server.domain.notification.service.NotificationCommandService; +import com.assu.server.domain.notification.service.NotificationQueryService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; +import org.springframework.data.domain.Pageable; +import java.nio.file.AccessDeniedException; + +@RestController +@RequestMapping("notifications") +@RequiredArgsConstructor public class NotificationController { + private final NotificationQueryService query; + private final NotificationCommandService command; + + @GetMapping + public Page list( + @RequestParam(defaultValue = "all") String status, + @PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC) Pageable pageable, + @RequestParam Long memberId) { + + if (!"all".equalsIgnoreCase(status) && !"unread".equalsIgnoreCase(status)) { + throw new IllegalArgumentException("status must be one of [all, unread]"); + } + return query.listByStatus(status, pageable, memberId); + } + + + @PostMapping("/{notification_id}/read") + public void markRead(@PathVariable Long id, + @RequestParam Long memberId) throws AccessDeniedException { + command.markRead(id, memberId); + }U } diff --git a/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java b/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java index f87a43a..7dc85cb 100644 --- a/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java +++ b/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java @@ -1,4 +1,44 @@ package com.assu.server.domain.notification.converter; +import com.assu.server.domain.notification.dto.NotificationResponseDTO; +import com.assu.server.domain.notification.entity.Notification; + +import java.time.Duration; +import java.time.LocalDateTime; + public class NotificationConverter { + public static NotificationResponseDTO toDto(Notification n) { + return NotificationResponseDTO.builder() + .id(n.getId()) + .type(n.getType().name()) + .refId(n.getRefId()) + .title(n.getTitle()) + .messagePreview(n.getMessagePreview()) + .deeplink(n.getDeeplink()) + .isRead(n.isRead()) + .createdAt(n.getCreatedAt()) + .readAt(n.getReadAt()) + .timeAgo(toTimeAgo(n.getCreatedAt())) + .build(); + } + + /** 1시간 전까지 분 단위, 그 이후는 시간 단위(24h 초과 시 일 단위) */ + public static String toTimeAgo(LocalDateTime createdAt) { + if (createdAt == null) return ""; + + LocalDateTime now = LocalDateTime.now(); + if (createdAt.isAfter(now)) return "방금 전"; + + Duration d = Duration.between(createdAt, now); + long minutes = d.toMinutes(); + + if (minutes < 1) return "방금 전"; + if (minutes < 60) return minutes + "분 전"; + + long hours = minutes / 60; + if (hours < 24) return hours + "시간 전"; + + long days = hours / 24; + return days + "일 전"; + } } diff --git a/src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java b/src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java index 2f30739..bd4ffec 100644 --- a/src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java +++ b/src/main/java/com/assu/server/domain/notification/dto/NotificationResponseDTO.java @@ -1,4 +1,25 @@ package com.assu.server.domain.notification.dto; +import com.assu.server.domain.notification.entity.Notification; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor public class NotificationResponseDTO { + + private Long id; + private String type; + private Long refId; + private String title; + private String messagePreview; + private String deeplink; + private boolean isRead; + private LocalDateTime createdAt; + private LocalDateTime readAt; + private String timeAgo; } + diff --git a/src/main/java/com/assu/server/domain/notification/entity/Notification.java b/src/main/java/com/assu/server/domain/notification/entity/Notification.java index 5ac04a3..5ab52a5 100644 --- a/src/main/java/com/assu/server/domain/notification/entity/Notification.java +++ b/src/main/java/com/assu/server/domain/notification/entity/Notification.java @@ -1,39 +1,57 @@ package com.assu.server.domain.notification.entity; -import com.assu.server.domain.certification.entity.QRCertification; import com.assu.server.domain.common.entity.BaseEntity; -import com.assu.server.domain.partner.entity.Partner; - -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; + +import com.assu.server.domain.common.entity.Member; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity @Getter -@NoArgsConstructor @Builder +@NoArgsConstructor @AllArgsConstructor public class Notification extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "qr_id") - private QRCertification qrVerification; + @JoinColumn(name = "receiver_id", nullable = false) + private Member receiver; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "partner_id") - private Partner partner; + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 40) + private NotificationType type; + + // 원천 도메인의 식별자(폴리모픽 FK를 애플리케이션 레벨에서 관리) + @Column(nullable = false) + private Long refId; + + // 목록용 스냅샷(조인 없이 렌더) + @Column(nullable = false) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String messagePreview; + + private String deeplink; // ex) /chat/rooms/123 + + @Column(nullable = false) + private boolean isRead = false; + + @Column(nullable = true) + private LocalDateTime readAt; - private String content; - private Boolean isChecked; + public void markRead() { + if (!this.isRead) { + this.isRead = true; + this.readAt = LocalDateTime.now(); + } + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java b/src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java new file mode 100644 index 0000000..6f18073 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java @@ -0,0 +1,34 @@ +package com.assu.server.domain.notification.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotificationOutbox { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name="notification_id", nullable=false, unique=true) + private Notification notification; + + @Enumerated(EnumType.STRING) @Column(nullable=false) + private Status status; // PENDING, SENT, FAILED + + @Column(nullable=false) private int retryCount; + + public enum Status { PENDING, SENT, FAILED } + public void markSent(){ this.status = Status.SENT; } + public void markFailed(){ this.status = Status.FAILED; } + public void incRetry(){ this.retryCount++; } +} diff --git a/src/main/java/com/assu/server/domain/notification/entity/NotificationType.java b/src/main/java/com/assu/server/domain/notification/entity/NotificationType.java new file mode 100644 index 0000000..d1c79d2 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/entity/NotificationType.java @@ -0,0 +1,21 @@ +package com.assu.server.domain.notification.entity; + +import java.util.Arrays; + +public enum NotificationType { + CHAT("chat"), + PARTNER_SUGGESTION("partner_suggestion"), + PARTNER_PROPOSAL("partner_proposal"), + ORDER("order"); + + private final String code; + NotificationType(String code) { this.code = code; } + public String code() { return code; } + + public static NotificationType from(String code) { + return Arrays.stream(values()) + .filter(t -> t.code.equalsIgnoreCase(code)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unsupported type: " + code)); + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java new file mode 100644 index 0000000..2ce6a27 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java @@ -0,0 +1,10 @@ +package com.assu.server.domain.notification.repository; + +import com.assu.server.domain.notification.entity.NotificationOutbox; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface NotificationOutboxRepository extends JpaRepository { + List findTop50ByStatusOrderByIdAsc(NotificationOutbox.Status status); +} diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java index 592687f..d44bbae 100644 --- a/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java @@ -1,4 +1,21 @@ package com.assu.server.domain.notification.repository; -public class NotificationRepository { +import com.assu.server.domain.deviceToken.entity.DeviceToken; +import com.assu.server.domain.notification.entity.Notification; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; + +public interface NotificationRepository extends JpaRepository { + Page findByReceiverId(Long receiverId, Pageable pageable); + Page findByReceiverIdAndIsReadFalse(Long receiverId, Pageable pageable); + + @Modifying + @Query("update Notification n set n.isRead=true, n.readAt=:now where n.receiver.id=:memberId and n.isRead=false") + void markAllRead(@Param("memberId") Long memberId, @Param("now") LocalDateTime now); } diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java new file mode 100644 index 0000000..597494f --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java @@ -0,0 +1,12 @@ +package com.assu.server.domain.notification.service; + +import com.assu.server.domain.notification.entity.Notification; +import com.assu.server.domain.notification.entity.NotificationType; + +import java.nio.file.AccessDeniedException; +import java.util.Map; + +public interface NotificationCommandService { + Notification createAndQueue(com.assu.server.domain.common.entity.Member receiver, NotificationType type, Long refId, Map ctx); + void markRead(Long notificationId, Long currentMemberId) throws AccessDeniedException; +} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java new file mode 100644 index 0000000..01feba3 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java @@ -0,0 +1,46 @@ +package com.assu.server.domain.notification.service; + +import com.assu.server.domain.notification.entity.Notification; +import com.assu.server.domain.notification.entity.NotificationOutbox; +import com.assu.server.domain.notification.entity.NotificationType; +import com.assu.server.domain.notification.repository.NotificationOutboxRepository; +import com.assu.server.domain.notification.repository.NotificationRepository; +import com.assu.server.infra.NotificationFactory; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.nio.file.AccessDeniedException; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class NotificationCommandServiceImpl implements NotificationCommandService { + private final NotificationRepository notificationRepository; + private final NotificationOutboxRepository outboxRepository; + private final NotificationFactory notificationFactory; + + @Transactional + @Override + public Notification createAndQueue(com.assu.server.domain.common.entity.Member receiver, NotificationType type, Long refId, Map ctx) { + Notification notification = notificationFactory.create(receiver, type, refId, ctx); + notificationRepository.save(notification); + outboxRepository.save(NotificationOutbox.builder() + .notification(notification) + .status(NotificationOutbox.Status.PENDING) + .retryCount(0) + .build()); + return notification; + } + + + @Transactional + @Override + public void markRead(Long notificationId, Long currentMemberId) throws AccessDeniedException { + Notification n = notificationRepository.findById(notificationId).orElseThrow(); + if (!n.getReceiver().getId().equals(currentMemberId)) { + throw new AccessDeniedException("not yours"); + } + n.markRead(); + } +} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java b/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java new file mode 100644 index 0000000..6157c0e --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java @@ -0,0 +1,37 @@ +package com.assu.server.domain.notification.service; + +import com.assu.server.domain.notification.entity.Notification; +import com.assu.server.domain.notification.entity.NotificationOutbox; +import com.assu.server.domain.notification.repository.NotificationOutboxRepository; +import com.assu.server.infra.FcmClient; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class NotificationDispatcher { + private final NotificationOutboxRepository outboxRepo; + private final FcmClient fcmClient; + + @Scheduled(fixedDelay = 1000) // 1초 간격 배치 + @Transactional + public void dispatch() { + List batch = + outboxRepo.findTop50ByStatusOrderByIdAsc(NotificationOutbox.Status.PENDING); + + for (NotificationOutbox o : batch) { + try { + Notification n = o.getNotification(); + fcmClient.sendToMember(n.getReceiver(), n); + o.markSent(); + } catch (Exception e) { + o.incRetry(); + if (o.getRetryCount() >= 5) o.markFailed(); // 과도한 재시도 방지 + } + } + } +} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java new file mode 100644 index 0000000..fc45a22 --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java @@ -0,0 +1,9 @@ +package com.assu.server.domain.notification.service; + +import com.assu.server.domain.notification.dto.NotificationResponseDTO; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface NotificationQueryService { + Page listByStatus(String status, Pageable pageable, Long memberId); +} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java new file mode 100644 index 0000000..39c8d8e --- /dev/null +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java @@ -0,0 +1,33 @@ +package com.assu.server.domain.notification.service; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.notification.converter.NotificationConverter; +import com.assu.server.domain.notification.dto.NotificationResponseDTO; +import com.assu.server.domain.notification.entity.Notification; +import com.assu.server.domain.notification.repository.NotificationRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class NotificationQueryServiceImpl implements NotificationQueryService { + private final NotificationRepository notificationRepository; + //private final MemberRepository memberRepository; + + @Transactional + @Override + public Page listByStatus(String status, Pageable pageable, Long memberId) { + boolean unreadOnly = "unread".equalsIgnoreCase(status); + + Page page = unreadOnly + ? notificationRepository.findByReceiverIdAndIsReadFalse(memberId, pageable) + : notificationRepository.findByReceiverId(memberId, pageable); + + return page.map(NotificationConverter::toDto); + } +} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationService.java deleted file mode 100644 index a3fe03b..0000000 --- a/src/main/java/com/assu/server/domain/notification/service/NotificationService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notification.service; - -public interface NotificationService { -} diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java deleted file mode 100644 index 8d68840..0000000 --- a/src/main/java/com/assu/server/domain/notification/service/NotificationServiceImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.notification.service; - -public class NotificationServiceImpl { -} diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java deleted file mode 100644 index e042199..0000000 --- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.user.repository; - -public class StudentRepository { -} diff --git a/src/main/java/com/assu/server/infra/FcmClient.java b/src/main/java/com/assu/server/infra/FcmClient.java new file mode 100644 index 0000000..799e723 --- /dev/null +++ b/src/main/java/com/assu/server/infra/FcmClient.java @@ -0,0 +1,49 @@ +package com.assu.server.infra; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository; +import com.assu.server.domain.notification.entity.Notification; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class FcmClient { + private final FirebaseMessaging messaging; + private final DeviceTokenRepository tokenRepo; + + public void sendToMember(Member receiver, Notification n) { + List tokens = tokenRepo.findActiveTokensByMemberId(receiver.getId()); + if (tokens.isEmpty()) return; // 토큰 없으면 조용히 스킵 + + for (String token : tokens) { + Message msg = Message.builder() + .setToken(token) + // notification 채널(시스템 트레이 자동 표시) + data(딥링크/타입/ID) + .setNotification(com.google.firebase.messaging.Notification.builder() + .setTitle(n.getTitle()) + .setBody(n.getMessagePreview()) + .build()) + .putData("type", n.getType().name()) + .putData("refId", String.valueOf(n.getRefId())) + .putData("deeplink", n.getDeeplink()==null? "" : n.getDeeplink()) + .putData("notificationId", String.valueOf(n.getId())) + .build(); + try { + messaging.send(msg); + } catch (FirebaseMessagingException e) { + // 실패 코드에 따라 토큰 비활성화 등 치료 + String code = e.getMessagingErrorCode() == null ? "" : e.getMessagingErrorCode().name(); + if ("UNREGISTERED".equals(code) || "INVALID_ARGUMENT".equals(code)) { + tokenRepo.findByToken(token).ifPresent(t -> t.setActive(false)); + } + // 로깅/모니터링 + } + } + } +} diff --git a/src/main/java/com/assu/server/infra/NotificationFactory.java b/src/main/java/com/assu/server/infra/NotificationFactory.java new file mode 100644 index 0000000..0632e9a --- /dev/null +++ b/src/main/java/com/assu/server/infra/NotificationFactory.java @@ -0,0 +1,73 @@ +package com.assu.server.infra; + +import com.assu.server.domain.notification.entity.Notification; +import com.assu.server.domain.notification.entity.NotificationType; +import com.assu.server.domain.common.entity.Member; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class NotificationFactory { + + public Notification create(Member receiver, NotificationType type, Long refId, Map ctx) { + String title; + String preview; + String deeplink; + + switch (type) { + case CHAT -> { + String sender = asString(ctx.get("senderName"), "알 수 없음"); + String msg = asString(ctx.get("message"), ""); + title = "ASSU"; + preview = sender + ": " + truncateByCodePoint(msg, 200); + deeplink = "/chat/rooms/" + refId; + } + case PARTNER_SUGGESTION -> { + title = "제휴 건의"; + preview = "새로운 제휴 건의가 도착했어요!"; + deeplink = "/partner/suggestions/" + refId; + } + case ORDER -> { + title = "주문 안내"; + String tableNum = asString(ctx.get("table_num"), "?"); + String paper = asString(ctx.get("paper_content"), "선택한 혜택"); + preview = tableNum + "번 테이블에서 " + paper + " 혜택을 선택하셨어요."; + deeplink = "/orders/" + refId; // 애매함, UI 상에서 이동할 곳이 없음 + } + case PARTNER_PROPOSAL -> { + String partnerName = asString(ctx.get("partner_name"), "파트너"); + title = "제휴 제안"; + preview = partnerName + "에서 제휴 제안이 왔어요!"; + deeplink = "/partner/proposals/" + refId; + } + default -> throw new IllegalArgumentException("Unknown type: " + type); + } + + return Notification.builder() + .receiver(receiver) + .type(type) + .refId(refId) + .title(title) + .messagePreview(preview) + .deeplink(deeplink) + .isRead(false) + .build(); + } + + // ===== helpers ===== + private static String asString(Object v, String def) { + if (v == null) return def; + String s = String.valueOf(v).trim(); + return s.isEmpty() ? def : s; + } + + /** 코드포인트 기준 안전 절단(한글/이모지 깨짐 방지) */ + private static String truncateByCodePoint(String src, int maxCodePoints) { + if (src == null) return ""; + int len = src.codePointCount(0, src.length()); + if (len <= maxCodePoints) return src; + int endIdx = src.offsetByCodePoints(0, maxCodePoints); + return src.substring(0, endIdx); + } +} From 0949e58814b02f09c9da9996afcb2f7b858f0abf Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 13 Aug 2025 13:06:51 +0900 Subject: [PATCH 040/270] =?UTF-8?q?[FEAT/#21]=20best=20=EC=9D=B8=EA=B8=B0?= =?UTF-8?q?=20=EB=A7=A4=EC=9E=A5=20=EC=A1=B0=ED=9A=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PartnershipController.java | 1 + .../store/controller/StoreController.java | 20 ++++++++++++++++ .../domain/store/dto/StoreResponseDTO.java | 15 ++++++++++++ .../domain/store/service/StoreService.java | 3 +++ .../store/service/StoreServiceImpl.java | 23 ++++++++++++++++++- .../PartnershipUsageRepository.java | 13 +++++++++++ 6 files changed, 74 insertions(+), 1 deletion(-) 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 3377910..7e2142a 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 @@ -2,6 +2,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/com/assu/server/domain/store/controller/StoreController.java b/src/main/java/com/assu/server/domain/store/controller/StoreController.java index 1f9d61b..c902c1a 100644 --- a/src/main/java/com/assu/server/domain/store/controller/StoreController.java +++ b/src/main/java/com/assu/server/domain/store/controller/StoreController.java @@ -1,4 +1,24 @@ package com.assu.server.domain.store.controller; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.assu.server.domain.store.dto.StoreResponseDTO; +import com.assu.server.global.apiPayload.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "가게 관련 api", description = "가게와 관련된 api") +@RequiredArgsConstructor public class StoreController { + + @GetMapping("/store/best") + @Operation(summary = "홈화면의 현재 인기 매장 조회 api", description = "관리자, 사용자, 제휴업체 모두 사용하는 api") + public ResponseEntity> getTodayBestStore(){ + + } } diff --git a/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java b/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java index bbe84ba..d1465ae 100644 --- a/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java +++ b/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java @@ -1,4 +1,19 @@ package com.assu.server.domain.store.dto; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + public class StoreResponseDTO { + + @AllArgsConstructor + @RequiredArgsConstructor + @Builder + @Getter + public static class todayBest{ + List bestStores; + } } diff --git a/src/main/java/com/assu/server/domain/store/service/StoreService.java b/src/main/java/com/assu/server/domain/store/service/StoreService.java index 15ad373..e447492 100644 --- a/src/main/java/com/assu/server/domain/store/service/StoreService.java +++ b/src/main/java/com/assu/server/domain/store/service/StoreService.java @@ -1,4 +1,7 @@ package com.assu.server.domain.store.service; +import com.assu.server.domain.store.dto.StoreResponseDTO; + public interface StoreService { + StoreResponseDTO.todayBest getTodayBestStore(); } diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java index a92599e..07236c6 100644 --- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java +++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java @@ -1,4 +1,25 @@ package com.assu.server.domain.store.service; -public class StoreServiceImpl { +import java.util.List; + +import com.assu.server.domain.store.dto.StoreResponseDTO; +import com.assu.server.domain.store.repository.StoreRepository; +import com.assu.server.domain.user.repository.PartnershipUsageRepository; + +import jakarta.transaction.Transactional; + +public class StoreServiceImpl implements StoreService{ + + private PartnershipUsageRepository partnershipUsageRepository; + + @Override + @Transactional + public StoreResponseDTO.todayBest getTodayBestStore() { + List bestStores = partnershipUsageRepository.findTodayPopularPartnership(); + + return StoreResponseDTO.todayBest.builder() + .bestStores(bestStores) + .build(); + } + } diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java index d38de37..075d1eb 100644 --- a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java @@ -1,9 +1,22 @@ package com.assu.server.domain.user.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import com.assu.server.domain.user.entity.PartnershipUsage; public interface PartnershipUsageRepository extends JpaRepository { + @Query(value = """ + SELECT place + FROM partnership_usage + WHERE date >= CONVERT_TZ(CURDATE(), '+00:00', '+09:00') + AND date < CONVERT_TZ(CURDATE() + INTERVAL 1 DAY, '+00:00', '+09:00') + GROUP BY place + ORDER BY COUNT(*) DESC + LIMIT 10 + """, nativeQuery = true) + List findTodayPopularPartnership(); } From 2302f1155e28d6f2e2b7569f13f87b05fbef61de Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Wed, 13 Aug 2025 13:11:12 +0900 Subject: [PATCH 041/270] =?UTF-8?q?[FEAT/#21]=20best=20=EC=9D=B8=EA=B8=B0?= =?UTF-8?q?=20=EB=A7=A4=EC=9E=A5=20=EC=A1=B0=ED=9A=8C=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/store/controller/StoreController.java | 9 ++++++++- .../server/domain/store/service/StoreServiceImpl.java | 5 +++++ .../global/apiPayload/code/status/SuccessStatus.java | 5 ++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/store/controller/StoreController.java b/src/main/java/com/assu/server/domain/store/controller/StoreController.java index c902c1a..2b5a0b6 100644 --- a/src/main/java/com/assu/server/domain/store/controller/StoreController.java +++ b/src/main/java/com/assu/server/domain/store/controller/StoreController.java @@ -1,11 +1,15 @@ package com.assu.server.domain.store.controller; +import java.awt.*; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.assu.server.domain.store.dto.StoreResponseDTO; +import com.assu.server.domain.store.service.StoreService; import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -16,9 +20,12 @@ @RequiredArgsConstructor public class StoreController { + private final StoreService storeService; + @GetMapping("/store/best") @Operation(summary = "홈화면의 현재 인기 매장 조회 api", description = "관리자, 사용자, 제휴업체 모두 사용하는 api") public ResponseEntity> getTodayBestStore(){ - + StoreResponseDTO.todayBest result = storeService.getTodayBestStore(); + return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.BEST_STORE_SUCCESS, result)); } } diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java index 07236c6..e807872 100644 --- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java +++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java @@ -2,12 +2,17 @@ import java.util.List; +import org.springframework.stereotype.Service; + import com.assu.server.domain.store.dto.StoreResponseDTO; import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.domain.user.repository.PartnershipUsageRepository; import jakarta.transaction.Transactional; + +@Service +@Transactional public class StoreServiceImpl implements StoreService{ private PartnershipUsageRepository partnershipUsageRepository; diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java index 46a3521..4fcca7a 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java @@ -110,7 +110,10 @@ public enum SuccessStatus implements BaseCode { // 개인 인증 - PERSONAL_CERTIFICATION_SUCCESS(HttpStatus.OK, "PERSONAL201", "개인 인증이 완료 되었습니다.") + PERSONAL_CERTIFICATION_SUCCESS(HttpStatus.OK, "PERSONAL201", "개인 인증이 완료 되었습니다."), + + // 베스트 조회 + BEST_STORE_SUCCESS(HttpStatus.OK, "STORE205", "베스트 매장 조회에 성공하였습니다") ; private final HttpStatus httpStatus; From 167e81ca654d1666d656ebf691b158bc1b93503f Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:29:35 +1000 Subject: [PATCH 042/270] =?UTF-8?q?[FEAT/#22]=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=09-=20=EB=AC=B8?= =?UTF-8?q?=EC=9D=98=20=EC=83=9D=EC=84=B1=20API=20=09-=20=EB=AC=B8?= =?UTF-8?q?=EC=9D=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=09-=20=EB=AC=B8=EC=9D=98=20=EB=8B=A8=EA=B1=B4=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20=09-=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=EC=9E=90=20=EB=8B=B5=EB=B3=80=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=B2=98=EB=A6=AC=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inquiry/controller/InquiryController.java | 60 +++++++++++++++ .../inquiry/converter/InquiryConverter.java | 17 +++++ .../inquiry/dto/InquiryCreateRequestDTO.java | 12 +++ .../inquiry/dto/InquiryResponseDTO.java | 28 +++++++ .../server/domain/inquiry/entity/Inquiry.java | 47 ++++++++++++ .../inquiry/repository/InquiryRepository.java | 12 +++ .../inquiry/service/InquiryService.java | 13 ++++ .../inquiry/service/InquiryServiceImpl.java | 75 +++++++++++++++++++ 8 files changed, 264 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java create mode 100644 src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java create mode 100644 src/main/java/com/assu/server/domain/inquiry/dto/InquiryCreateRequestDTO.java create mode 100644 src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java create mode 100644 src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java create mode 100644 src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java create mode 100644 src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java new file mode 100644 index 0000000..93efb21 --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java @@ -0,0 +1,60 @@ +package com.assu.server.domain.inquiry.controller; + +import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO; +import com.assu.server.domain.inquiry.dto.InquiryResponseDTO; +import com.assu.server.domain.inquiry.service.InquiryService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/member/inquiries") +@RequiredArgsConstructor +public class InquiryController { + + private final InquiryService inquiryService; + + @PostMapping + public ResponseEntity create( + @RequestBody @Valid InquiryCreateRequestDTO req, + @RequestParam Long memberId + ) { + Long id = inquiryService.create(req, memberId); + return ResponseEntity.ok(id); + } + + @GetMapping + public Page list( + @RequestParam(defaultValue = "all") String status, + @PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC) Pageable pageable, + @RequestParam Long memberId + ) { + if (!"all".equalsIgnoreCase(status) + && !"waiting".equalsIgnoreCase(status) + && !"answered".equalsIgnoreCase(status)) { + throw new IllegalArgumentException("상태값: [all, waiting, answered]"); + } + return inquiryService.list(status, pageable, memberId); + } + + /** 단건 상세 조회*/ + @GetMapping("/{inquiry_id}") + public InquiryResponseDTO get( + @PathVariable Long id, + @RequestParam Long memberId + ) { + return inquiryService.get(id, memberId); + } + + /** 운영자: 답변 완료 처리 */ + @PatchMapping("/{inquiry_id}/answer") + public ResponseEntity markAnswered(@PathVariable Long id) { + inquiryService.markAnswered(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java b/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java new file mode 100644 index 0000000..a31a4e0 --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java @@ -0,0 +1,17 @@ +package com.assu.server.domain.inquiry.converter; + +import com.assu.server.domain.inquiry.dto.InquiryResponseDTO; +import com.assu.server.domain.inquiry.entity.Inquiry; + +public class InquiryConverter { + public InquiryResponseDTO toDto(Inquiry i) { + return InquiryResponseDTO.builder() + .id(i.getId()) + .title(i.getTitle()) + .content(i.getContent()) + .email(i.getEmail()) + .status(i.getStatus().name()) + .createdAt(i.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/assu/server/domain/inquiry/dto/InquiryCreateRequestDTO.java b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryCreateRequestDTO.java new file mode 100644 index 0000000..24a958b --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryCreateRequestDTO.java @@ -0,0 +1,12 @@ +package com.assu.server.domain.inquiry.dto; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor @Builder +public class InquiryCreateRequestDTO { + private String title; + private String content; + private String email; +} diff --git a/src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java new file mode 100644 index 0000000..39fc513 --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java @@ -0,0 +1,28 @@ +package com.assu.server.domain.inquiry.dto; +import com.assu.server.domain.inquiry.entity.Inquiry; +import lombok.*; +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class InquiryResponseDTO { + private Long id; + private String title; + private String content; + private String email; + private String status; + private LocalDateTime createdAt; + + public static InquiryResponseDTO from(Inquiry inquiry) { + return InquiryResponseDTO.builder() + .id(inquiry.getId()) + .title(inquiry.getTitle()) + .content(inquiry.getContent()) + .email(inquiry.getEmail()) + .status(inquiry.getStatus().name()) + .createdAt(inquiry.getCreatedAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java b/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java new file mode 100644 index 0000000..4e9568c --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java @@ -0,0 +1,47 @@ +package com.assu.server.domain.inquiry.entity; + +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Inquiry extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false, length = 120) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Column(nullable = false, length = 120) + private String email; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private Status status; + + private LocalDateTime answeredAt; + + public enum Status { WAITING, ANSWERED } + + // 상태 전환 + public void markAnswered() { + this.status = Status.ANSWERED; + this.answeredAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java b/src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java new file mode 100644 index 0000000..92d8f02 --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java @@ -0,0 +1,12 @@ +package com.assu.server.domain.inquiry.repository; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.inquiry.entity.Inquiry; +import com.assu.server.domain.inquiry.entity.Inquiry.Status; +import org.springframework.data.domain.*; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InquiryRepository extends JpaRepository { + Page findByMemberId(Long memberId, Pageable pageable); + Page findByMemberIdAndStatus(Long memberId, Status status, Pageable pageable); +} diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java new file mode 100644 index 0000000..acce980 --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java @@ -0,0 +1,13 @@ +package com.assu.server.domain.inquiry.service; + +import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO; +import com.assu.server.domain.inquiry.dto.InquiryResponseDTO; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface InquiryService { + Long create(InquiryCreateRequestDTO req, Long memberId); + Page list(String status, Pageable pageable, Long memberId); + InquiryResponseDTO get(Long id, Long memberId); + void markAnswered(Long id); +} diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java new file mode 100644 index 0000000..c5036c5 --- /dev/null +++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java @@ -0,0 +1,75 @@ +package com.assu.server.domain.inquiry.service; + +import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.inquiry.converter.InquiryConverter; +import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO; +import com.assu.server.domain.inquiry.dto.InquiryResponseDTO; +import com.assu.server.domain.inquiry.entity.Inquiry; +import com.assu.server.domain.inquiry.entity.Inquiry.Status; +import com.assu.server.domain.inquiry.repository.InquiryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class InquiryServiceImpl implements InquiryService { + + private final InquiryRepository inquiryRepository; + private final InquiryConverter inquiryConverter; + private final MemberRepository memberRepository; + + /** 문의 등록 */ + @Transactional + @Override + public Long create(InquiryCreateRequestDTO req, Long memberId) { + Member member = memberRepository.getReferenceById(memberId); + + Inquiry inquiry = Inquiry.builder() + .member(member) + .title(req.getTitle()) + .content(req.getContent()) + .email(req.getEmail()) + .status(Status.WAITING) + .build(); + + inquiryRepository.save(inquiry); + return inquiry.getId(); + } + + /** 문의 내역 조회 (status=all|waiting|answered) */ + @Transactional(readOnly = true) + @Override + public Page list(String status, Pageable pageable, Long memberId) { + Page page = switch (status.toLowerCase()) { + case "waiting" -> inquiryRepository.findByMemberIdAndStatus(memberId, Status.WAITING, pageable); + case "answered" -> inquiryRepository.findByMemberIdAndStatus(memberId, Status.ANSWERED, pageable); + case "all" -> inquiryRepository.findByMemberId(memberId, pageable); + default -> throw new IllegalArgumentException("status must be one of [all, waiting, answered]"); + }; + + return page.map(inquiryConverter::toDto); + } + + /** 단건 상세 조회 */ + @Transactional(readOnly = true) + @Override + public InquiryResponseDTO get(Long id, Long memberId) { + Inquiry inquiry = inquiryRepository.findById(id).orElseThrow(); + if (!inquiry.getMember().getId().equals(memberId)) { + throw new IllegalArgumentException("not yours"); + } + return inquiryConverter.toDto(inquiry); + } + + /** 운영자가 답변 완료 처리 */ + @Transactional + @Override + public void markAnswered(Long id) { + Inquiry i = inquiryRepository.findById(id).orElseThrow(); + i.markAnswered(); + // TODO: 필요 시 '답변 완료' 알림 발송 + } +} \ No newline at end of file From cd5176efa6629b1c45dff9778765d94028b86df0 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Wed, 13 Aug 2025 16:48:34 +0900 Subject: [PATCH 043/270] =?UTF-8?q?feat/#13-review=20=20-=20=EB=88=84?= =?UTF-8?q?=EC=A0=81=20=EA=B0=80=EC=9E=85=EC=9E=90=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudentAdminController.java | 27 ++++++++++++++ .../converter/StudentAdminConverter.java | 15 ++++++++ .../mapping/dto/StudentAdminRequestDTO.java | 5 +++ .../mapping/dto/StudentAdminResponseDTO.java | 19 ++++++++++ .../domain/mapping/entity/StudentAdmin.java | 30 ++++++++++++++++ .../repository/StudentAdminRepository.java | 18 ++++++++++ .../mapping/service/StudentAdminService.java | 7 ++++ .../service/StudentAdminServiceImpl.java | 35 +++++++++++++++++++ .../review/service/ReviewServiceImpl.java | 6 ++-- .../apiPayload/code/status/ErrorStatus.java | 4 ++- 10 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java create mode 100644 src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java create mode 100644 src/main/java/com/assu/server/domain/mapping/dto/StudentAdminRequestDTO.java create mode 100644 src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java create mode 100644 src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java create mode 100644 src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java create mode 100644 src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java new file mode 100644 index 0000000..bb32dd7 --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java @@ -0,0 +1,27 @@ +package com.assu.server.domain.mapping.controller; + +import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO; +import com.assu.server.domain.mapping.service.StudentAdminService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin") +public class StudentAdminController { + private final StudentAdminService studentAdminService; + @Operation( + summary = "누적 가입수 조회 API입니다.", + description = "admin으로 접근해주세요." + ) + @GetMapping + public BaseResponse getCountAdmin() { + return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth()); + } + +} diff --git a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java new file mode 100644 index 0000000..abff2bd --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java @@ -0,0 +1,15 @@ +package com.assu.server.domain.mapping.converter; + +import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO; + +public class StudentAdminConverter { + + public static StudentAdminResponseDTO.CountAdminAuthResponseDTO countAdminAuthDTO(Long adminId, Long total, String adminName) { + return StudentAdminResponseDTO.CountAdminAuthResponseDTO.builder() + .adminId(adminId) + .studentCount(total) + .adminName(adminName) + .build(); + } + +} diff --git a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminRequestDTO.java b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminRequestDTO.java new file mode 100644 index 0000000..b4f2c06 --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminRequestDTO.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.mapping.dto; + +public class StudentAdminRequestDTO { + +} diff --git a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java new file mode 100644 index 0000000..fa042ac --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java @@ -0,0 +1,19 @@ +package com.assu.server.domain.mapping.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class StudentAdminResponseDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CountAdminAuthResponseDTO{ // admin에 따른 총 누적 가입자 수 + private Long studentCount; + private Long adminId; + private String adminName; + } +} diff --git a/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java b/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java new file mode 100644 index 0000000..97770d8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java @@ -0,0 +1,30 @@ +package com.assu.server.domain.mapping.entity; + +import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.admin.entity.Admin; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "student_admin_mapping") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class StudentAdmin { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // Student 연결 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "student_id", nullable = false) + private Student student; + + // Admin 연결 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "admin_id", nullable = false) + private Admin admin; + +} diff --git a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java new file mode 100644 index 0000000..d77a64e --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java @@ -0,0 +1,18 @@ +package com.assu.server.domain.mapping.repository; + +import com.assu.server.domain.mapping.entity.StudentAdmin; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.Collection; + +public interface StudentAdminRepository extends JpaRepository { + @Query(""" + select count(sa) + from StudentAdmin sa + where sa.admin.id = :adminId + """) + Long countAllByAdminId(@Param("adminId") Long adminId); +} diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java new file mode 100644 index 0000000..3639fcc --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java @@ -0,0 +1,7 @@ +package com.assu.server.domain.mapping.service; + +import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO; + +public interface StudentAdminService { + StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(); +} diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java new file mode 100644 index 0000000..42833b0 --- /dev/null +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java @@ -0,0 +1,35 @@ +package com.assu.server.domain.mapping.service; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.mapping.converter.StudentAdminConverter; +import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO; +import com.assu.server.domain.mapping.repository.StudentAdminRepository; +import com.assu.server.domain.user.service.StudentService; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@RequiredArgsConstructor +public class StudentAdminServiceImpl implements StudentAdminService { + private final StudentAdminRepository studentAdminRepository; + private final AdminRepository adminRepository; + + @Override + @Transactional + public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth() { + //Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 6L; + Long total = studentAdminRepository.countAllByAdminId(memberId); + Admin admin = adminRepository.findById(memberId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + String adminName = admin.getName(); + + return StudentAdminConverter.countAdminAuthDTO(memberId, total, adminName); + } + +} diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index ad7aeb0..041a416 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -64,10 +64,10 @@ public List checkStudentReview( @Override @Transactional public List checkPartnerReview() { - //Long partnerId = SecurityUtil.getCurrentUserId; - Long partnerId = 2L; //ID 하드코딩 한 것 + //Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 2L; - Partner partner = partnerRepository.findById(partnerId) + Partner partner = partnerRepository.findById(memberId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); System.out.println("파트너 id는 "+partner.getId()); Store store = storeRepository.findByPartner(partner) diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index 7180da8..a83747e 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -25,7 +25,9 @@ public enum ErrorStatus implements BaseErrorCode { //파트너 에러 NO_SUCH_PARTNER(HttpStatus.NOT_FOUND, "PARTNER_4002", "존재하지 않는 파트너 ID입니다."), //스투던트 에러 - NO_SUCH_STUDENT(HttpStatus.NOT_FOUND, "STUDENT_4003", "존재하지 않는 학생 ID입니다.") + NO_SUCH_STUDENT(HttpStatus.NOT_FOUND, "STUDENT_4003", "존재하지 않는 학생 ID입니다."), + //어드민 에러 + NO_SUCH_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_4004", "존재하지 않는 관리자 ID입니다.") ; private final HttpStatus httpStatus; From 04e36cc631067942e424387bcf8b9951ddd564ca Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Wed, 13 Aug 2025 17:20:18 +0900 Subject: [PATCH 044/270] =?UTF-8?q?feat/#13-review=20=20-=20=EB=88=84?= =?UTF-8?q?=EC=A0=81=20=EA=B0=80=EC=9E=85=EC=9E=90=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudentAdminController.java | 10 +++++++++- .../converter/StudentAdminConverter.java | 7 +++++++ .../mapping/dto/StudentAdminResponseDTO.java | 9 +++++++++ .../domain/mapping/entity/StudentAdmin.java | 3 ++- .../repository/StudentAdminRepository.java | 19 +++++++++++++++++++ .../mapping/service/StudentAdminService.java | 1 + .../service/StudentAdminServiceImpl.java | 12 +++++++++++- 7 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java index bb32dd7..d3b8a55 100644 --- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java +++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java @@ -16,12 +16,20 @@ public class StudentAdminController { private final StudentAdminService studentAdminService; @Operation( - summary = "누적 가입수 조회 API입니다.", + summary = "누적 가입자 수 조회 API 입니다.", description = "admin으로 접근해주세요." ) @GetMapping public BaseResponse getCountAdmin() { return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth()); } + @Operation( + summary = "신규 한 달 가입자 수 조회 API 입니다.", + description = "admin으로 접근해주세요." + ) + @GetMapping("/new") + public BaseResponse getNewStudentCountAdmin(){ + return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getNewStudentCountAdmin()); + } } diff --git a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java index abff2bd..0bb6ed2 100644 --- a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java +++ b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java @@ -12,4 +12,11 @@ public static StudentAdminResponseDTO.CountAdminAuthResponseDTO countAdminAuthDT .build(); } + public static StudentAdminResponseDTO.NewCountAdminResponseDTO newCountAdminResponseDTO(Long adminId, Long total, String adminName){ + return StudentAdminResponseDTO.NewCountAdminResponseDTO.builder() + .adminId(adminId) + .newStudentCount(total) + .adminName(adminName) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java index fa042ac..fe49519 100644 --- a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java +++ b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java @@ -16,4 +16,13 @@ public static class CountAdminAuthResponseDTO{ // admin에 따른 총 누적 가 private Long adminId; private String adminName; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class NewCountAdminResponseDTO{ //신규 가입자수 (매달 1일 초기화) + private Long newStudentCount; + private Long adminId; + private String adminName; + } } diff --git a/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java b/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java index 97770d8..c0bc842 100644 --- a/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java +++ b/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java @@ -1,5 +1,6 @@ package com.assu.server.domain.mapping.entity; +import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.user.entity.Student; import com.assu.server.domain.admin.entity.Admin; import jakarta.persistence.*; @@ -11,7 +12,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder -public class StudentAdmin { +public class StudentAdmin extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java index d77a64e..84e0165 100644 --- a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java +++ b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; +import java.time.YearMonth; import java.util.Collection; public interface StudentAdminRepository extends JpaRepository { @@ -15,4 +16,22 @@ select count(sa) where sa.admin.id = :adminId """) Long countAllByAdminId(@Param("adminId") Long adminId); + + + @Query(""" + select count(sa) + from StudentAdmin sa + where sa.admin.id = :adminId + and sa.createdAt >= :from + and sa.createdAt < :to + """) + Long countByAdminIdBetween(@Param("adminId") Long adminId, + @Param("from") LocalDateTime from, + @Param("to") LocalDateTime to); + + default Long countThisMonthByAdminId(Long adminId) { + LocalDateTime from = YearMonth.now().atDay(1).atStartOfDay(); + LocalDateTime to = LocalDateTime.now(); + return countByAdminIdBetween(adminId, from, to); + } } diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java index 3639fcc..3faafd8 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java @@ -4,4 +4,5 @@ public interface StudentAdminService { StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(); + StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(); } diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java index 42833b0..8aff9d5 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java @@ -31,5 +31,15 @@ public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth() { return StudentAdminConverter.countAdminAuthDTO(memberId, total, adminName); } - + @Override + @Transactional + public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin() { + //Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 5L; + Long total = studentAdminRepository.countThisMonthByAdminId(memberId); + Admin admin = adminRepository.findById(memberId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + String adminName = admin.getName(); + return StudentAdminConverter.newCountAdminResponseDTO(memberId, total, adminName); + } } From 9faeaf01d0dfb652c4595a501218b8d951b1e01b Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Wed, 13 Aug 2025 17:34:50 +0900 Subject: [PATCH 045/270] =?UTF-8?q?[Feat/#14]=20=20-=20=EA=B1=B4=EC=9D=98?= =?UTF-8?q?=EB=90=9C=20=EC=A0=9C=ED=9C=B4=20=EB=AA=A8=EB=91=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=20-=20=EC=A0=9C=ED=9C=B4=20=EC=A0=9C=EC=95=88?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EC=84=B1=20=20-=20=EC=A0=9C=ED=9C=B4=20?= =?UTF-8?q?=EC=A0=9C=EC=95=88=EC=84=9C=20=EC=83=81=EC=84=B8=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=20-=20=EC=A0=9C=EC=95=88=EB=90=9C=20=EC=A0=9C?= =?UTF-8?q?=ED=9C=B4=20=EC=A0=84=EC=B2=B4/=EC=9D=BC=EB=B6=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=20-=20=EC=A0=9C=ED=9C=B4=EB=8B=A8=EC=B2=B4=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=20-=20=EC=A0=9C=ED=9C=B4=EC=97=85?= =?UTF-8?q?=EC=B2=B4=20=EC=B6=94=EC=B2=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- .../admin/controller/AdminController.java | 26 ++++ .../domain/admin/dto/AdminResponseDTO.java | 16 +++ .../admin/repository/AdminRepository.java | 30 ++++ .../domain/admin/service/AdminService.java | 4 + .../admin/service/AdminServiceImpl.java | 47 ++++++- .../partner/controller/PartnerController.java | 23 +++ .../partner/dto/PartnerResponseDTO.java | 27 ++++ .../partner/repository/PartnerRepository.java | 33 ++++- .../partner/service/PartnerService.java | 5 + .../partner/service/PartnerServiceImpl.java | 58 +++++++- .../controller/PartnershipController.java | 54 ++++++++ .../controller/PatnershipController.java | 4 - .../converter/PartnershipConverter.java | 130 +++++++++++++++++ .../converter/PatnershipConverter.java | 4 - .../dto/PartnershipRequestDTO.java | 39 ++++++ .../dto/PartnershipResponseDTO.java | 51 +++++++ .../partnership/dto/PatnershipRequestDTO.java | 4 - .../dto/PatnershipResponseDTO.java | 4 - .../domain/partnership/entity/Goods.java | 24 ++++ .../domain/partnership/entity/Paper.java | 18 +-- .../partnership/entity/PaperContent.java | 34 ++--- ...perContentType.java => CriterionType.java} | 6 +- .../partnership/entity/enums/OptionType.java | 5 + .../repository/GoodsRepository.java | 7 + .../repository/PaperContentRepository.java | 27 ++++ .../repository/PaperRepository.java | 7 + .../repository/PatnershipRepository.java | 4 - .../service/PartnershipService.java | 18 +++ .../service/PartnershipServiceImpl.java | 131 ++++++++++++++++++ .../service/PatnershipService.java | 4 - .../service/PatnershipServiceImpl.java | 4 - .../controller/SuggestionController.java | 16 ++- .../converter/SuggestionConverter.java | 22 +++ .../suggestion/dto/SuggestionRequestDTO.java | 1 - .../suggestion/dto/SuggestionResponseDTO.java | 55 +++++--- .../repository/SuggestionRepository.java | 12 ++ .../suggestion/service/SuggestionService.java | 7 + .../service/SuggestionServiceImpl.java | 25 +++- .../apiPayload/code/status/ErrorStatus.java | 37 +++++ 40 files changed, 935 insertions(+), 91 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java create mode 100644 src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java create mode 100644 src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java create mode 100644 src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/dto/PatnershipRequestDTO.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/dto/PatnershipResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/Goods.java rename src/main/java/com/assu/server/domain/partnership/entity/enums/{PaperContentType.java => CriterionType.java} (50%) create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java create mode 100644 src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java delete mode 100644 src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java diff --git a/.gitignore b/.gitignore index 9503f1d..66fb1ea 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ out/ ### Secret ### src/main/resources/application-secret.yml src/test/resources/application-test.yml -src/test/resources/application-secret.yml \ No newline at end of file +src/test/resources/application-secret.yml +#src/main/resources/firebase/service-account.json \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java index e39e2b3..c41f470 100644 --- a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java +++ b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java @@ -1,4 +1,30 @@ package com.assu.server.domain.admin.controller; +import com.assu.server.domain.admin.dto.AdminResponseDTO; +import com.assu.server.domain.admin.service.AdminService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +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 +@RequestMapping("/admin") +@RequiredArgsConstructor public class AdminController { + + private final AdminService adminService; + + @Operation( + summary = "제휴하지 않은 파트너를 추천하는 API 입니다.", + description = "제휴하지 않은 파트너 중 한 곳을 랜덤으로 조회합니다." + ) + @GetMapping("/partner-recommend") + public BaseResponse randomPartnerRecommend( + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, adminService.suggestRandomPartner()); + } } diff --git a/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java b/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java index 871d3db..3157770 100644 --- a/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java +++ b/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java @@ -1,4 +1,20 @@ package com.assu.server.domain.admin.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + public class AdminResponseDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class RandomPartnerResponseDTO { + private Long partnerId; + private String partnerAddress; + private String partnerDetailAddress; + private String partnerName; + } } diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java index 4fd442a..0786124 100644 --- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java +++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java @@ -2,6 +2,36 @@ import com.assu.server.domain.admin.entity.Admin; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface AdminRepository extends JpaRepository { + // 후보 수 카운트: 해당 partner와 ACTIVE 제휴가 없는 admin 수 + @Query(value = """ + SELECT COUNT(*) + FROM admin a + LEFT JOIN paper pa + ON pa.admin_id = a.id + AND pa.partner_id = :partnerId + AND pa.is_activated = 'ACTIVE' + WHERE pa.id IS NULL + """, nativeQuery = true) + long countPartner(@Param("partnerId") Long partnerId); + + // 랜덤 오프셋으로 1~N건 가져오기 (LIMIT :offset, :limit) + @Query(value = """ + SELECT a.* + FROM admin a + LEFT JOIN paper pa + ON pa.admin_id = a.id + AND pa.partner_id = :partnerId + AND pa.is_activated = 'ACTIVE' + WHERE pa.id IS NULL + LIMIT :offset, :limit + """, nativeQuery = true) + List findPartnerWithOffset(@Param("partnerId") Long partnerId, + @Param("offset") int offset, + @Param("limit") int limit); } diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminService.java b/src/main/java/com/assu/server/domain/admin/service/AdminService.java index c01f598..ca13088 100644 --- a/src/main/java/com/assu/server/domain/admin/service/AdminService.java +++ b/src/main/java/com/assu/server/domain/admin/service/AdminService.java @@ -1,4 +1,8 @@ package com.assu.server.domain.admin.service; +import com.assu.server.domain.admin.dto.AdminResponseDTO; + public interface AdminService { + + AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(); } diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java index 3c3eb41..841a438 100644 --- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java @@ -1,4 +1,49 @@ package com.assu.server.domain.admin.service; -public class AdminServiceImpl { +import com.assu.server.domain.admin.dto.AdminResponseDTO; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partner.repository.PartnerRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ThreadLocalRandom; + +@Service +@RequiredArgsConstructor +public class AdminServiceImpl implements AdminService { + + private final AdminRepository adminRepository; + private final PartnerRepository partnerRepository; + + @Override + public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner() { +// Long adminId = SecurityUtil.getCurrentId(); + Long adminId = 1L; + Admin admin = adminRepository.findById(adminId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + + long total = partnerRepository.countUnpartneredActiveByAdmin(admin.getId()); + if (total <= 0) { + throw new DatabaseException(ErrorStatus.NO_AVAILABLE_PARTNER); + } + + int offset = ThreadLocalRandom.current().nextInt((int)total); + + Partner picked = partnerRepository.findUnpartneredActiveByAdminWithOffset(admin.getId(), offset); + if(picked == null) { + throw new DatabaseException(ErrorStatus.NO_AVAILABLE_PARTNER); + } + + return AdminResponseDTO.RandomPartnerResponseDTO.builder() + .partnerId(picked.getId()) + .partnerName(picked.getName()) + .partnerAddress(picked.getAddress()) + .partnerDetailAddress(picked.getDetailAddress()) + .build(); + + } } diff --git a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java index ec3d25d..697a9e0 100644 --- a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java +++ b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java @@ -1,4 +1,27 @@ package com.assu.server.domain.partner.controller; +import com.assu.server.domain.partner.dto.PartnerResponseDTO; +import com.assu.server.domain.partner.service.PartnerService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/partner") +@RequiredArgsConstructor public class PartnerController { + + private final PartnerService partnerService; + + @Operation( + summary = "제휴하지 않은 어드민을 추천하는 API 입니다.", + description = "제휴하지 않은 어드민 중 두 곳을 랜덤으로 조회합니다." + ) + @GetMapping("/admin-recommend") + public BaseResponse randomAdminRecommend(){ + return BaseResponse.onSuccess(SuccessStatus._OK, partnerService.getRandomAdmin()); + } } diff --git a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java index 8e5e0af..c87fff8 100644 --- a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java @@ -1,4 +1,31 @@ package com.assu.server.domain.partner.dto; +import com.assu.server.domain.admin.entity.Admin; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + public class PartnerResponseDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class RandomAdminResponseDTO { + private List admins; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class AdminLiteDTO { + private Long adminId; + private String adminAddress; + private String adminDetailAddress; + private String adminName; + } } diff --git a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java index f6f0ce4..5a98909 100644 --- a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java +++ b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java @@ -1,4 +1,35 @@ package com.assu.server.domain.partner.repository; -public class PartnerRepository { +import com.assu.server.domain.partner.entity.Partner; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PartnerRepository extends JpaRepository { + + // 현재 admin과 'ACTIVE' 상태로 제휴 중인 partner를 제외한 후보 수 + @Query(value = """ + SELECT COUNT(*) + FROM partner p + LEFT JOIN paper pa + ON pa.partner_id = p.id + AND pa.admin_id = :adminId + AND pa.is_activated = 'ACTIVE' + WHERE pa.id IS NULL + """, nativeQuery = true) + long countUnpartneredActiveByAdmin(@Param("adminId") Long adminId); + + // 위 후보들 중에서 offset 하나만 가져오기 (랜덤 오프셋으로 1건) + @Query(value = """ + SELECT p.* + FROM partner p + LEFT JOIN paper pa + ON pa.partner_id = p.id + AND pa.admin_id = :adminId + AND pa.is_activated = 'ACTIVE' + WHERE pa.id IS NULL + LIMIT :offset, 1 + """, nativeQuery = true) + Partner findUnpartneredActiveByAdminWithOffset(@Param("adminId") Long adminId, + @Param("offset") int offset); } diff --git a/src/main/java/com/assu/server/domain/partner/service/PartnerService.java b/src/main/java/com/assu/server/domain/partner/service/PartnerService.java index 0922855..269287e 100644 --- a/src/main/java/com/assu/server/domain/partner/service/PartnerService.java +++ b/src/main/java/com/assu/server/domain/partner/service/PartnerService.java @@ -1,4 +1,9 @@ package com.assu.server.domain.partner.service; +import com.assu.server.domain.partner.dto.PartnerResponseDTO; + public interface PartnerService { + + PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(); + } diff --git a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java index 2513922..d29f408 100644 --- a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java +++ b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java @@ -1,4 +1,60 @@ package com.assu.server.domain.partner.service; -public class PartnerServiceImpl { +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.partner.dto.PartnerResponseDTO; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partner.repository.PartnerRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PartnerServiceImpl implements PartnerService { + + private final PartnerRepository partnerRepository; + private final AdminRepository adminRepository; + + @Override + public PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin() { + // Long adminId = SecurityUtil.getCurrentId(); + Long partnerId = 5L; + + Partner partner = partnerRepository.findById(partnerId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + + long total = adminRepository.countPartner(partnerId); + if (total == 0) { + throw new DatabaseException(ErrorStatus.NO_SUCH_ADMIN); + } + + int limit = (int) Math.min(2, total); + + int offset = 0; + if (total > 2) { + offset = ThreadLocalRandom.current().nextInt(0, (int)(total - limit + 1)); + } + + List picked = adminRepository.findPartnerWithOffset(partner.getId(), offset, limit); + + List admins = picked.stream() + .map(a -> PartnerResponseDTO.AdminLiteDTO.builder() + .adminId(a.getId()) + .adminAddress(a.getOfficeAddress()) + .adminDetailAddress(a.getDetailAddress()) + .adminName(a.getName()) + .build()) + .collect(Collectors.toList()); + + return PartnerResponseDTO.RandomAdminResponseDTO.builder() + .admins(admins) + .build(); + } + } 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 new file mode 100644 index 0000000..920a67d --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/controller/PartnershipController.java @@ -0,0 +1,54 @@ +package com.assu.server.domain.partnership.controller; + +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; +import com.assu.server.domain.partnership.service.PartnershipService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/partnership") +public class PartnershipController { + + private final PartnershipService partnershipService; + + @Operation( + summary = "제휴 제안서를 작성하는 API 입니다.", + description = "제공 서비스 종류(서비스 제공, 할인), 서비스 제공 기준(금액, 인원수), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성해주세요." + ) + @PostMapping("/proposal") + public BaseResponse writePartnership( + @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO + ){ + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.writePartnership(partnershipRequestDTO)); + } + + @Operation( + summary = "제휴를 조회하는 API 입니다.", + description = "전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정해주세요." + ) + @GetMapping + public BaseResponse> list( + @RequestParam(name = "all", defaultValue = "false") boolean all + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnerships(all)); + } + + @Operation( + summary = "제휴를 상세조회하는 API 입니다.", + description = "제휴 아이디를 입력하세요." + ) + @GetMapping("/{partnershipId}") + public BaseResponse getPartnership( + @PathVariable Long partnershipId + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.getPartnership(partnershipId)); + } + +} diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java b/src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java deleted file mode 100644 index 117e59c..0000000 --- a/src/main/java/com/assu/server/domain/partnership/controller/PatnershipController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.controller; - -public class PatnershipController { -} diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java new file mode 100644 index 0000000..64c0d22 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -0,0 +1,130 @@ +package com.assu.server.domain.partnership.converter; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; +import com.assu.server.domain.partnership.entity.Goods; +import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.store.entity.Store; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class PartnershipConverter { + + public static Paper toPaperEntity( + PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO, + Admin admin, + Partner partner, + Store store + + ) { + return Paper.builder() + .partnershipPeriodStart(partnershipRequestDTO.getPartnershipPeriodStart()) + .partnershipPeriodEnd(partnershipRequestDTO.getPartnershipPeriodEnd()) + .isActivated(ActivationStatus.SUSPEND) + .admin(admin) + .store(store) + .partner(partner) + .build(); + } + + public static List toPaperContents( + PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO, + Paper paper + ) { + if (partnershipRequestDTO.getOptions() == null || partnershipRequestDTO.getOptions().isEmpty()) { + return Collections.emptyList(); + } + List contents = new ArrayList<>(partnershipRequestDTO.getOptions().size()); + for (var o : partnershipRequestDTO.getOptions()) { + PaperContent content = PaperContent.builder() + .paper(paper) + .criterionType(o.getCriterionType()) + .optionType(o.getOptionType()) + .people(o.getPeople()) + .cost(o.getCost()) + .category(o.getCategory()) + .discount(o.getDiscountRate()) + .build(); + contents.add(content); + } + return contents; + } + + public static List> toGoodsBatches( + PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO + ) { + if (partnershipRequestDTO == null || partnershipRequestDTO.getOptions().isEmpty()) { + return List.of(); + } + List> batches = new ArrayList<>(partnershipRequestDTO.getOptions().size()); + for (var o : partnershipRequestDTO.getOptions()) { + if (o.getGoods() == null || o.getGoods().isEmpty()) { + batches.add(List.of()); + continue; + } + List goodsList = o.getGoods().stream() + .map(g -> Goods.builder() + .belonging(g.getGoodsName()) + .build()) + .collect(Collectors.toList()); + batches.add(goodsList); + } + return batches; + } + + public static PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipResultDTO( + Paper paper, + List contents, + List> goodsBatches + ) { + List optionDTOS = new ArrayList<>(); + int n = contents == null ? 0 : contents.size(); + for(int i = 0;i < n;i++){ + PaperContent pc = contents.get(i); + List goods = (goodsBatches != null && goodsBatches.size() > i) + ? goodsBatches.get(i) : List.of(); + optionDTOS.add(optionResultDTO(pc, goods)); + } + + return PartnershipResponseDTO.WritePartnershipResponseDTO.builder() + .partnershipId(paper.getId()) + .partnershipPeriodStart(paper.getPartnershipPeriodStart()) + .partnershipPeriodEnd(paper.getPartnershipPeriodEnd()) + .adminId(paper.getAdmin() == null ? null : paper.getAdmin().getId()) + .partnerId(paper.getStore() == null ? null : paper.getPartner().getId()) + .storeId(paper.getStore() == null ? null : paper.getStore().getId()) + .options(optionDTOS) + .build(); + } + + public static PartnershipResponseDTO.PartnershipOptionResponseDTO optionResultDTO( + PaperContent pc, List goods + ) { + return PartnershipResponseDTO.PartnershipOptionResponseDTO.builder() + .optionType(pc.getOptionType()) + .criterionType(pc.getCriterionType()) + .people(pc.getPeople()) + .cost(pc.getCost()) + .category(pc.getCategory()) + .discountRate(pc.getDiscount()) + .goods(goodsResultDTO(goods)) + .build(); + } + + public static List goodsResultDTO(List goods) { + if(goods == null || goods.isEmpty()) return List.of(); + return goods.stream() + .map(g -> PartnershipResponseDTO.PartnershipGoodsResponseDTO.builder() + .goodsId(g.getId()) + .goodsName(g.getBelonging()) + .build()) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java deleted file mode 100644 index 9f9cbc5..0000000 --- a/src/main/java/com/assu/server/domain/partnership/converter/PatnershipConverter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.converter; - -public class PatnershipConverter { -} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java new file mode 100644 index 0000000..d764af3 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java @@ -0,0 +1,39 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; +import lombok.Getter; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public class PartnershipRequestDTO { + + @Getter + public static class WritePartnershipRequestDTO { + private LocalDate partnershipPeriodStart; + private LocalDate partnershipPeriodEnd; + private Long adminId; // 제안 학생회 아이디 + private Long partnerId; // 제안자 아이디 + private Long storeId; // 제안 가게 아이디 + private List options; // 동적으로 받는 제안 항목 + } + + @Getter + public static class PartnershipOptionRequestDTO { + private OptionType optionType; // 제공 서비스 종류 (서비스 제공, 할인) + private CriterionType criterionType; // 서비스 제공 기준 (금액, 인원) + private Integer people; + private Long cost; + private String category; + private Long discountRate; + private List goods; // 서비스 제공 항목 + + } + + @Getter + public static class PartnershipGoodsRequestDTO { + private String goodsName; + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java new file mode 100644 index 0000000..a869b0e --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java @@ -0,0 +1,51 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; +import lombok.*; + +import java.time.LocalDate; +import java.util.List; + +public class PartnershipResponseDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class WritePartnershipResponseDTO { + private Long partnershipId; + private LocalDate partnershipPeriodStart; + private LocalDate partnershipPeriodEnd; + private Long adminId; + private Long partnerId; + private Long storeId; + private List options; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class PartnershipOptionResponseDTO { + private OptionType optionType; + private CriterionType criterionType; + private Integer people; + private Long cost; + private String category; + private Long discountRate; + + private List goods; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class PartnershipGoodsResponseDTO { + private Long goodsId; + private String goodsName; + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PatnershipRequestDTO.java deleted file mode 100644 index 99e4c2b..0000000 --- a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipRequestDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.dto; - -public class PatnershipRequestDTO { -} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PatnershipResponseDTO.java deleted file mode 100644 index 4c6f5e0..0000000 --- a/src/main/java/com/assu/server/domain/partnership/dto/PatnershipResponseDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.dto; - -public class PatnershipResponseDTO { -} diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Goods.java b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java new file mode 100644 index 0000000..7db9812 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java @@ -0,0 +1,24 @@ +package com.assu.server.domain.partnership.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class Goods { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id") + private PaperContent content; + + private String belonging; + +} diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java index faeb633..9ac9689 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java @@ -5,20 +5,16 @@ import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.store.entity.Store; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @NoArgsConstructor @@ -29,12 +25,12 @@ public class Paper extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String partnershipPeriod; // 이게 뭘로 들어오는거지. 그냥 LocalDate 로 하는게 낫지 않나? + private LocalDate partnershipPeriodStart; // LocalDate vs String + private LocalDate partnershipPeriodEnd; @Enumerated(EnumType.STRING) private ActivationStatus isActivated; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "admin_id") private Admin admin; diff --git a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java index 29e3195..8af76bf 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java @@ -1,22 +1,17 @@ package com.assu.server.domain.partnership.entity; import com.assu.server.domain.common.entity.BaseEntity; -import com.assu.server.domain.partnership.entity.enums.PaperContentType; -import com.assu.server.domain.user.entity.enums.Major; - -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; + +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @@ -33,19 +28,20 @@ public class PaperContent extends BaseEntity { private Paper paper; @Enumerated(EnumType.STRING) - private PaperContentType type; + private CriterionType criterionType; - private Integer people; + @Enumerated(EnumType.STRING) + private OptionType optionType; - private String belonging; + private Integer people; private Long cost; - private Long discount; + private String category; - private String goods; + private Long discount; - @Enumerated(EnumType.STRING) - private Major major; + @OneToMany(mappedBy = "content", cascade = CascadeType.ALL, orphanRemoval = true) + private List goods = new ArrayList<>(); } diff --git a/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java b/src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java similarity index 50% rename from src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java rename to src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java index 80f7023..bf77b82 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java @@ -1,5 +1,5 @@ package com.assu.server.domain.partnership.entity.enums; -public enum PaperContentType{ - PEOPLE, BELONGING, COST -} \ No newline at end of file +public enum CriterionType { + PRICE, HEADCOUNT +} diff --git a/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java b/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java new file mode 100644 index 0000000..1799b04 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java @@ -0,0 +1,5 @@ +package com.assu.server.domain.partnership.entity.enums; + +public enum OptionType { + SERVICE, DISCOUNT +} diff --git a/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java new file mode 100644 index 0000000..bede045 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java @@ -0,0 +1,7 @@ +package com.assu.server.domain.partnership.repository; + +import com.assu.server.domain.partnership.entity.Goods; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface GoodsRepository extends JpaRepository { +} diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java new file mode 100644 index 0000000..095d439 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java @@ -0,0 +1,27 @@ +package com.assu.server.domain.partnership.repository; + +import com.assu.server.domain.partnership.entity.PaperContent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface PaperContentRepository extends JpaRepository { + + @Query(""" + select distinct pc + from PaperContent pc + left join fetch pc.goods g + where pc.paper.id in :paperIds + """) + List findAllByPaperIdInFetchGoods(@Param("paperIds") List paperIds); + + @Query(""" + select distinct pc + from PaperContent pc + left join fetch pc.goods g + where pc.paper.id in :paperIds + """) + List findAllByOnePaperIdInFetchGoods(@Param("paperIds") Long paperIds); +} diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java new file mode 100644 index 0000000..120c192 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -0,0 +1,7 @@ +package com.assu.server.domain.partnership.repository; + +import com.assu.server.domain.partnership.entity.Paper; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaperRepository extends JpaRepository { +} diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java deleted file mode 100644 index 7971fb1..0000000 --- a/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.repository; - -public class PatnershipRepository { -} 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 new file mode 100644 index 0000000..7012635 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipService.java @@ -0,0 +1,18 @@ +package com.assu.server.domain.partnership.service; + +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; +import io.swagger.v3.oas.annotations.parameters.RequestBody; + +import java.util.List; + +public interface PartnershipService { + + PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership( + @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request + ); + + List listPartnerships(boolean all); + + PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId); +} 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 new file mode 100644 index 0000000..4f3affe --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java @@ -0,0 +1,131 @@ +package com.assu.server.domain.partnership.service; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partner.repository.PartnerRepository; +import com.assu.server.domain.partnership.converter.PartnershipConverter; +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; +import com.assu.server.domain.partnership.entity.Goods; +import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.repository.GoodsRepository; +import com.assu.server.domain.partnership.repository.PaperContentRepository; +import com.assu.server.domain.partnership.repository.PaperRepository; +import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.store.repository.StoreRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.exception.DatabaseException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PartnershipServiceImpl implements PartnershipService { + + private final PaperRepository paperRepository; + private final PaperContentRepository paperContentRepository; + private final GoodsRepository goodsRepository; + + private final AdminRepository adminRepository; + private final PartnerRepository partnerRepository; + private final StoreRepository storeRepository; + + @Override + public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(PartnershipRequestDTO.WritePartnershipRequestDTO request) { + + Admin admin = adminRepository.findById(request.getAdminId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + Partner partner = partnerRepository.findById(request.getPartnerId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + Store store = storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + + Paper paper = PartnershipConverter.toPaperEntity(request, admin, partner, store); + paper = paperRepository.save(paper); + + List contents = PartnershipConverter.toPaperContents(request, paper); + contents = contents.isEmpty() ? contents : paperContentRepository.saveAll(contents); + + List> requestGoodsBatches = PartnershipConverter.toGoodsBatches(request); + + List> attachedGoodsBatches = new ArrayList<>(); + List toPersist = new ArrayList<>(); + + for(int i = 0;i < contents.size();i++){ + PaperContent content = contents.get(i); + List batch = (requestGoodsBatches.size() > i) ? requestGoodsBatches.get(i) : Collections.emptyList(); + + List attached = new ArrayList<>(batch.size()); + for(Goods g : batch){ + Goods entity = Goods.builder() + .content(content) + .belonging(g.getBelonging()) + .build(); + attached.add(entity); + toPersist.add(entity); + } + attachedGoodsBatches.add(attached); + } + + if(!toPersist.isEmpty()){ + goodsRepository.saveAll(toPersist); + } + + return PartnershipConverter.writePartnershipResultDTO(paper, contents, attachedGoodsBatches); + } + + @Override + public List listPartnerships(boolean all) { + Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); + List papers; + + if (all) { + papers = paperRepository.findAll(sort); + } else { + papers = paperRepository.findAll(PageRequest.of(0, 2, sort)).getContent(); + } + if (papers.isEmpty()) return List.of(); + + List paperIds = papers.stream().map(Paper::getId).toList(); + List allContents = paperContentRepository.findAllByPaperIdInFetchGoods(paperIds); + + Map> byPaperId = allContents.stream() + .collect(Collectors.groupingBy(pc -> pc.getPaper().getId())); + + List result = new ArrayList<>(papers.size()); + for (Paper p : papers) { + List contents = byPaperId.getOrDefault(p.getId(), List.of()); + List> goodsBatches = contents.stream() + .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods()) + .toList(); + result.add(PartnershipConverter.writePartnershipResultDTO(p, contents, goodsBatches)); + } + return result; + } + + @Override + public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId) { + Paper paper = paperRepository.findById(partnershipId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); + + List contents = paperContentRepository.findAllByOnePaperIdInFetchGoods(partnershipId); + + List> goodsBatches = contents.stream() + .map(pc -> pc.getGoods() == null ? Collections.emptyList() : pc.getGoods()) + .toList(); + + return PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java b/src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java deleted file mode 100644 index 1437bad..0000000 --- a/src/main/java/com/assu/server/domain/partnership/service/PatnershipService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.service; - -public interface PatnershipService { -} diff --git a/src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java deleted file mode 100644 index 80c89ab..0000000 --- a/src/main/java/com/assu/server/domain/partnership/service/PatnershipServiceImpl.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partnership.service; - -public class PatnershipServiceImpl { -} diff --git a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java index 50ccb9c..dbac01c 100644 --- a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java +++ b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java @@ -7,10 +7,9 @@ import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -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 org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController @RequiredArgsConstructor // 파라미터가 있어야만 하는 생성자 @@ -29,4 +28,13 @@ public BaseResponse writeSugge ){ return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO)); } + + @Operation( + summary = "제휴 건의를 조회하는 API 입니다.", + description = "모든 제휴 건의를 조회합니다." + ) + @GetMapping("/list") + public BaseResponse> getSuggestions() { + return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.getSuggestions()); + } } diff --git a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java index bb53db5..8a8607c 100644 --- a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java +++ b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java @@ -7,6 +7,9 @@ import com.assu.server.domain.suggestion.entity.Suggestion; import com.assu.server.domain.user.entity.Student; +import java.util.List; +import java.util.stream.Collectors; + public class SuggestionConverter { public static SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestionResultDTO(Suggestion suggestion){ @@ -29,4 +32,23 @@ public static Suggestion toSuggestionEntity(SuggestionRequestDTO.WriteSuggestion .content(suggestionRequestDTO.getBenefit()) .build(); } + + public static SuggestionResponseDTO.GetSuggestionResponseDTO GetSuggestionResultDTO(Suggestion s){ + + Student student = s.getStudent(); + return SuggestionResponseDTO.GetSuggestionResponseDTO.builder() + .suggestionId(s.getId()) + .createdAt(s.getCreatedAt()) + .content(s.getContent()) + .studentNumber(student.getStudentNumber()) + .enrollmentStatus(student.getEnrollmentStatus()) + .studentMajor(student.getMajor()) + .build(); + } + + public static List toGetSuggestionDTOList(List list) { + return list.stream() + .map(SuggestionConverter::GetSuggestionResultDTO) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java index bdf11eb..410462d 100644 --- a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java +++ b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionRequestDTO.java @@ -7,7 +7,6 @@ public class SuggestionRequestDTO { @Getter public static class WriteSuggestionRequestDTO{ private Long adminId; // 건의 대상 - private Long studentId; // 건의자 아이디 private String storeName; // 희망 가게 private String benefit; // 희망 혜택 } diff --git a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java index 08f47cc..51de446 100644 --- a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java +++ b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java @@ -1,23 +1,40 @@ -package com.assu.server.domain.suggestion.dto; + package com.assu.server.domain.suggestion.dto; -import com.assu.server.domain.admin.entity.Admin; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; + import com.assu.server.domain.admin.entity.Admin; + import com.assu.server.domain.user.entity.enums.EnrollmentStatus; + import com.assu.server.domain.user.entity.enums.Major; + import lombok.AllArgsConstructor; + import lombok.Builder; + import lombok.Getter; + import lombok.NoArgsConstructor; -public class SuggestionResponseDTO { + import java.time.LocalDateTime; - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class WriteSuggestionResponseDTO { - private Long suggestionId; // 제휴 번호 - private Long memberId; // 제안인 아이디 - private Long studentNumber; // 제안인 학번 - private Long suggestionSubjectId; // 건의 대상 아이디 - private String suggestionStore; // 희망 가게 이름 - private String suggestionBenefit; // 희망 혜택 + public class SuggestionResponseDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class WriteSuggestionResponseDTO { + private Long suggestionId; // 제안 번호 + private Long memberId; // 제안인 아이디 + private Long studentNumber; // 제안인 학번 + private Long suggestionSubjectId; // 건의 대상 아이디 + private String suggestionStore; // 희망 가게 이름 + private String suggestionBenefit; // 희망 혜택 + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class GetSuggestionResponseDTO { + private Long suggestionId; + private LocalDateTime createdAt; + private String content; + private Major studentMajor; + private Long studentNumber; + private EnrollmentStatus enrollmentStatus; + } } -} diff --git a/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java b/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java index 9239b54..b712598 100644 --- a/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java +++ b/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java @@ -2,7 +2,19 @@ import com.assu.server.domain.suggestion.entity.Suggestion; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface SuggestionRepository extends JpaRepository { + @Query(""" + select s + from Suggestion s + join fetch s.student st + where s.admin.id = :adminId + order by s.createdAt desc + """) + List findAllSuggestions(@Param("adminId") Long adminId); } diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java index 188ccfe..b8e710f 100644 --- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java +++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java @@ -1,14 +1,21 @@ package com.assu.server.domain.suggestion.service; +import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO; import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO; import com.assu.server.domain.suggestion.entity.Suggestion; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import java.util.List; + public interface SuggestionService { SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion( @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO request ); + + List getSuggestions(); + } diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java index c2eac45..a4b72f6 100644 --- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java +++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java @@ -2,6 +2,13 @@ import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.partnership.converter.PartnershipConverter; +import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; +import com.assu.server.domain.partnership.entity.Goods; +import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.repository.PaperContentRepository; +import com.assu.server.domain.partnership.repository.PaperRepository; import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.suggestion.converter.SuggestionConverter; import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO; @@ -15,6 +22,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Collections; +import java.util.List; + @Service @RequiredArgsConstructor public class SuggestionServiceImpl implements SuggestionService { @@ -26,16 +36,27 @@ public class SuggestionServiceImpl implements SuggestionService { @Override public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(SuggestionRequestDTO.WriteSuggestionRequestDTO request) { // Long memberId = SecurityUtil.getCurrentUserId; - Long memberId = 1L; + Long memberId = 9L; Admin admin = adminRepository.findById(request.getAdminId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); Student student = studentRepository.findById(memberId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT)); Suggestion suggestion = SuggestionConverter.toSuggestionEntity(request, admin, student); suggestionRepository.save(suggestion); return SuggestionConverter.writeSuggestionResultDTO(suggestion); } + + @Override + public List getSuggestions() { + // Long adminId = SecurityUtil.getCurrentUserId(); + Long adminId = 1L; + + List list = suggestionRepository + .findAllSuggestions(adminId); + + return SuggestionConverter.toGetSuggestionDTOList(list); + } } diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index c7b4ed5..fa37249 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -4,6 +4,7 @@ import com.assu.server.global.apiPayload.code.ErrorReasonDTO; import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; import org.springframework.http.HttpStatus; @Getter @@ -24,6 +25,42 @@ public enum ErrorStatus implements BaseErrorCode { // 어드민 에러 NO_SUCH_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_5001", "존재하지 않는 학생회입니다."), + // 파트너 에러 + NO_SUCH_PARTNER(HttpStatus.NOT_FOUND, "PARTNER_5003", "존재하지 않는 파트너입니다."), + + // 학생 에러 + NO_SUCH_STUDENT(HttpStatus.NOT_FOUND, "STUDENT_5004", "존재하지 않는 학생입니다."), + + // 스토어 에러 + NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_6001", "존재하지 않는 가게입니다."), + + // 혜택 없음 에러 + OPTION_NOT_EMPTY(HttpStatus.BAD_REQUEST, "OPTION_7001", "혜택은 한 가지 이상이어야 합니다."), + + // 벨류(금액, 인원수) 에러 + VALUE_IS_REQUIRED(HttpStatus.NOT_FOUND, "VALUE_8001", "값을 알 수 없습니다."), + + // 서비스 아이템 에러 + SERVICE_ITEM_REQUIRED(HttpStatus.NOT_FOUND, "SERVICE_ITEM_9001", "서비스 품목은 한 가지 이상이어야 합니다."), + + // 카테고리 에러 + CATEGORY_REQUIRED(HttpStatus.NOT_FOUND, "CATEGORY_10001", "품목에 대한 카테고리가 설정되어야 합니다."), + + // 할인율 에러 + DISCOUNT_RATE_REQUIRED(HttpStatus.NOT_FOUND, "DISCOUNT_11001", "할인율 값을 알 수 없습니다."), + + // 혜택 타입 에러 + UNSUPPORTED_OPTION_TYPE(HttpStatus.NOT_FOUND, "OPTION_7002", "지원하지 않는 혜택 항목입니다."), + + // 제휴 아이디 에러 + NO_SUCH_PAPER(HttpStatus.NOT_FOUND, "PAPER_12001", "존재하지 않는 제휴입니다."), + + // 어드민 찾기 에러 + NO_AVAILABLE_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_5002", "제휴단체를 찾을 수 없습니다."), + + // 파트너 찾기 에러 + NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "PARTNER_5502", "제휴업체를 찾을 수 없습니다."), + ; private final HttpStatus httpStatus; From cda83b7f0e9f19c758c6e2bb6b79d7a13793d12d Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Wed, 13 Aug 2025 18:04:04 +0900 Subject: [PATCH 046/270] =?UTF-8?q?[Feat/#14]=20=20-=20=EA=B1=B4=EC=9D=98?= =?UTF-8?q?=EB=90=9C=20=EC=A0=9C=ED=9C=B4=20=EB=AA=A8=EB=91=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=20-=20=EC=A0=9C=ED=9C=B4=20=EC=A0=9C=EC=95=88?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EC=84=B1=20=20-=20=EC=A0=9C=ED=9C=B4=20?= =?UTF-8?q?=EC=A0=9C=EC=95=88=EC=84=9C=20=EC=83=81=EC=84=B8=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=20-=20=EC=A0=9C=EC=95=88=EB=90=9C=20=EC=A0=9C?= =?UTF-8?q?=ED=9C=B4=20=EC=A0=84=EC=B2=B4/=EC=9D=BC=EB=B6=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=20-=20=EC=A0=9C=ED=9C=B4=EB=8B=A8=EC=B2=B4=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=20-=20=EC=A0=9C=ED=9C=B4=EC=97=85?= =?UTF-8?q?=EC=B2=B4=20=EC=B6=94=EC=B2=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/assu/server/domain/admin/controller/AdminController.java | 1 - .../assu/server/domain/partner/controller/PartnerController.java | 1 - .../com/assu/server/domain/partner/dto/PartnerResponseDTO.java | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java index c41f470..e51c526 100644 --- a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java +++ b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java @@ -6,7 +6,6 @@ import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -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; diff --git a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java index 697a9e0..282acb8 100644 --- a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java +++ b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java @@ -6,7 +6,6 @@ import com.assu.server.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController diff --git a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java index c87fff8..517e8c5 100644 --- a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java @@ -1,6 +1,5 @@ package com.assu.server.domain.partner.dto; -import com.assu.server.domain.admin.entity.Admin; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; From 8272336d70f94da6a4921608c5b7aef61e7c5852 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Wed, 13 Aug 2025 17:20:18 +0900 Subject: [PATCH 047/270] =?UTF-8?q?feat/#13-review=20=20-=20=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20=EA=B0=80=EC=9E=85=EC=9E=90=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudentAdminController.java | 10 +++++++++- .../converter/StudentAdminConverter.java | 7 +++++++ .../mapping/dto/StudentAdminResponseDTO.java | 9 +++++++++ .../domain/mapping/entity/StudentAdmin.java | 3 ++- .../repository/StudentAdminRepository.java | 19 +++++++++++++++++++ .../mapping/service/StudentAdminService.java | 1 + .../service/StudentAdminServiceImpl.java | 12 +++++++++++- 7 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java index bb32dd7..d3b8a55 100644 --- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java +++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java @@ -16,12 +16,20 @@ public class StudentAdminController { private final StudentAdminService studentAdminService; @Operation( - summary = "누적 가입수 조회 API입니다.", + summary = "누적 가입자 수 조회 API 입니다.", description = "admin으로 접근해주세요." ) @GetMapping public BaseResponse getCountAdmin() { return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth()); } + @Operation( + summary = "신규 한 달 가입자 수 조회 API 입니다.", + description = "admin으로 접근해주세요." + ) + @GetMapping("/new") + public BaseResponse getNewStudentCountAdmin(){ + return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getNewStudentCountAdmin()); + } } diff --git a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java index abff2bd..0bb6ed2 100644 --- a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java +++ b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java @@ -12,4 +12,11 @@ public static StudentAdminResponseDTO.CountAdminAuthResponseDTO countAdminAuthDT .build(); } + public static StudentAdminResponseDTO.NewCountAdminResponseDTO newCountAdminResponseDTO(Long adminId, Long total, String adminName){ + return StudentAdminResponseDTO.NewCountAdminResponseDTO.builder() + .adminId(adminId) + .newStudentCount(total) + .adminName(adminName) + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java index fa042ac..fe49519 100644 --- a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java +++ b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java @@ -16,4 +16,13 @@ public static class CountAdminAuthResponseDTO{ // admin에 따른 총 누적 가 private Long adminId; private String adminName; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class NewCountAdminResponseDTO{ //신규 가입자수 (매달 1일 초기화) + private Long newStudentCount; + private Long adminId; + private String adminName; + } } diff --git a/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java b/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java index 97770d8..c0bc842 100644 --- a/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java +++ b/src/main/java/com/assu/server/domain/mapping/entity/StudentAdmin.java @@ -1,5 +1,6 @@ package com.assu.server.domain.mapping.entity; +import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.user.entity.Student; import com.assu.server.domain.admin.entity.Admin; import jakarta.persistence.*; @@ -11,7 +12,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder -public class StudentAdmin { +public class StudentAdmin extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java index d77a64e..84e0165 100644 --- a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java +++ b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; +import java.time.YearMonth; import java.util.Collection; public interface StudentAdminRepository extends JpaRepository { @@ -15,4 +16,22 @@ select count(sa) where sa.admin.id = :adminId """) Long countAllByAdminId(@Param("adminId") Long adminId); + + + @Query(""" + select count(sa) + from StudentAdmin sa + where sa.admin.id = :adminId + and sa.createdAt >= :from + and sa.createdAt < :to + """) + Long countByAdminIdBetween(@Param("adminId") Long adminId, + @Param("from") LocalDateTime from, + @Param("to") LocalDateTime to); + + default Long countThisMonthByAdminId(Long adminId) { + LocalDateTime from = YearMonth.now().atDay(1).atStartOfDay(); + LocalDateTime to = LocalDateTime.now(); + return countByAdminIdBetween(adminId, from, to); + } } diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java index 3639fcc..3faafd8 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java @@ -4,4 +4,5 @@ public interface StudentAdminService { StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(); + StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(); } diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java index 42833b0..8aff9d5 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java @@ -31,5 +31,15 @@ public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth() { return StudentAdminConverter.countAdminAuthDTO(memberId, total, adminName); } - + @Override + @Transactional + public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin() { + //Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 5L; + Long total = studentAdminRepository.countThisMonthByAdminId(memberId); + Admin admin = adminRepository.findById(memberId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + String adminName = admin.getName(); + return StudentAdminConverter.newCountAdminResponseDTO(memberId, total, adminName); + } } From f08b70e2e158b529ffb3b49b3341d24874336734 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Wed, 13 Aug 2025 18:48:11 +0900 Subject: [PATCH 048/270] =?UTF-8?q?feat/#13-review=20=20-=20=20=EC=98=A4?= =?UTF-8?q?=EB=8A=98=20=EC=A0=9C=ED=9C=B4=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=88=98=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/admin/entity/Admin.java | 6 +---- .../admin/repository/AdminRepository.java | 5 ++++- .../domain/common/entity/AdminUser.java | 22 +++++++++++++++++++ .../controller/StudentAdminController.java | 8 +++++++ .../converter/StudentAdminConverter.java | 9 ++++++++ .../mapping/dto/StudentAdminResponseDTO.java | 10 +++++++++ .../repository/StudentAdminRepository.java | 12 ++++++++++ .../mapping/service/StudentAdminService.java | 1 + .../service/StudentAdminServiceImpl.java | 13 +++++++++++ .../domain/user/entity/PartnershipUsage.java | 3 +-- src/test/resources/application-secret.yml | 0 11 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/common/entity/AdminUser.java delete mode 100644 src/test/resources/application-secret.yml diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java index 271200b..8cf3257 100644 --- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java +++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java @@ -1,11 +1,7 @@ package com.assu.server.domain.admin.entity; import com.assu.server.domain.common.entity.Member; -import jakarta.persistence.Entity; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.MapsId; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java index 4e6b1fa..f84a9e0 100644 --- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java +++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java @@ -1,4 +1,7 @@ package com.assu.server.domain.admin.repository; -public class AdminRepository { +import com.assu.server.domain.admin.entity.Admin; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminRepository extends JpaRepository { } diff --git a/src/main/java/com/assu/server/domain/common/entity/AdminUser.java b/src/main/java/com/assu/server/domain/common/entity/AdminUser.java new file mode 100644 index 0000000..3b5a5a6 --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/entity/AdminUser.java @@ -0,0 +1,22 @@ +package com.assu.server.domain.common.entity; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.user.entity.Student; +import jakarta.persistence.*; + +@Entity +public class AdminUser extends BaseEntity{ + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "admin_id") + private Admin admin; + + @ManyToOne + @JoinColumn(name = "student_id") + private Student student; + +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java index d3b8a55..9bac60d 100644 --- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java +++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java @@ -32,4 +32,12 @@ public BaseResponse getNewStud return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getNewStudentCountAdmin()); } + @Operation( + summary = "오늘 제휴 사용자 수 조회 API 입니다.", + description = "admin으로 접근해주세요." + ) + @GetMapping("/countUser") + public BaseResponse getCountUser(){ + return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsagePerson()); + } } diff --git a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java index 0bb6ed2..d5be4fe 100644 --- a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java +++ b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java @@ -19,4 +19,13 @@ public static StudentAdminResponseDTO.NewCountAdminResponseDTO newCountAdminResp .adminName(adminName) .build(); } + + public static StudentAdminResponseDTO.CountUsagePersonResponseDTO countUsagePersonDTO(Long adminId, Long total, String adminName){ + return StudentAdminResponseDTO.CountUsagePersonResponseDTO.builder() + .adminId(adminId) + .usagePersonCount(total) + .adminName(adminName) + .build(); + } + } diff --git a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java index fe49519..77fef52 100644 --- a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java +++ b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java @@ -25,4 +25,14 @@ public static class NewCountAdminResponseDTO{ //신규 가입자수 (매달 1일 private Long adminId; private String adminName; } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CountUsagePersonResponseDTO{ + private Long usagePersonCount; + private Long adminId; + private String adminName; + } } diff --git a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java index 84e0165..9bdf471 100644 --- a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java +++ b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.util.Collection; @@ -34,4 +35,15 @@ default Long countThisMonthByAdminId(Long adminId) { LocalDateTime to = LocalDateTime.now(); return countByAdminIdBetween(adminId, from, to); } + // 오늘 하루, '나를 admin으로 제휴 맺은 partner'의 제휴를 사용한 '고유 사용자 수' + @Query(value = """ + SELECT COUNT(DISTINCT pu.student_id) + FROM partnership_usage pu + JOIN paper_content pc ON pc.id = pu.paper_id + JOIN paper p ON p.id = pc.paper_id + WHERE p.admin_id = :adminId + AND pu.created_at >= CURRENT_DATE + AND pu.created_at < CURRENT_DATE + INTERVAL 1 DAY + """, nativeQuery = true) + Long countTodayUsersByAdmin(@Param("adminId") Long adminId); } diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java index 3faafd8..55a1757 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java @@ -5,4 +5,5 @@ public interface StudentAdminService { StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(); StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(); + StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson(); } diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java index 8aff9d5..c81db61 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java @@ -42,4 +42,17 @@ public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin( String adminName = admin.getName(); return StudentAdminConverter.newCountAdminResponseDTO(memberId, total, adminName); } + + @Override + @Transactional + public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson() { + //Long memberId = SecurityUtil.getCurrentUserId; + Long memberId = 5L; + Long total = studentAdminRepository.countTodayUsersByAdmin(memberId); + Admin admin = adminRepository.findById(memberId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + String adminName =admin.getName(); + return StudentAdminConverter.countUsagePersonDTO(memberId, total, adminName); + } + } diff --git a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java index 85959cf..96d8e7a 100644 --- a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java +++ b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java @@ -35,6 +35,5 @@ public class PartnershipUsage extends BaseEntity { private String partnershipContent; private Boolean isReviewed; private Integer discount; - - + private Long paperId; } \ No newline at end of file diff --git a/src/test/resources/application-secret.yml b/src/test/resources/application-secret.yml deleted file mode 100644 index e69de29..0000000 From 826db4d24d427bb2c0684185788f83f892a01654 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Thu, 14 Aug 2025 09:21:41 +0900 Subject: [PATCH 049/270] =?UTF-8?q?[FEAT/#21]=20=EB=82=98=EC=9D=98=20?= =?UTF-8?q?=EC=A0=9C=ED=9C=B4=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/store/service/StoreService.java | 4 ++ .../store/service/StoreServiceImpl.java | 51 +++++++++++++++++++ .../user/controller/StudentController.java | 25 +++++++++ .../domain/user/dto/StudentResponseDTO.java | 27 ++++++++++ .../PartnershipUsageRepository.java | 9 ++++ 5 files changed, 116 insertions(+) diff --git a/src/main/java/com/assu/server/domain/store/service/StoreService.java b/src/main/java/com/assu/server/domain/store/service/StoreService.java index e447492..efcd08b 100644 --- a/src/main/java/com/assu/server/domain/store/service/StoreService.java +++ b/src/main/java/com/assu/server/domain/store/service/StoreService.java @@ -1,7 +1,11 @@ package com.assu.server.domain.store.service; +import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; import com.assu.server.domain.store.dto.StoreResponseDTO; +import com.assu.server.domain.user.dto.StudentResponseDTO; public interface StoreService { StoreResponseDTO.todayBest getTodayBestStore(); + + StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month); } diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java index e807872..28b9be2 100644 --- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java +++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java @@ -1,11 +1,22 @@ package com.assu.server.domain.store.service; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; +import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.entity.enums.OptionType; +import com.assu.server.domain.partnership.repository.PaperContentRepository; import com.assu.server.domain.store.dto.StoreResponseDTO; import com.assu.server.domain.store.repository.StoreRepository; +import com.assu.server.domain.user.dto.StudentResponseDTO; +import com.assu.server.domain.user.entity.PartnershipUsage; import com.assu.server.domain.user.repository.PartnershipUsageRepository; import jakarta.transaction.Transactional; @@ -16,6 +27,7 @@ public class StoreServiceImpl implements StoreService{ private PartnershipUsageRepository partnershipUsageRepository; + private PaperContentRepository paperContentRepository; @Override @Transactional @@ -27,4 +39,43 @@ public StoreResponseDTO.todayBest getTodayBestStore() { .build(); } + @Override + @Transactional + public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) { + List usages = partnershipUsageRepository.findByStudentAndCreatedAtMonth(studentId, year, month); + + Set contentIds = usages.stream() + .map(PartnershipUsage::getContentId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map contentMap = paperContentRepository.findAllById(contentIds) + .stream() + .collect(Collectors.toMap(PaperContent::getId, Function.identity())); + + long serviceCount = 0; + int totalDiscount = 0; + List usageDetails = new ArrayList<>(); + + for (PartnershipUsage usage : usages) { + PaperContent content = contentMap.get(usage.getContentId()); + if (content == null) continue; + + String desc; + if (content.getOptionType() == OptionType.SERVICE) { + serviceCount++; + desc = String.format("%s에서 %d명 서비스 제공받았어요!", content.getCategory(), content.getPeople()); + } else { + int discount = usage.getDiscount() != null ? usage.getDiscount() : 0; + totalDiscount += discount; + desc = String.format("%,d원 할인 혜택을 받았어요!", discount); + } + + usageDetails.add(new StudentResponseDTO.UsageDetailDTO( + usage.getPlace(), + usage.getDate(), + desc + ));} + } + } diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java index 303f234..463dac3 100644 --- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java +++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java @@ -1,4 +1,29 @@ package com.assu.server.domain.user.controller; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import com.assu.server.domain.user.dto.StudentResponseDTO; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.util.PrincipalDetails; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "유저 관련 api", description = "유저와 관련된 로직을 처리하는 api") +@RequiredArgsConstructor public class StudentController { + + @GetMapping("/partnership/{year}/{month}") + @Operation(summary = "유저의 제휴 내역을 조회", description = "건수 및 금액으로 조회") + public ResponseEntity> getMyPartnership( + @PathVariable int year, @PathVariable int month, @AuthenticationPrincipal PrincipalDetails userDetails + ){ + + } } diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java index 1ebaae2..33577dd 100644 --- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java @@ -1,4 +1,31 @@ package com.assu.server.domain.user.dto; +import java.time.LocalDateTime; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + public class StudentResponseDTO { + + @Getter + @Builder + @AllArgsConstructor + @RequiredArgsConstructor + public static class myPartnership { + private long serviceCount; // SERVICE 개수 + private int totalDiscount; // DISCOUNT 총액 + private List usageDetails; + } + + @Getter + @AllArgsConstructor + public static class UsageDetailDTO { + private String storeName; + private LocalDateTime usedAt; + private String benefitDescription; + private boolean isReviewed; + } } diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java index 075d1eb..4d5edb2 100644 --- a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java @@ -19,4 +19,13 @@ ORDER BY COUNT(*) DESC LIMIT 10 """, nativeQuery = true) List findTodayPopularPartnership(); + + @Query(""" + SELECT pu + FROM PartnershipUsage pu + WHERE pu.student.id = :studentId + AND YEAR(pu.createdAt) = :year + AND MONTH(pu.createdAt) = :month +""") + List myPartnershipByYearAndMonth(); } From bc66988486e6a7581478a9165855ac3e47f4f4f4 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Thu, 14 Aug 2025 13:51:51 +0900 Subject: [PATCH 050/270] =?UTF-8?q?[FEAT/#21]=20=EC=9E=84=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 -- .../store/service/StoreServiceImpl.java | 69 ++++++++++--------- .../user/controller/StudentController.java | 2 +- .../server/global/config/SecurityConfig.java | 37 ++++++++++ src/main/resources/application.yml | 11 +++ 5 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/assu/server/global/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index cbfbe06..dfeb49c 100644 --- a/build.gradle +++ b/build.gradle @@ -62,13 +62,6 @@ dependencies { // WebSocket 기본 기능 implementation 'org.springframework.boot:spring-boot-starter-websocket' - implementation 'org.springframework:spring-messaging' - - // STOMP 메세징 지원 - implementation 'org.springframework.boot:spring-boot-starter-web' - - // JSON 처리 - implementation 'com.fasterxml.jackson.core:jackson-databind' } tasks.named('test') { diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java index 28b9be2..eaa90d9 100644 --- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java +++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java @@ -42,40 +42,41 @@ public StoreResponseDTO.todayBest getTodayBestStore() { @Override @Transactional public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) { - List usages = partnershipUsageRepository.findByStudentAndCreatedAtMonth(studentId, year, month); - - Set contentIds = usages.stream() - .map(PartnershipUsage::getContentId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - Map contentMap = paperContentRepository.findAllById(contentIds) - .stream() - .collect(Collectors.toMap(PaperContent::getId, Function.identity())); - - long serviceCount = 0; - int totalDiscount = 0; - List usageDetails = new ArrayList<>(); - - for (PartnershipUsage usage : usages) { - PaperContent content = contentMap.get(usage.getContentId()); - if (content == null) continue; - - String desc; - if (content.getOptionType() == OptionType.SERVICE) { - serviceCount++; - desc = String.format("%s에서 %d명 서비스 제공받았어요!", content.getCategory(), content.getPeople()); - } else { - int discount = usage.getDiscount() != null ? usage.getDiscount() : 0; - totalDiscount += discount; - desc = String.format("%,d원 할인 혜택을 받았어요!", discount); - } - - usageDetails.add(new StudentResponseDTO.UsageDetailDTO( - usage.getPlace(), - usage.getDate(), - desc - ));} + // List usages = partnershipUsageRepository.findByStudentAndCreatedAtMonth(studentId, year, month); + // + // Set contentIds = usages.stream() + // .map(PartnershipUsage::getContentId) + // .filter(Objects::nonNull) + // .collect(Collectors.toSet()); + // + // Map contentMap = paperContentRepository.findAllById(contentIds) + // .stream() + // .collect(Collectors.toMap(PaperContent::getId, Function.identity())); + // + // long serviceCount = 0; + // int totalDiscount = 0; + // List usageDetails = new ArrayList<>(); + // + // for (PartnershipUsage usage : usages) { + // PaperContent content = contentMap.get(usage.getContentId()); + // if (content == null) continue; + // + // String desc; + // if (content.getOptionType() == OptionType.SERVICE) { + // serviceCount++; + // desc = String.format("%s에서 %d명 서비스 제공받았어요!", content.getCategory(), content.getPeople()); + // } else { + // int discount = usage.getDiscount() != null ? usage.getDiscount() : 0; + // totalDiscount += discount; + // desc = String.format("%,d원 할인 혜택을 받았어요!", discount); + // } + // + // usageDetails.add(new StudentResponseDTO.UsageDetailDTO( + // usage.getPlace(), + // usage.getDate(), + // desc + // ));} + return null; } } diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java index 463dac3..645f22b 100644 --- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java +++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java @@ -24,6 +24,6 @@ public class StudentController { public ResponseEntity> getMyPartnership( @PathVariable int year, @PathVariable int month, @AuthenticationPrincipal PrincipalDetails userDetails ){ - + return null; } } diff --git a/src/main/java/com/assu/server/global/config/SecurityConfig.java b/src/main/java/com/assu/server/global/config/SecurityConfig.java new file mode 100644 index 0000000..af68335 --- /dev/null +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -0,0 +1,37 @@ +package com.assu.server.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/chat/**", + "/suggestion/**", + "/review/**", + "/ws/**", + "/pub/**", // STOMP 메시지 전송 + "/sub/**", // STOMP 메시지 구독 + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**", + "/certification/**" + ).permitAll() + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.disable()) // websocket은 csrf 필요 없음 + .formLogin(login -> login.disable()) + .httpBasic(basic -> basic.disable()); + + return http.build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 692f20a..98fa2eb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,15 @@ spring: + batch: + jdbc: + initialize-schema: never + job: + enabled: false + + datasource: + url: jdbc:mariadb://localhost:3306/assu_maria_db?allowPublicKeyRetrieval=true&useSSL=false + username: root + password: ${MARIA_DB_PASSWORD} + driver-class-name: org.mariadb.jdbc.Driver profiles: active: local # 여기에 local, blue, green 셋중 하나로 입력 config: From 53e83a2c439884a7dc69c22dfe2ffe8a25ffef18 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Thu, 14 Aug 2025 22:02:37 +0900 Subject: [PATCH 051/270] =?UTF-8?q?[FEAT/#15]=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/admin/entity/Admin.java | 3 + .../config/CertifyWebSocketConfig.java | 2 +- .../controller/CertificationController.java | 35 ++++-------- .../service/CertificationServiceImpl.java | 3 +- .../common/repository/MemberRepository.java | 3 + .../controller/PaperController.java | 9 ++- .../domain/partnership/entity/Goods.java | 2 - .../partnership/entity/PaperContent.java | 2 - .../user/repository/StudentRepository.java | 4 ++ .../server/global/config/SecurityConfig.java | 5 +- src/main/resources/application.yml | 6 -- src/main/resources/certify-test.html | 56 +++++++++++++++++++ 12 files changed, 90 insertions(+), 40 deletions(-) create mode 100644 src/main/resources/certify-test.html diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java index 373bd4a..944af35 100644 --- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java +++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java @@ -4,6 +4,8 @@ import com.assu.server.domain.user.entity.enums.Major; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.JoinColumn; import jakarta.persistence.MapsId; import jakarta.persistence.OneToOne; @@ -40,6 +42,7 @@ public class Admin { private LocalDateTime signVerifiedAt; + @Enumerated(EnumType.STRING) private Major major; public void setMember(Member member) { diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java index 8706bb7..71628c9 100644 --- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java +++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java @@ -18,6 +18,6 @@ public void configureMessageBroker(MessageBrokerRegistry config) { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") // 클라이언트 WebSocket 연결 주소 - .setAllowedOriginPatterns("*"); // CORS 허용 + .setAllowedOriginPatterns("*").withSockJS(); // CORS 허용 } } diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java index 71c8028..e7a95d3 100644 --- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java +++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java @@ -15,12 +15,15 @@ import com.assu.server.domain.common.entity.Member; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.common.repository.MemberRepository; import com.assu.server.domain.user.entity.Student; import com.assu.server.domain.user.entity.enums.EnrollmentStatus; import com.assu.server.domain.user.entity.enums.Major; import com.assu.server.domain.user.repository.StudentRepository; import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.exception.exception.GeneralException; import com.assu.server.global.util.PrincipalDetails; import com.fasterxml.jackson.databind.ser.Serializers; @@ -34,15 +37,18 @@ public class CertificationController { private final CertificationService certificationService; + private final MemberRepository memberRepository; // 지금은 그냥 임시 데이터 하드 코딩이라 여기에 둔거여 @PostMapping("/certification/session") @Operation(summary = "세션 정보를 요청하는 api", description = "인원 수 기준이 요구되는 제휴일 때 세션을 만들고, 대표자 QR에 담을 정보를 요청하는 api 입니다.") public ResponseEntity> getSessionId( - @AuthenticationPrincipal PrincipalDetails userDetails, + // @AuthenticationPrincipal PrincipalDetails userDetails, @RequestBody CertificationRequestDTO.groupRequest dto ) { - Member member = userDetails.getMember(); + // Member member = userDetails.getMember(); + Member member = memberRepository.findMemberById(1L) + .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)); CertificationResponseDTO.getSessionIdResponse result = certificationService.getSessionId(dto, member); return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_SESSION_CREATE, result)); @@ -50,32 +56,13 @@ public ResponseEntity certifyGroup( + public ResponseEntity> certifyGroup( CertificationRequestDTO.groupSessionRequest dto // 나중에 여기에 Security + WebSocket 설정 완료한 후 // @AuthenticationPrincipal 넣어주기 ) { - // 일단 더미 유저로 - Member member = new Member(); - member.setId(1L); - member.setIsActivated(ActivationStatus.ACTIVE); - member.setRole(UserRole.USER); - member.setIsPhoneVerified(true); - member.setPhoneNum("01012345678"); - member.setPhoneVerifiedAt(LocalDateTime.now()); - - Student dummyStudent = Student.builder() - .member(member) - .department("IT대학") - .enrollmentStatus(EnrollmentStatus.ENROLLED) - .yearSemester("2025-1") - .university("숭실대학교") - .stamp(0) - .major(Major.COM) - .build(); - - // Member와 StudentProfile 연결 (양방향인 경우) - member.setStudentProfile(dummyStudent); + Member member = memberRepository.findMemberById(4L).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)); certificationService.handleCertification(dto, member); 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 7ea8a2e..9305588 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 @@ -25,6 +25,7 @@ import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.repository.StudentRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.exception.GeneralException; import jakarta.transaction.Transactional; @@ -39,6 +40,7 @@ public class CertificationServiceImpl implements CertificationService { private final AdminRepository adminRepository; private final StoreRepository storeRepository; private final AssociateCertificationRepository associateCertificationRepository; + private final StudentRepository studentRepository; // 세션 메니저 private final CertificationSessionManager sessionManager; @@ -84,7 +86,6 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId( @Override public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member) { - Long userId = member.getId(); // 제휴 대상인지 확인하기 diff --git a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java index 207a939..fed9d67 100644 --- a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java +++ b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java @@ -1,9 +1,12 @@ package com.assu.server.domain.common.repository; +import java.util.Optional; + import com.assu.server.domain.common.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { + Optional findMemberById(Long id); } diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java index 639b029..7ec815a 100644 --- a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java +++ b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestController; import com.assu.server.domain.common.entity.Member; +import com.assu.server.domain.common.repository.MemberRepository; import com.assu.server.domain.partnership.dto.PaperResponseDTO; import com.assu.server.domain.partnership.service.PaperQueryService; import com.assu.server.global.apiPayload.BaseResponse; @@ -29,16 +30,18 @@ public class PaperController { private final PaperQueryService paperQueryService; + private final MemberRepository memberRepository; @GetMapping("/store/{storeId}/papers") @Operation(summary = "유저에게 적용 가능한 제휴 컨텐츠 조회", description = "유저가 속한 단과대, 학부 admin_id과 store_id 를 가진 제휴 컨텐츠 제공") @Parameters({ @Parameter(name = "storeId", description = "QR에서 추출한 storeId를 입력해주세요") }) - public ResponseEntity> getStorePaperContent(@PathVariable Long storeId, - @AuthenticationPrincipal PrincipalDetails userDetails + public ResponseEntity> getStorePaperContent(@PathVariable Long storeId + // , @AuthenticationPrincipal PrincipalDetails userDetails ) { - Member member = userDetails.getMember(); + // Member member = userDetails.getMember(); + Member member = memberRepository.findById(1L).orElse(null); PaperResponseDTO.partnershipContent result = paperQueryService.getStorePaperContent(storeId, member); diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Goods.java b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java index 2618438..04fba5b 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/Goods.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java @@ -1,7 +1,5 @@ package com.assu.server.domain.partnership.entity; -import com.assu.server.domain.partnership.repository.PaperContentRepository; - import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; diff --git a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java index 7b2881b..657928a 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java @@ -42,8 +42,6 @@ public class PaperContent extends BaseEntity { @Enumerated(EnumType.STRING) private OptionType optionType; - - private Integer people; private String category; diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java index 3762339..da003e3 100644 --- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java @@ -1,8 +1,12 @@ package com.assu.server.domain.user.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import com.assu.server.domain.user.entity.Student; public interface StudentRepository extends JpaRepository { + + Optional findStudentById(Long id); } 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 c50ebd9..6bba7a1 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -23,7 +23,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/swagger-ui/**", "/swagger-ui.html", "/swagger-resources/**", - "/webjars/**" + "/webjars/**", + "/certification/**", + "/certify-test.html" + ,"/store/**" ).permitAll() .anyRequest().authenticated() ) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c17a186..643a36d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,12 +6,6 @@ spring: job: enabled: false - datasource: - url: jdbc:mariadb://localhost:3306/assu_maria_db?allowPublicKeyRetrieval=true&useSSL=false - username: root - password: ${MARIA_DB_PASSWORD} - driver-class-name: org.mariadb.jdbc.Driver - profiles: active: local # 여기에 local, blue, green 셋중 하나로 입력 config: diff --git a/src/main/resources/certify-test.html b/src/main/resources/certify-test.html new file mode 100644 index 0000000..fa33109 --- /dev/null +++ b/src/main/resources/certify-test.html @@ -0,0 +1,56 @@ + + + + + Certify Test + + + + +

WebSocket Certify Test

+ +
+
+
+ + +
+ +

+
+
+
+
\ No newline at end of file

From ce58d083ee1296cf948e320f27ebded9995cb604 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Thu, 14 Aug 2025 23:56:49 +0900
Subject: [PATCH 052/270] =?UTF-8?q?[FEAT/#21]=20=EB=8C=80=EC=8B=9C?=
 =?UTF-8?q?=EB=B3=B4=EB=93=9C=20(=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=9C?=
 =?UTF-8?q?=ED=9C=B4)=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/store/service/StoreService.java    |  2 -
 .../store/service/StoreServiceImpl.java       | 46 ++----------------
 .../domain/user/dto/StudentResponseDTO.java   |  9 ++--
 .../PartnershipUsageRepository.java           | 19 ++++----
 .../domain/user/service/StudentService.java   |  3 ++
 .../user/service/StudentServiceImpl.java      | 48 ++++++++++++++++++-
 6 files changed, 70 insertions(+), 57 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/store/service/StoreService.java b/src/main/java/com/assu/server/domain/store/service/StoreService.java
index efcd08b..cbb8652 100644
--- a/src/main/java/com/assu/server/domain/store/service/StoreService.java
+++ b/src/main/java/com/assu/server/domain/store/service/StoreService.java
@@ -6,6 +6,4 @@
 
 public interface StoreService {
 	StoreResponseDTO.todayBest getTodayBestStore();
-
-	StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month);
 }
diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
index eaa90d9..69856fd 100644
--- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
@@ -20,14 +20,14 @@
 import com.assu.server.domain.user.repository.PartnershipUsageRepository;
 
 import jakarta.transaction.Transactional;
-
+import lombok.RequiredArgsConstructor;
 
 @Service
 @Transactional
+@RequiredArgsConstructor
 public class StoreServiceImpl implements StoreService{
 
-	private PartnershipUsageRepository partnershipUsageRepository;
-	private PaperContentRepository paperContentRepository;
+	private final PartnershipUsageRepository partnershipUsageRepository;
 
 	@Override
 	@Transactional
@@ -39,44 +39,6 @@ public StoreResponseDTO.todayBest getTodayBestStore() {
 			.build();
 	}
 
-	@Override
-	@Transactional
-	public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) {
-		// List usages = partnershipUsageRepository.findByStudentAndCreatedAtMonth(studentId, year, month);
-		//
-		// Set contentIds = usages.stream()
-		// 	.map(PartnershipUsage::getContentId)
-		// 	.filter(Objects::nonNull)
-		// 	.collect(Collectors.toSet());
-		//
-		// Map contentMap = paperContentRepository.findAllById(contentIds)
-		// 	.stream()
-		// 	.collect(Collectors.toMap(PaperContent::getId, Function.identity()));
-		//
-		// long serviceCount = 0;
-		// int totalDiscount = 0;
-		// List usageDetails = new ArrayList<>();
-		//
-		// for (PartnershipUsage usage : usages) {
-		// 	PaperContent content = contentMap.get(usage.getContentId());
-		// 	if (content == null) continue;
-		//
-		// 	String desc;
-		// 	if (content.getOptionType() == OptionType.SERVICE) {
-		// 		serviceCount++;
-		// 		desc = String.format("%s에서 %d명 서비스 제공받았어요!", content.getCategory(), content.getPeople());
-		// 	} else {
-		// 		int discount = usage.getDiscount() != null ? usage.getDiscount() : 0;
-		// 		totalDiscount += discount;
-		// 		desc = String.format("%,d원 할인 혜택을 받았어요!", discount);
-		// 	}
-		//
-		// 	usageDetails.add(new StudentResponseDTO.UsageDetailDTO(
-		// 		usage.getPlace(),
-		// 		usage.getDate(),
-		// 		desc
-		// 	));}
-	return null;
-	}
+
 
 }
diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
index 33577dd..cc22236 100644
--- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.user.dto;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -15,16 +16,16 @@ public class StudentResponseDTO {
 	@AllArgsConstructor
 	@RequiredArgsConstructor
 	public static class myPartnership {
-		private long serviceCount;        // SERVICE 개수
-		private int totalDiscount;        // DISCOUNT 총액
-		private List usageDetails;
+		private long serviceCount;
+		private List details;
 	}
 
 	@Getter
 	@AllArgsConstructor
+	@Builder
 	public static class UsageDetailDTO {
 		private String storeName;
-		private LocalDateTime usedAt;
+		private LocalDate usedAt;
 		private String benefitDescription;
 		private boolean isReviewed;
 	}
diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
index 4d5edb2..bf82668 100644
--- a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
+++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
@@ -4,6 +4,7 @@
 
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
 import com.assu.server.domain.user.entity.PartnershipUsage;
 
@@ -20,12 +21,14 @@ ORDER BY COUNT(*) DESC
         """, nativeQuery = true)
 	List findTodayPopularPartnership();
 
-	@Query("""
-    SELECT pu
-    FROM PartnershipUsage pu
-    WHERE pu.student.id = :studentId
-      AND YEAR(pu.createdAt) = :year
-      AND MONTH(pu.createdAt) = :month
-""")
-	List myPartnershipByYearAndMonth();
+	@Query("SELECT pu FROM PartnershipUsage pu " +
+		"WHERE pu.student.id= :studentId " +
+		"AND YEAR(pu.date) = :year " +
+		"AND MONTH(pu.date) = :month " +
+		"ORDER BY pu.date DESC")
+	List findByYearAndMonth(
+		@Param("studentId") Long studentId,
+		@Param("year") int year,
+		@Param("month") int month
+	);
 }
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java
index 84c57a1..c0db7c1 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentService.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java
@@ -1,4 +1,7 @@
 package com.assu.server.domain.user.service;
 
+import com.assu.server.domain.user.dto.StudentResponseDTO;
+
 public interface StudentService {
+	StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month);
 }
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
index 61a060f..49e8b5e 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
@@ -1,4 +1,50 @@
 package com.assu.server.domain.user.service;
 
-public class StudentServiceImpl {
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.springframework.stereotype.Service;
+
+import com.assu.server.domain.partnership.entity.PaperContent;
+import com.assu.server.domain.partnership.entity.enums.OptionType;
+import com.assu.server.domain.partnership.repository.PaperContentRepository;
+import com.assu.server.domain.partnership.service.PaperQueryService;
+import com.assu.server.domain.user.dto.StudentResponseDTO;
+import com.assu.server.domain.user.entity.PartnershipUsage;
+import com.assu.server.domain.user.repository.PartnershipUsageRepository;
+
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StudentServiceImpl implements StudentService {
+
+	private final PaperContentRepository paperContentRepository;
+	private final PartnershipUsageRepository partnershipUsageRepository;
+
+	@Override
+	@Transactional
+	public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) {
+		List usages = partnershipUsageRepository.findByYearAndMonth(studentId, year, month);
+
+		return StudentResponseDTO.myPartnership.builder()
+			.serviceCount(usages.size())
+			.details(usages.stream()
+				.map(u -> StudentResponseDTO.UsageDetailDTO.builder()
+					.storeName(u.getPlace())
+					.usedAt(u.getDate())
+					.benefitDescription(u.getPartnershipContent())
+					.isReviewed(u.getIsReviewed())
+					.build()
+				).toList()
+			)
+			.build();
+	}
 }

From b30248ae7dc3c32719b75b8fdffd05f1b32aa7b4 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Fri, 15 Aug 2025 02:46:41 +0900
Subject: [PATCH 053/270] =?UTF-8?q?[FEAT/#17]=20=EB=B2=88=ED=98=B8=20?=
 =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84=20(=EC=B6=94=ED=9B=84?=
 =?UTF-8?q?=20=EC=82=AC=EC=9A=A9..)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |  3 +
 .../auth/controller/AuthController.java       | 36 ++++++++-
 .../domain/auth/dto/AuthReqeustDTO.java       |  4 -
 .../domain/auth/dto/AuthRequestDTO.java       | 34 +++++++++
 .../domain/auth/exception/AuthException.java  | 11 +++
 .../domain/auth/service/AuthServiceImpl.java  |  7 +-
 .../domain/auth/service/PhoneAuthService.java |  6 ++
 .../auth/service/PhoneAuthServiceImpl.java    | 47 ++++++++++++
 .../apiPayload/code/status/ErrorStatus.java   |  2 +
 .../apiPayload/code/status/SuccessStatus.java | 73 +------------------
 .../server/global/config/RedisConfig.java     | 50 +++++++++++++
 .../server/global/util/RandomNumberUtil.java  | 11 +++
 12 files changed, 208 insertions(+), 76 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/auth/dto/AuthReqeustDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/AuthRequestDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/exception/AuthException.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java
 create mode 100644 src/main/java/com/assu/server/global/config/RedisConfig.java
 create mode 100644 src/main/java/com/assu/server/global/util/RandomNumberUtil.java

diff --git a/build.gradle b/build.gradle
index 29f8d3b..80b9d97 100644
--- a/build.gradle
+++ b/build.gradle
@@ -33,6 +33,9 @@ dependencies {
 	implementation 'org.springframework.boot:spring-boot-starter-security'
 	testImplementation 'org.springframework.security:spring-security-test'
 
+	// redis
+	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+
 	// rabbit mq
 	implementation 'org.springframework.boot:spring-boot-starter-amqp'
 	testImplementation 'org.springframework.amqp:spring-rabbit-test'
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 7bfc995..0155147 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,4 +1,38 @@
 package com.assu.server.domain.auth.controller;
 
+import com.assu.server.domain.auth.dto.AuthRequestDTO;
+import com.assu.server.domain.auth.service.PhoneAuthService;
+import com.assu.server.global.apiPayload.BaseResponse;
+import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+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;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/auth")
 public class AuthController {
-}
+    private final PhoneAuthService phoneAuthService;
+
+    @PostMapping("/phone-numbers/send")
+    public BaseResponse sendAuthNumber(
+            @RequestBody @Valid AuthRequestDTO.PhoneAuthSendRequest request
+    ) {
+        phoneAuthService.sendAuthNumber(request.getPhoneNumber());
+        return BaseResponse.onSuccess(SuccessStatus.SEND_AUTH_NUMBER_SUCCESS, null);
+    }
+
+    @PostMapping("/phone-numbers/verify")
+    public BaseResponse checkAuthNumber(
+            @RequestBody @Valid AuthRequestDTO.PhoneAuthVerifyRequest request
+    ) {
+        phoneAuthService.verifyAuthNumber(
+                request.getPhoneNumber(),
+                request.getAuthNumber()
+        );
+        return BaseResponse.onSuccess(SuccessStatus.VERIFY_AUTH_NUMBER_SUCCESS, null);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/dto/AuthReqeustDTO.java b/src/main/java/com/assu/server/domain/auth/dto/AuthReqeustDTO.java
deleted file mode 100644
index 51038d9..0000000
--- a/src/main/java/com/assu/server/domain/auth/dto/AuthReqeustDTO.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.auth.dto;
-
-public class AuthReqeustDTO {
-}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/AuthRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/AuthRequestDTO.java
new file mode 100644
index 0000000..9fe4319
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/AuthRequestDTO.java
@@ -0,0 +1,34 @@
+package com.assu.server.domain.auth.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+public class AuthRequestDTO {
+
+    @Builder
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class PhoneAuthVerifyRequest {
+        @NotBlank
+        @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.")
+        private String phoneNumber;
+
+        @NotBlank
+        private String authNumber;
+    }
+
+    @Builder
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class PhoneAuthSendRequest {
+        @NotBlank
+        @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.")
+        private String phoneNumber;
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/exception/AuthException.java b/src/main/java/com/assu/server/domain/auth/exception/AuthException.java
new file mode 100644
index 0000000..fe63a1c
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/exception/AuthException.java
@@ -0,0 +1,11 @@
+package com.assu.server.domain.auth.exception;
+
+import com.assu.server.global.apiPayload.code.BaseErrorCode;
+import com.assu.server.global.exception.exception.GeneralException;
+
+public class AuthException extends GeneralException {
+
+    public AuthException(BaseErrorCode code) {
+        super(code);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java
index edde394..70f844c 100644
--- a/src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java
@@ -1,4 +1,9 @@
 package com.assu.server.domain.auth.service;
 
-public class AuthServiceImpl {
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class AuthServiceImpl implements AuthService {
 }
diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java
new file mode 100644
index 0000000..3b80700
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java
@@ -0,0 +1,6 @@
+package com.assu.server.domain.auth.service;
+
+public interface PhoneAuthService {
+    void sendAuthNumber(String phoneNumber);
+    void verifyAuthNumber(String phoneNumber, String authNumber);
+}
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
new file mode 100644
index 0000000..6c97198
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthServiceImpl.java
@@ -0,0 +1,47 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.util.RandomNumberUtil;
+import com.assu.server.domain.auth.exception.AuthException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+
+@Service
+@RequiredArgsConstructor
+public class PhoneAuthServiceImpl implements PhoneAuthService {
+
+    private final StringRedisTemplate redisTemplate;
+
+    private static final Duration AUTH_CODE_TTL = Duration.ofMinutes(5); // 인증번호 5분 유효
+
+    @Async
+    @Override
+    public void sendAuthNumber(String phoneNumber) {
+        String authNumber = RandomNumberUtil.generateSixDigit();
+
+        ValueOperations valueOps = redisTemplate.opsForValue();
+        valueOps.set(phoneNumber, authNumber, AUTH_CODE_TTL);
+
+        // 알리고 API로 실제 문자 발송 처리 필요
+        // 예: aligoService.sendSms(phoneNumber, authNumber);
+        System.out.println("[SMS] 전송 대상: " + phoneNumber + ", 인증번호: " + authNumber);
+    }
+
+    @Override
+    public void verifyAuthNumber(String phoneNumber, String authNumber) {
+        ValueOperations valueOps = redisTemplate.opsForValue();
+        String stored = valueOps.get(phoneNumber);
+
+        if (stored == null || !stored.equals(authNumber)) {
+            throw new AuthException(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
+        }
+
+        // 인증 성공 시 Redis에서 삭제(Optional)
+        redisTemplate.delete(phoneNumber);
+    }
+}
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index ddefa9e..1ea5724 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -21,6 +21,8 @@ public enum ErrorStatus implements BaseErrorCode {
     // 멤버 에러
     NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."),
 
+    // 인증 에러
+    NOT_VERIFIED_PHONE_NUMBER(HttpStatus.BAD_REQUEST,"AUTH_4001","전화번호 인증에 실패했습니다.")
     ;
 
     private final HttpStatus httpStatus;
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
index 0ca43cf..5e7bccc 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
@@ -16,76 +16,9 @@ public enum SuccessStatus implements BaseCode {
     MEMBER_SUCCESS(HttpStatus.OK, "MEMBER_200", "성공적으로 조회되었습니다."),
     MEMBER_CREATED(HttpStatus.CREATED, "MEMBER_201", "성공적으로 생성되었습니다."),
 
-    //옷 성공
-    CLOTH_SUCCESS(HttpStatus.OK, "CLOTH_200", "옷이 성공적으로 조회되었습니다."),
-    CLOTH_CREATED(HttpStatus.CREATED, "CLOTH_201", "옷이 성공적으로 생성되었습니다."),
-    CLOTH_EDITED(HttpStatus.NO_CONTENT, "CLOTH_204", "옷이 성공적으로 수정되었습니다."),
-    CLOTH_DELETED(HttpStatus.NO_CONTENT, "CLOTH_204", "옷이 성공적으로 삭제되었습니다."),
-
-    //카테고리 성공
-    CATEGORY_SUCCESS(HttpStatus.OK, "CATEGORY_200", "성공적으로 조회되었습니다."),
-    CATEGORY_CREATED(HttpStatus.CREATED, "CATEGORY_201", "성공적으로 생성되었습니다."),
-
-    //폴더 성공
-    FOLDER_SUCCESS(HttpStatus.OK, "FOLDER_200", "성공적으로 조회되었습니다."),
-    FOLDER_CREATED(HttpStatus.CREATED, "FOLDER_201", "성공적으로 생성되었습니다."),
-    FOLDER_DELETED(HttpStatus.NO_CONTENT, "FOLDER_204", "성공적으로 삭제되었습니다."),
-    FOLDER_EDIT_SUCCESS(HttpStatus.NO_CONTENT, "FOLDER_204", "성공적으로 수정되었습니다."),
-    FOLDER_ADD_CLOTHES_SUCCESS(HttpStatus.CREATED, "FOLDER_201", "성공적으로 추가되었습니다."),
-    FOLDER_DELETE_CLOTHES_SUCCESS(HttpStatus.NO_CONTENT, "FOLDER_204", "성공적으로 삭제되었습니다."),
-    FOLDER_CLOTHES_SUCCESS(HttpStatus.OK, "FOLDER_200", "성공적으로 반영되었습니다."),
-
-    //검색 성공
-    SEARCH_SUCCESS(HttpStatus.OK, "SEARCH_200", "성공적으로 조회되었습니다."),
-
-    //기록 성공
-    HISTORY_SUCCESS(HttpStatus.OK, "HISTORY_200", "성공적으로 조회되었습니다."),
-    HISTORY_CREATED(HttpStatus.CREATED, "HISTORY_201", "성공적으로 생성되었습니다."),
-    HISTORY_LIKE_STATUS_CHANGED(HttpStatus.OK,"HISTORY_200","좋아요 상태가 성공적으로 변경되었습니다."),
-    HISTORY_COMMENT_CREATED(HttpStatus.CREATED,"HISTORY_201","성공적으로 댓글이 생성되었습니다."),
-    HISTORY_UPDATED(HttpStatus.NO_CONTENT,"HISTORY_204","성공적으로 수정되었습니다"),
-    HISTORY_COMMENT_DELETED(HttpStatus.NO_CONTENT,"HISTORY_204","댓글이 성공적으로 삭제되었습니다"),
-    HISTORY_COMMENT_UPDATED(HttpStatus.NO_CONTENT,"HISTORY_204","댓글이 성공적으로 수정되었습니다"),
-    HISTORY_DELETED(HttpStatus.NO_CONTENT,"HISTORY_204","기록이 성공적으로 삭제되었습니다"),
-    HISTORY_LIKE_USER(HttpStatus.OK,"HISTORY_200","기록의 좋아요를 누른 유저 정보를 성공적으로 조회했습니다."),
-    HISTORY_CHECK_SUCCESS(HttpStatus.OK, "HISTORY_200","나의 기록인지 성공적으로 조회했습니다."),
-
-    //알림 성공
-    NOTIFICATION_SUCCESS(HttpStatus.OK, "NOTIFICATION_200", "성공적으로 조회되었습니다."),
-    UNREAD_NOTIFICATION_CHECKED(HttpStatus.OK,"NOTIFICATION_200","읽지 않은 알림 여부가 성공적으로 조회되었습니다."),
-    NOTIFICATION_READ(HttpStatus.NO_CONTENT,"NOTIFICATION_204","알림이 성공적으로 읽음 처리되었습니다."),
-    NOTIFICATION_SEND_SUCCESS(HttpStatus.NO_CONTENT,"NOTIFICATION_204","알림이 성공적으로 발송되었습니다"),
-    NOTIFICATION_HISTORY_LIKED_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","기록 좋아요 알림이 성공적으로 발송되었습니다."),
-    NOTIFICATION_NEW_FOLLOWER_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","팔로우 알림이 성공적으로 발송되었습니다."),
-    NOTIFICATION_HISTORY_COMMENT_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","기록 댓글 알림이 성공적으로 발송되었습니다."),
-    NOTIFICATION_REPLY_SUCCESS(HttpStatus.OK,"NOTIFICATION_200","댓글에 대한 답글 알림이 성공적으로 발송되었습니다."),
-
-    //홈 성공
-    HOME_SUCCESS(HttpStatus.OK, "HOME_200", "성공적으로 조회되었습니다."),
-
-    //기타 멤버 관련 성공
-    MEMBER_ACTION_SUCCESS(HttpStatus.OK, "MEMBER_ACTION_200", "멤버 관련 요소가 성공적으로 조회되었습니다."),
-    MEMBER_ACTION_CREATED(HttpStatus.CREATED, "MEMBER_ACTION_201", "멤버 관련 요소가 성공적으로 생성되었습니다."),
-    MEMBER_ACTION_EDITED(HttpStatus.OK, "MEMBER_ACTION_204", "멤버 관련 요소가 성공적으로 수정되었습니다."),
-
-
-    //아이디 성공
-    MEMBER_ID_SUCCESS(HttpStatus.OK, "MEMBER_ID_200", "사용가능한 아이디입니다."),
-
-    //로그인 성공
-    LOGIN_SUCCESS(HttpStatus.OK, "LOGIN_200", "로그인에 성공하였습니다."),
-    LOGIN_CREATED(HttpStatus.CREATED, "LOGIN_201", "회원가입과 로그인에 성공하였습니다."),
-    LOGIN_UPDATED(HttpStatus.NO_CONTENT, "LOGIN_204", "로그인 정보가 성공적으로 수정되었습니다."),
-
-    //로그아웃 성공
-    LOGOUT_SUCCESS(HttpStatus.OK, "LOGOUT_200", "로그아웃에 성공하였습니다."),
-    UNLINK_SUCCESS(HttpStatus.OK, "UNLINK_200", "회원탈퇴에 성공하였습니다."),
-
-    //Elastic Search 인덱스 생성 및 동기화 성공
-    CLOTH_SYNC_CREATED(HttpStatus.CREATED, "SEARCH_201", "옷 검색 인덱스가 성공적으로 생성되었습니다."),
-    HISTORY_SYNC_CREATED(HttpStatus.CREATED, "SEARCH_201", "기록 검색 인덱스가 성공적으로 생성되었습니다."),
-    MEMBER_SYNC_CREATED(HttpStatus.CREATED, "SEARCH_201", "유저 검색 인덱스가 성공적으로 생성되었습니다."),
-
+    //인증 관련 성공
+    SEND_AUTH_NUMBER_SUCCESS(HttpStatus.OK, "AUTH_200", "성공적으로 조회되었습니다."),
+    VERIFY_AUTH_NUMBER_SUCCESS(HttpStatus.OK, "AUTH_201", "성공적으로 생성되었습니다."),
 
     //신고 성공
     REPORT_HISTORY_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "기록 신고의 정보가 성공적으로 조회되었습니다."),
diff --git a/src/main/java/com/assu/server/global/config/RedisConfig.java b/src/main/java/com/assu/server/global/config/RedisConfig.java
new file mode 100644
index 0000000..b645501
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/RedisConfig.java
@@ -0,0 +1,50 @@
+package com.assu.server.global.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+@EnableConfigurationProperties(RedisProperties.class)
+@RequiredArgsConstructor
+public class RedisConfig {
+
+    private final RedisProperties properties;
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(properties.getHost(), properties.getPort());
+        return lettuceConnectionFactory;
+    }
+
+    @Bean
+    public RedisTemplate redisTemplate() {
+        RedisTemplate redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory());
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+
+        ObjectMapper objectMapper = new ObjectMapper()
+                .registerModule(new JavaTimeModule())
+                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+                .enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
+
+        redisTemplate.setValueSerializer(serializer);
+        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashValueSerializer(serializer);
+
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/global/util/RandomNumberUtil.java b/src/main/java/com/assu/server/global/util/RandomNumberUtil.java
new file mode 100644
index 0000000..58ca023
--- /dev/null
+++ b/src/main/java/com/assu/server/global/util/RandomNumberUtil.java
@@ -0,0 +1,11 @@
+package com.assu.server.global.util;
+
+import java.util.Random;
+
+public class RandomNumberUtil {
+    public static String generateSixDigit() {
+        Random random = new Random();
+        int number = 100000 + random.nextInt(900000); // 100000~999999
+        return String.valueOf(number);
+    }
+}

From c97c7a6fcbaf13b418b79dc8a9cd3d7f4805b5ee Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Fri, 15 Aug 2025 02:56:38 +0900
Subject: [PATCH 054/270] =?UTF-8?q?[Feat/#14]=20=20-=20=EC=A0=9C=ED=9C=B4?=
 =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=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     | 12 +++++++
 .../dto/PartnershipRequestDTO.java            |  2 +-
 .../dto/PartnershipResponseDTO.java           | 13 ++++++++
 .../domain/partnership/entity/Paper.java      |  8 ++---
 .../service/PartnershipService.java           |  2 ++
 .../service/PartnershipServiceImpl.java       | 33 +++++++++++++++++++
 .../apiPayload/code/status/ErrorStatus.java   | 10 ++----
 .../server/global/config/SecurityConfig.java  | 30 +++++++----------
 8 files changed, 79 insertions(+), 31 deletions(-)

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 920a67d..6ec2fb9 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
@@ -51,4 +51,16 @@ public BaseResponse getPartn
         return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.getPartnership(partnershipId));
     }
 
+    @Operation(
+            summary = "제휴 상태를 업데이트하는 API 입니다.",
+            description = "바꾸고 싶은 상태를 입력하세요(PENDING/ACTIVE/INACTIVE)"
+    )
+    @PatchMapping("/{partnershipId}/status")
+    public BaseResponse updatePartnershipStatus(
+            @PathVariable("partnershipId") Long partnershipId,
+            @RequestBody PartnershipRequestDTO.UpdateRequestDTO request
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.updatePartnershipStatus(partnershipId, request));
+    }
+
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
index d764af3..a0e1db2 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
@@ -2,7 +2,7 @@
 
 import com.assu.server.domain.partnership.entity.enums.CriterionType;
 import com.assu.server.domain.partnership.entity.enums.OptionType;
-import lombok.Getter;
+import lombok.*;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
index a869b0e..8cf4a36 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
@@ -5,6 +5,7 @@
 import lombok.*;
 
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 public class PartnershipResponseDTO {
@@ -48,4 +49,16 @@ public static class PartnershipGoodsResponseDTO {
         private Long goodsId;
         private String goodsName;
     }
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class UpdateResponseDTO {
+        private Long partnershipId;
+        private String prevStatus;
+        private String newStatus;
+        private LocalDateTime changedAt;
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
index 9ac9689..934773f 100644
--- a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
+++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
@@ -6,10 +6,7 @@
 import com.assu.server.domain.store.entity.Store;
 
 import jakarta.persistence.*;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 import java.time.LocalDate;
 import java.util.ArrayList;
@@ -28,7 +25,8 @@ public class Paper extends BaseEntity {
 	private LocalDate partnershipPeriodStart; //  LocalDate vs String
 	private LocalDate partnershipPeriodEnd;
 
-	@Enumerated(EnumType.STRING)
+	@Setter
+    @Enumerated(EnumType.STRING)
 	private ActivationStatus isActivated;
 
 	@ManyToOne(fetch = FetchType.LAZY)
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 7012635..cf236f9 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
@@ -15,4 +15,6 @@ PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(
     List listPartnerships(boolean all);
 
     PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId);
+
+    PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request);
 }
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 4f3affe..37d0944 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
@@ -2,6 +2,7 @@
 
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
+import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.domain.partnership.converter.PartnershipConverter;
@@ -24,6 +25,7 @@
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -128,4 +130,35 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long pa
 
         return PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches);
     }
+
+    @Override
+    @Transactional
+    public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request) {
+        Paper paper = paperRepository.findById(partnershipId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER));
+
+        if(request == null || request.getStatus() == null){
+            throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
+        }
+
+        ActivationStatus prev = paper.getIsActivated();
+        ActivationStatus next = parseStatus(request.getStatus());
+
+        paper.setIsActivated(next);
+
+        return PartnershipResponseDTO.UpdateResponseDTO.builder()
+                .partnershipId(paper.getId())
+                .prevStatus(prev == null ? null : prev.name())
+                .newStatus(next.name())
+                .changedAt(LocalDateTime.now())
+                .build();
+    }
+
+    private ActivationStatus parseStatus(String raw) {
+        try {
+            return ActivationStatus.valueOf(raw.trim().toUpperCase());
+        } catch (Exception e) {
+            throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
+        }
+    }
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 441dd1e..81b4970 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -31,13 +31,6 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."),
     NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
-
-    // 어드민 에러
-    NO_SUCH_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_5001", "존재하지 않는 학생회입니다."),
-
-    // 파트너 에러
-    NO_SUCH_PARTNER(HttpStatus.NOT_FOUND, "PARTNER_5003", "존재하지 않는 파트너입니다."),
-
     // 학생 에러
     NO_SUCH_STUDENT(HttpStatus.NOT_FOUND, "STUDENT_5004", "존재하지 않는 학생입니다."),
 
@@ -71,6 +64,9 @@ public enum ErrorStatus implements BaseErrorCode {
     // 파트너 찾기 에러
     NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "PARTNER_5502", "제휴업체를 찾을 수 없습니다."),
 
+    // 유효하지 않은 요청
+    INVALID_REQUEST(HttpStatus.NOT_FOUND, "INVALID_13001", "유효하지 않은 요청입니다."),
+
     ;
 
     private final HttpStatus httpStatus;
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 dc6a9ed..d27975d 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -2,7 +2,9 @@
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.web.SecurityFilterChain;
 
 @Configuration
@@ -11,24 +13,16 @@ public class SecurityConfig {
     @Bean
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
-                .authorizeHttpRequests(auth -> auth
-                        .requestMatchers(
-                                "/chat/**",
-                                "/suggestion/**",
-                                "/review/**",
-                                "/ws/**",
-                                "/pub/**",     // STOMP 메시지 전송
-                                "/sub/**",     // STOMP 메시지 구독
-                                "/v3/api-docs/**",
-                                "/swagger-ui/**",
-                                "/swagger-ui.html",
-                                "/swagger-resources/**",
-                                "/webjars/**"
-                        ).permitAll()
-                        .anyRequest().authenticated()
-                )
-                .csrf(csrf -> csrf.disable())  // CSRF 비활성화
-                .formLogin(login -> login.disable())
+                // CSRF 비활성화
+                .csrf(csrf -> csrf.disable())
+                // CORS 기본값(필요 없으면 이 줄 삭제해도 됨)
+                .cors(Customizer.withDefaults())
+                // 모든 요청 허용
+                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+                // 세션 미사용 (원하면 STATELESS 유지, 필요 없으면 이 줄 삭제 가능)
+                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                // 폼 로그인/HTTP Basic 비활성화
+                .formLogin(form -> form.disable())
                 .httpBasic(basic -> basic.disable());
 
         return http.build();

From 9c0f0359e7529120580c3a31ecec513d2e15d218 Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Fri, 15 Aug 2025 14:19:45 +0900
Subject: [PATCH 055/270] =?UTF-8?q?feat/#13-review=20=20-=20=20=EB=88=84?=
 =?UTF-8?q?=EC=A0=81=20=EC=A0=9C=ED=9C=B4=20=EC=82=AC=EC=9A=A9=20=EA=B1=B4?=
 =?UTF-8?q?=20=EC=88=9C=20=EC=8A=A4=ED=86=A0=EC=96=B4=20=EC=A1=B0=ED=9A=8C?=
 =?UTF-8?q?=20api=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/StudentAdminController.java    | 21 ++++++++++
 .../converter/StudentAdminConverter.java      | 23 ++++++++++-
 .../mapping/dto/StudentAdminResponseDTO.java  | 22 +++++++++++
 .../repository/StudentAdminRepository.java    | 23 +++++++++++
 .../mapping/service/StudentAdminService.java  |  2 +
 .../service/StudentAdminServiceImpl.java      | 39 +++++++++++++++++++
 .../repository/PartnershipRepository.java     | 12 ++++++
 .../repository/PatnershipRepository.java      |  4 --
 .../apiPayload/code/status/ErrorStatus.java   |  4 +-
 9 files changed, 143 insertions(+), 7 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PartnershipRepository.java
 delete mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java

diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
index 9bac60d..7d83a0a 100644
--- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
+++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
@@ -40,4 +40,25 @@ public BaseResponse getNewStud
     public BaseResponse getCountUser(){
         return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsagePerson());
     }
+    @Operation(
+            summary = "제휴업체 누적별 1위 업체 조회 API입니다.",
+            description = "adminId로 접근해주세요."
+    )
+        @GetMapping("/top")
+        public BaseResponse getTopUsage() {
+            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsage());
+        }
+
+        /**
+         * 제휴 업체별 누적 제휴 이용 현황 리스트 반환 (사용량 내림차순)
+         */
+        @Operation(
+                summary = "제휴업체 누적 사용 수 내림차순 조회 API입니다.",
+                description = "adminId로 접근해주세요."
+        )
+        @GetMapping("/usage")
+        public BaseResponse getUsageList() {
+            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsageList());
+        }
+
 }
diff --git a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java
index d5be4fe..1af32e8 100644
--- a/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java
+++ b/src/main/java/com/assu/server/domain/mapping/converter/StudentAdminConverter.java
@@ -1,6 +1,11 @@
 package com.assu.server.domain.mapping.converter;
 
+import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO;
+import com.assu.server.domain.partnership.entity.Paper;
+import com.assu.server.domain.user.entity.PartnershipUsage;
+
+import java.util.List;
 
 public class StudentAdminConverter {
 
@@ -19,7 +24,7 @@ public static StudentAdminResponseDTO.NewCountAdminResponseDTO newCountAdminResp
                 .adminName(adminName)
                 .build();
     }
-
+    //오늘 사용자수
     public static StudentAdminResponseDTO.CountUsagePersonResponseDTO countUsagePersonDTO(Long adminId, Long total, String adminName){
         return StudentAdminResponseDTO.CountUsagePersonResponseDTO.builder()
                 .adminId(adminId)
@@ -27,5 +32,19 @@ public static StudentAdminResponseDTO.CountUsagePersonResponseDTO countUsagePers
                 .adminName(adminName)
                 .build();
     }
-
+    //업체별 누적 사용건수
+    public static StudentAdminResponseDTO.CountUsageResponseDTO countUsageResponseDTO(Admin admin, Paper paper, Long total) {
+        return StudentAdminResponseDTO.CountUsageResponseDTO.builder()
+                .usageCount(total)
+                .adminId(admin.getId())
+                .adminName(admin.getName())
+                .storeId(paper.getStore().getId())
+                .storeName(paper.getStore().getName())
+                .build();
+    }
+    public static StudentAdminResponseDTO.CountUsageListResponseDTO countUsageListResponseDTO(List countUsageList) {
+        return StudentAdminResponseDTO.CountUsageListResponseDTO.builder()
+                .items(countUsageList)
+                .build();
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java
index 77fef52..1114529 100644
--- a/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/mapping/dto/StudentAdminResponseDTO.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.mapping.dto;
 
+import java.util.List;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
@@ -35,4 +36,25 @@ public static class CountUsagePersonResponseDTO{
         private Long adminId;
         private String adminName;
     }
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class CountUsageResponseDTO{ //제휴 업체별 누적 제휴 이용현황
+        private Long usageCount;
+        private Long adminId;
+        private String adminName;
+        private Long storeId;
+        private String storeName;
+
+    }
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class CountUsageListResponseDTO {
+        private List items; // 사용량 내림차순 정렬됨
+    }
+
 }
diff --git a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java
index 9bdf471..a61a068 100644
--- a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java
+++ b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java
@@ -9,6 +9,7 @@
 import java.time.LocalDateTime;
 import java.time.YearMonth;
 import java.util.Collection;
+import java.util.List;
 
 public interface StudentAdminRepository extends JpaRepository {
     @Query("""
@@ -46,4 +47,26 @@ SELECT COUNT(DISTINCT pu.student_id)
           AND pu.created_at <  CURRENT_DATE + INTERVAL 1 DAY
         """, nativeQuery = true)
     Long countTodayUsersByAdmin(@Param("adminId") Long adminId);
+
+    // 누적: admin이 제휴한 모든 store의 사용 건수 (0건 포함), 사용량 내림차순
+    @Query(value = """
+        SELECT
+          p.store_id                        AS storeId,
+          s.name                            AS storeName,
+          CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount
+        FROM paper p
+        JOIN store s              ON s.id = p.store_id
+        LEFT JOIN paper_content pc ON pc.paper_id = p.id
+        LEFT JOIN partnership_usage pu ON pu.paper_id = pc.id
+        WHERE p.admin_id = :adminId
+        GROUP BY p.store_id, s.name
+        ORDER BY usageCount DESC, storeId ASC
+        """, nativeQuery = true)
+    List findUsageByStore(@Param("adminId") Long adminId);
+
+    interface StoreUsage {
+        Long getStoreId();
+        String getStoreName();
+        Long getUsageCount();
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java
index 55a1757..c6eb3e9 100644
--- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java
+++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java
@@ -6,4 +6,6 @@ public interface StudentAdminService {
     StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth();
     StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin();
     StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson();
+    StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage();
+    StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList();
 }
diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java
index c81db61..04ed053 100644
--- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java
@@ -5,6 +5,8 @@
 import com.assu.server.domain.mapping.converter.StudentAdminConverter;
 import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO;
 import com.assu.server.domain.mapping.repository.StudentAdminRepository;
+import com.assu.server.domain.partnership.entity.Paper;
+import com.assu.server.domain.partnership.repository.PartnershipRepository;
 import com.assu.server.domain.user.service.StudentService;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.exception.DatabaseException;
@@ -12,12 +14,15 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+
 @Service
 @Transactional
 @RequiredArgsConstructor
 public class StudentAdminServiceImpl implements StudentAdminService {
     private final StudentAdminRepository studentAdminRepository;
     private final AdminRepository adminRepository;
+    private final PartnershipRepository partnershipRepository;
 
     @Override
     @Transactional
@@ -55,4 +60,38 @@ public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson()
         return StudentAdminConverter.countUsagePersonDTO(memberId, total, adminName);
     }
 
+    @Override
+    @Transactional
+    public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage() {
+        //Long memberId = SecurityUtil.getCurrentUserId;
+        Long memberId = 5L;
+        Admin admin = adminRepository.findById(memberId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+        String adminName =admin.getName();
+        List storeUsages = studentAdminRepository.findUsageByStore(memberId);
+        var top = storeUsages.get(0);
+        Paper paper = partnershipRepository.findFirstByAdmin_IdAndStore_IdOrderByIdAsc(memberId, top.getStoreId())
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE));
+        Long total = top.getUsageCount();
+
+        return StudentAdminConverter.countUsageResponseDTO(admin, paper, total);
+    }
+
+    @Override
+    @Transactional
+    public StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList() {
+        // Long memberId = SecurityUtil.getCurrentUserId();
+        Long memberId = 5L;
+
+        Admin admin = adminRepository.findById(memberId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+        List storeUsages = studentAdminRepository.findUsageByStore(memberId);
+        var items = storeUsages.stream().map(row -> {
+            Paper paper = partnershipRepository.findFirstByAdmin_IdAndStore_IdOrderByIdAsc(memberId, row.getStoreId())
+                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE));
+            return StudentAdminConverter.countUsageResponseDTO(admin, paper, row.getUsageCount());
+        }).toList();
+        return StudentAdminConverter.countUsageListResponseDTO(items);
+    }
+
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PartnershipRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PartnershipRepository.java
new file mode 100644
index 0000000..0d080d9
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PartnershipRepository.java
@@ -0,0 +1,12 @@
+package com.assu.server.domain.partnership.repository;
+
+import com.assu.server.domain.partnership.entity.Paper;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+public interface PartnershipRepository extends JpaRepository  {
+
+    Optional findFirstByAdmin_IdAndStore_IdOrderByIdAsc(Long adminId, Long storeId);
+}
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java
deleted file mode 100644
index 7971fb1..0000000
--- a/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.partnership.repository;
-
-public class PatnershipRepository {
-}
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index a83747e..590638b 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -27,7 +27,9 @@ public enum ErrorStatus implements BaseErrorCode {
     //스투던트 에러
     NO_SUCH_STUDENT(HttpStatus.NOT_FOUND, "STUDENT_4003", "존재하지 않는 학생 ID입니다."),
     //어드민 에러
-    NO_SUCH_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_4004", "존재하지 않는 관리자 ID입니다.")
+    NO_SUCH_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_4004", "존재하지 않는 관리자 ID입니다."),
+    //스토어의 제휴내역이 없을 때
+    NO_PAPER_FOR_STORE(HttpStatus.NOT_FOUND, "ADMIN_4005", "존재하지 않는 paper ID입니다.")
     ;
 
     private final HttpStatus httpStatus;

From fe5c116e7c81cebffd28fdbe57d71dcc1339f187 Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Fri, 15 Aug 2025 20:14:12 +0900
Subject: [PATCH 056/270] =?UTF-8?q?feat/#13-review=20=20-=20=20=EB=82=B4?=
 =?UTF-8?q?=20=EA=B0=80=EA=B2=8C=20=EC=88=9C=EC=9C=84=20=EC=A1=B0=ED=9A=8C?=
 =?UTF-8?q?=20api=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../store/controller/StoreController.java     | 35 ++++++++
 .../store/converter/StoreConverter.java       | 36 ++++++++
 .../domain/store/dto/StoreResponseDTO.java    | 25 ++++++
 .../store/repository/StoreRepository.java     | 86 +++++++++++++++++++
 .../domain/store/service/StoreService.java    |  4 +
 .../store/service/StoreServiceImpl.java       | 59 ++++++++++++-
 6 files changed, 244 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/store/controller/StoreController.java b/src/main/java/com/assu/server/domain/store/controller/StoreController.java
index 1f9d61b..54c0f9a 100644
--- a/src/main/java/com/assu/server/domain/store/controller/StoreController.java
+++ b/src/main/java/com/assu/server/domain/store/controller/StoreController.java
@@ -1,4 +1,39 @@
 package com.assu.server.domain.store.controller;
 
+import com.assu.server.domain.review.dto.ReviewResponseDTO;
+import com.assu.server.domain.store.dto.StoreResponseDTO;
+import com.assu.server.domain.store.service.StoreService;
+import com.assu.server.global.apiPayload.BaseResponse;
+import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/store")
 public class StoreController {
+
+    private final StoreService storeService;
+
+    @Operation(
+            summary = "내 가게 순위 조회 API입니다.",
+            description = "partnerId로 접근해주세요."
+    )
+    @GetMapping("/ranking")
+    public BaseResponse getWeeklyRank() {
+        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getWeeklyRank());
+    }
+
+    @Operation(
+            summary = "내 가게 순위 6주치 조회 API입니다.",
+            description = "partnerId로 접근해주세요"
+    )
+    @GetMapping("/ranking/weekly")
+    public BaseResponse> getWeeklyRankByPartnerId(){
+        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getListWeeklyRank().getItems());
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/store/converter/StoreConverter.java b/src/main/java/com/assu/server/domain/store/converter/StoreConverter.java
index fdaffa2..c61f0fa 100644
--- a/src/main/java/com/assu/server/domain/store/converter/StoreConverter.java
+++ b/src/main/java/com/assu/server/domain/store/converter/StoreConverter.java
@@ -1,4 +1,40 @@
 package com.assu.server.domain.store.converter;
 
+import com.assu.server.domain.store.dto.StoreResponseDTO;
+import com.assu.server.domain.store.repository.StoreRepository;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
 public class StoreConverter {
+    // 단건(이번 주) 변환: Row -> Response
+    public static StoreResponseDTO.WeeklyRankResponseDTO weeklyRankResponseDTO(StoreRepository.GlobalWeeklyRankRow r) {
+        return StoreResponseDTO.WeeklyRankResponseDTO.builder()
+                .usageCount(r.getUsageCount())
+                .rank(r.getStoreRank())
+                .build();
+    }
+
+    // 리스트 아이템 변환용: Row -> WeeklyRankResponseDTO
+    public static StoreResponseDTO.WeeklyRankResponseDTO weeklyRankItem(StoreRepository.GlobalWeeklyRankRow r) {
+        return StoreResponseDTO.WeeklyRankResponseDTO.builder()
+                .usageCount(r.getUsageCount())
+                .rank(r.getStoreRank())
+                .build();
+    }
+
+    // 리스트 래핑: storeId, storeName, items를 받아 최종 DTO 조립
+    public static StoreResponseDTO.ListWeeklyRankResponseDTO listWeeklyRankResponseDTO(
+            Long storeId, String storeName, List rows
+    ) {
+        List items = rows.stream()
+                .map(StoreConverter::weeklyRankItem)
+                .collect(Collectors.toList());
+
+        return StoreResponseDTO.ListWeeklyRankResponseDTO.builder()
+                .storeId(storeId)
+                .storeName(storeName)
+                .items(items)
+                .build();
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java b/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java
index bbe84ba..3eda65c 100644
--- a/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java
@@ -1,4 +1,29 @@
 package com.assu.server.domain.store.dto;
 
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
 public class StoreResponseDTO {
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class WeeklyRankResponseDTO {
+            private Long rank;           // 그 주 순위(1부터)
+            private Long usageCount;     // 그 주 사용 건수
+    }
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class ListWeeklyRankResponseDTO {
+        private Long storeId;
+        private String storeName;
+        private List items; // 과거→현재 (6개)
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index fc7b590..8669107 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -3,10 +3,96 @@
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.store.entity.Store;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Optional;
 
 public interface StoreRepository extends JpaRepository {
     Optional findByPartner(Partner  partner);
+
+    // [이번 주] 전체 스토어 중 특정 storeId의 주간 순위/건수 1건
+    @Query(value = """
+        WITH w AS (
+          SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AS week_start
+        ),
+        per_store AS (
+          SELECT
+            s.id                                        AS storeId,
+            s.name                                      AS storeName,
+            CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount,
+            (SELECT week_start FROM w)                  AS weekStart
+          FROM store s
+          LEFT JOIN paper p          ON p.store_id = s.id
+          LEFT JOIN paper_content pc ON pc.paper_id = p.id
+          LEFT JOIN partnership_usage pu
+                 ON pu.paper_id = pc.id
+                AND pu.created_at >= (SELECT week_start FROM w)
+                AND pu.created_at <  (SELECT week_start FROM w) + INTERVAL 7 DAY
+          GROUP BY s.id, s.name
+        )
+        SELECT
+          ps.weekStart  AS weekStart,
+          ps.storeId    AS storeId,
+          ps.storeName  AS storeName,
+          ps.usageCount AS usageCount,
+          CAST(
+            DENSE_RANK() OVER (ORDER BY ps.usageCount DESC, ps.storeId ASC)
+            AS UNSIGNED
+          )            AS storeRank
+        FROM per_store ps
+        WHERE ps.storeId = :storeId
+        """, nativeQuery = true)
+    List findGlobalWeeklyRankForStore(@Param("storeId") Long storeId);
+
+    interface GlobalWeeklyRankRow {
+        LocalDate getWeekStart();
+        Long getStoreId();
+        String getStoreName();
+        Long getUsageCount();
+        Long getStoreRank();
+    }
+
+    // [최근 6주] 전체 스토어 기준, 특정 storeId의 주간 순위/건수(월요일 시작) 추세
+    @Query(value = """
+        WITH RECURSIVE weeks AS (
+          SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AS week_start
+          UNION ALL
+          SELECT week_start - INTERVAL 7 DAY FROM weeks
+          WHERE week_start > DATE_SUB(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 5 WEEK)
+        ),
+        per_store_week AS (
+          SELECT
+            w.week_start                                  AS weekStart,
+            s.id                                          AS storeId,
+            s.name                                        AS storeName,
+            CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED)   AS usageCount
+          FROM weeks w
+          JOIN store s               ON 1=1
+          LEFT JOIN paper p          ON p.store_id = s.id
+          LEFT JOIN paper_content pc ON pc.paper_id = p.id
+          LEFT JOIN partnership_usage pu
+                 ON pu.paper_id = pc.id
+                AND pu.created_at >= w.week_start
+                AND pu.created_at <  w.week_start + INTERVAL 7 DAY
+          GROUP BY w.week_start, s.id, s.name
+        )
+        SELECT
+          pw.weekStart  AS weekStart,
+          pw.storeId    AS storeId,
+          pw.storeName  AS storeName,
+          pw.usageCount AS usageCount,
+          CAST(
+            DENSE_RANK() OVER (
+              PARTITION BY pw.weekStart
+              ORDER BY pw.usageCount DESC, pw.storeId ASC
+            ) AS UNSIGNED
+          )            AS storeRank
+        FROM per_store_week pw
+        WHERE pw.storeId = :storeId
+        ORDER BY pw.weekStart ASC
+        """, nativeQuery = true)
+    List findGlobalWeeklyTrendLast6Weeks(@Param("storeId") Long storeId);
 }
diff --git a/src/main/java/com/assu/server/domain/store/service/StoreService.java b/src/main/java/com/assu/server/domain/store/service/StoreService.java
index 15ad373..340edc7 100644
--- a/src/main/java/com/assu/server/domain/store/service/StoreService.java
+++ b/src/main/java/com/assu/server/domain/store/service/StoreService.java
@@ -1,4 +1,8 @@
 package com.assu.server.domain.store.service;
 
+import com.assu.server.domain.store.dto.StoreResponseDTO;
+
 public interface StoreService {
+    StoreResponseDTO.WeeklyRankResponseDTO getWeeklyRank();
+    StoreResponseDTO.ListWeeklyRankResponseDTO getListWeeklyRank();
 }
diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
index a92599e..bf3ffee 100644
--- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
@@ -1,4 +1,61 @@
 package com.assu.server.domain.store.service;
 
-public class StoreServiceImpl {
+import com.assu.server.domain.partner.entity.Partner;
+import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.store.converter.StoreConverter;
+import com.assu.server.domain.store.dto.StoreResponseDTO;
+import com.assu.server.domain.store.entity.Store;
+import com.assu.server.domain.store.repository.StoreRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.exception.exception.DatabaseException;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class StoreServiceImpl implements StoreService {
+    private final StoreRepository storeRepository;
+    private final PartnerRepository partnerRepository;
+
+    @Override
+    @Transactional
+    public StoreResponseDTO.WeeklyRankResponseDTO getWeeklyRank() {
+        // Long memberId = SecurityUtil.getCurrentUserId();
+        Long memberId = 2L;
+        Optional partner = partnerRepository.findById(memberId);
+        Store store = storeRepository.findByPartner(partner.orElse(null))
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+        Long storeId = store.getId();
+
+        List rows = storeRepository.findGlobalWeeklyRankForStore(storeId);
+        if (rows.isEmpty()) {
+            // 데이터가 없을 때 기본값 반환(필요 시 예외로 변경)
+            return StoreResponseDTO.WeeklyRankResponseDTO.builder()
+                    .rank(null)
+                    .usageCount(0L)
+                    .build();
+        }
+        return StoreConverter.weeklyRankResponseDTO(rows.get(0));
+    }
+
+    @Override
+    @Transactional
+    public StoreResponseDTO.ListWeeklyRankResponseDTO getListWeeklyRank() {
+        // Long memberId = SecurityUtil.getCurrentUserId();
+        Long memberId = 2L;
+        Optional partner = partnerRepository.findById(memberId);
+        Store store = storeRepository.findByPartner(partner.orElse(null))
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+        Long storeId = store.getId();
+
+        List rows = storeRepository.findGlobalWeeklyTrendLast6Weeks(storeId);
+
+        String storeName = rows.isEmpty() ? null : rows.get(0).getStoreName();
+        return StoreConverter.listWeeklyRankResponseDTO(storeId, storeName, rows);
+
+    }
 }

From 208953d3508d886693fddd1d3fbddb6e4d99e6ef Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Fri, 15 Aug 2025 21:28:35 +0900
Subject: [PATCH 057/270] =?UTF-8?q?feat/#13-review=20=20-=20=20=EC=B6=A9?=
 =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0=EC=A4=91=20=EB=B0=9C=EC=83=9D?=
 =?UTF-8?q?=EB=90=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?=
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 | 1 -
 1 file changed, 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 8eeb75f..c50ebd9 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -26,7 +26,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
                                 "/webjars/**"
                         ).permitAll()
                         .anyRequest().authenticated()
-                        .anyRequest().permitAll()  // ⭐ 모든 요청 허용
                 )
                 .csrf(csrf -> csrf.disable()) // websocket은 csrf 필요 없음
                 .formLogin(login -> login.disable())

From 4603ca212acacdabb87e6b0a17c50be4f30a5eea Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 15 Aug 2025 22:50:05 +1000
Subject: [PATCH 058/270] =?UTF-8?q?[MOD/#22]=20gitIgnore=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitignore | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 9503f1d..94fba86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,6 @@ out/
 ### Secret ###
 src/main/resources/application-secret.yml
 src/test/resources/application-test.yml
-src/test/resources/application-secret.yml
\ No newline at end of file
+src/test/resources/application-secret.yml
+
+resources/firebase/service-account.json
\ No newline at end of file

From 0e1ecefe4e73305e599987e608bf765289da1684 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 15 Aug 2025 23:33:02 +1000
Subject: [PATCH 059/270] =?UTF-8?q?[FIX/#22]=20=EC=98=A4=EB=A5=98=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../inquiry/controller/InquiryController.java | 39 ++++++++++---------
 .../inquiry/converter/InquiryConverter.java   |  2 +-
 .../inquiry/service/InquiryService.java       |  5 ++-
 .../inquiry/service/InquiryServiceImpl.java   | 37 ++++++++++++++++--
 .../server/global/config/SecurityConfig.java  |  1 +
 5 files changed, 60 insertions(+), 24 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index 93efb21..4432aa6 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -3,15 +3,18 @@
 import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO;
 import com.assu.server.domain.inquiry.dto.InquiryResponseDTO;
 import com.assu.server.domain.inquiry.service.InquiryService;
+import com.assu.server.global.apiPayload.BaseResponse;
+import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.web.PageableDefault;
-import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Map;
+
 @RestController
 @RequestMapping("/member/inquiries")
 @RequiredArgsConstructor
@@ -20,41 +23,39 @@ public class InquiryController {
     private final InquiryService inquiryService;
 
     @PostMapping
-    public ResponseEntity create(
+    public BaseResponse create(
             @RequestBody @Valid InquiryCreateRequestDTO req,
             @RequestParam Long memberId
     ) {
         Long id = inquiryService.create(req, memberId);
-        return ResponseEntity.ok(id);
+        return BaseResponse.onSuccess(SuccessStatus._OK, id);
     }
 
     @GetMapping
-    public Page list(
+    public BaseResponse> list(
             @RequestParam(defaultValue = "all") String status,
-            @PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "20") Integer size,
             @RequestParam Long memberId
     ) {
-        if (!"all".equalsIgnoreCase(status)
-                && !"waiting".equalsIgnoreCase(status)
-                && !"answered".equalsIgnoreCase(status)) {
-            throw new IllegalArgumentException("상태값: [all, waiting, answered]");
-        }
-        return inquiryService.list(status, pageable, memberId);
+        Map response = inquiryService.getInquiries(status, page, size, memberId);
+        return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
     /** 단건 상세 조회*/
-    @GetMapping("/{inquiry_id}")
-    public InquiryResponseDTO get(
-            @PathVariable Long id,
+    @GetMapping("/{inquiryId}")
+    public BaseResponse get(
+            @PathVariable("inquiryId") Long inquiryId,
             @RequestParam Long memberId
     ) {
-        return inquiryService.get(id, memberId);
+        InquiryResponseDTO response = inquiryService.get(inquiryId, memberId);
+        return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
     /** 운영자: 답변 완료 처리 */
-    @PatchMapping("/{inquiry_id}/answer")
-    public ResponseEntity markAnswered(@PathVariable Long id) {
-        inquiryService.markAnswered(id);
-        return ResponseEntity.noContent().build();
+    @PatchMapping("/{inquiryId}/answer")
+    public BaseResponse markAnswered(@PathVariable Long inquiryId) {
+        inquiryService.markAnswered(inquiryId);
+        return BaseResponse.onSuccess(SuccessStatus._OK, null);
     }
 }
diff --git a/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java b/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java
index a31a4e0..4b9108d 100644
--- a/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java
+++ b/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java
@@ -4,7 +4,7 @@
 import com.assu.server.domain.inquiry.entity.Inquiry;
 
 public class InquiryConverter {
-    public InquiryResponseDTO toDto(Inquiry i) {
+    public static InquiryResponseDTO toDto(Inquiry i) {
         return InquiryResponseDTO.builder()
                 .id(i.getId())
                 .title(i.getTitle())
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
index acce980..77af896 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
@@ -5,9 +5,12 @@
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 
+import java.util.Map;
+
+
 public interface InquiryService {
     Long create(InquiryCreateRequestDTO req, Long memberId);
-    Page list(String status, Pageable pageable, Long memberId);
+    Map getInquiries(String status, int page, int size, Long memberId);
     InquiryResponseDTO get(Long id, Long memberId);
     void markAnswered(Long id);
 }
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
index c5036c5..5aa332b 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.inquiry.service;
 
 import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.common.repository.MemberRepository;
 import com.assu.server.domain.inquiry.converter.InquiryConverter;
 import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO;
 import com.assu.server.domain.inquiry.dto.InquiryResponseDTO;
@@ -9,16 +10,20 @@
 import com.assu.server.domain.inquiry.repository.InquiryRepository;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 @Service
 @RequiredArgsConstructor
 public class InquiryServiceImpl implements InquiryService {
 
     private final InquiryRepository inquiryRepository;
-    private final InquiryConverter inquiryConverter;
     private final MemberRepository memberRepository;
 
     /** 문의 등록 */
@@ -42,6 +47,32 @@ public Long create(InquiryCreateRequestDTO req, Long memberId) {
     /** 문의 내역 조회 (status=all|waiting|answered) */
     @Transactional(readOnly = true)
     @Override
+    public Map getInquiries(String status, int page, int size, Long memberId) {
+        if (page < 1) {
+            throw new IllegalArgumentException("page는 1 이상이어야 합니다.");
+        }
+        if (size < 1 || size > 200) {
+            throw new IllegalArgumentException("size는 1~200 사이여야 합니다.");
+        }
+
+        String s = status.toLowerCase();
+        if (!s.equals("all") && !s.equals("waiting") && !s.equals("answered")) {
+            throw new IllegalArgumentException("status는 [all, waiting, answered] 중 하나여야 합니다.");
+        }
+
+        Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "id"));
+        Page p = list(s, pageable, memberId);
+
+        Map body = new LinkedHashMap<>();
+        body.put("items", p.getContent());
+        body.put("page", p.getNumber() + 1);
+        body.put("size", p.getSize());
+        body.put("totalPages", p.getTotalPages());
+        body.put("totalElements", p.getTotalElements());
+
+        return body;
+    }
+
     public Page list(String status, Pageable pageable, Long memberId) {
         Page page = switch (status.toLowerCase()) {
             case "waiting" -> inquiryRepository.findByMemberIdAndStatus(memberId, Status.WAITING, pageable);
@@ -50,7 +81,7 @@ public Page list(String status, Pageable pageable, Long memb
             default         -> throw new IllegalArgumentException("status must be one of [all, waiting, answered]");
         };
 
-        return page.map(inquiryConverter::toDto);
+        return page.map(InquiryConverter::toDto);
     }
 
     /** 단건 상세 조회 */
@@ -61,7 +92,7 @@ public InquiryResponseDTO get(Long id, Long memberId) {
         if (!inquiry.getMember().getId().equals(memberId)) {
             throw new IllegalArgumentException("not yours");
         }
-        return inquiryConverter.toDto(inquiry);
+        return InquiryConverter.toDto(inquiry);
     }
 
     /** 운영자가 답변 완료 처리 */
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 c50ebd9..4edd669 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -13,6 +13,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
                 .authorizeHttpRequests(auth -> auth
                         .requestMatchers(
+                                "/member/inquiries/**",
                                 "/chat/**",
                                 "/suggestion/**",
                                 "/review/**",

From fa1b8e78f08be3100b891ca7fa41a85c79ae7267 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 15 Aug 2025 23:46:01 +1000
Subject: [PATCH 060/270] =?UTF-8?q?[FEAT/#22]=20=EB=AC=B8=EC=9D=98=20?=
 =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../inquiry/controller/InquiryController.java | 27 ++++++++++++++++---
 .../inquiry/converter/InquiryConverter.java   |  1 +
 .../inquiry/dto/InquiryResponseDTO.java       |  2 ++
 .../server/domain/inquiry/entity/Inquiry.java |  7 +++--
 .../inquiry/service/InquiryService.java       |  2 +-
 .../inquiry/service/InquiryServiceImpl.java   | 16 +++++++----
 6 files changed, 44 insertions(+), 11 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index 4432aa6..f15203d 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -1,10 +1,12 @@
 package com.assu.server.domain.inquiry.controller;
 
+import com.assu.server.domain.inquiry.dto.InquiryAnswerRequestDTO;
 import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO;
 import com.assu.server.domain.inquiry.dto.InquiryResponseDTO;
 import com.assu.server.domain.inquiry.service.InquiryService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import io.swagger.v3.oas.annotations.Operation;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
@@ -22,6 +24,10 @@ public class InquiryController {
 
     private final InquiryService inquiryService;
 
+    @Operation(
+            summary = "문의를 생성하는 API입니다.",
+            description = "셍성 성공시 생성된 문의의 ID를 반환합니다."
+    )
     @PostMapping
     public BaseResponse create(
             @RequestBody @Valid InquiryCreateRequestDTO req,
@@ -31,6 +37,10 @@ public BaseResponse create(
         return BaseResponse.onSuccess(SuccessStatus._OK, id);
     }
 
+    @Operation(
+            summary = "문의 목록을 조회하는 API 입니다.",
+            description = "page는 1 이상이어야 합니다."
+    )
     @GetMapping
     public BaseResponse> list(
             @RequestParam(defaultValue = "all") String status,
@@ -43,6 +53,10 @@ public BaseResponse> list(
     }
 
     /** 단건 상세 조회*/
+    @Operation(
+            summary = "문의 단건 상세 조회 API 입니다.",
+            description = "문의 ID를 보내주세요."
+    )
     @GetMapping("/{inquiryId}")
     public BaseResponse get(
             @PathVariable("inquiryId") Long inquiryId,
@@ -52,10 +66,17 @@ public BaseResponse get(
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
-    /** 운영자: 답변 완료 처리 */
+    /** 문의 답변*/
+    @Operation(
+            summary = "운영자 답변 API입니다.",
+            description = "문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다."
+    )
     @PatchMapping("/{inquiryId}/answer")
-    public BaseResponse markAnswered(@PathVariable Long inquiryId) {
-        inquiryService.markAnswered(inquiryId);
+    public BaseResponse answer(
+            @PathVariable Long inquiryId,
+            @RequestBody @Valid InquiryAnswerRequestDTO req
+    ) {
+        inquiryService.answer(inquiryId, req.getAnswer());
         return BaseResponse.onSuccess(SuccessStatus._OK, null);
     }
 }
diff --git a/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java b/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java
index 4b9108d..0a7e98d 100644
--- a/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java
+++ b/src/main/java/com/assu/server/domain/inquiry/converter/InquiryConverter.java
@@ -11,6 +11,7 @@ public static InquiryResponseDTO toDto(Inquiry i) {
                 .content(i.getContent())
                 .email(i.getEmail())
                 .status(i.getStatus().name())
+                .answer(i.getAnswer())
                 .createdAt(i.getCreatedAt())
                 .build();
     }
diff --git a/src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java
index 39fc513..85bfe0a 100644
--- a/src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryResponseDTO.java
@@ -13,6 +13,7 @@ public class InquiryResponseDTO {
     private String content;
     private String email;
     private String status;
+    private String answer;
     private LocalDateTime createdAt;
 
     public static InquiryResponseDTO from(Inquiry inquiry) {
@@ -22,6 +23,7 @@ public static InquiryResponseDTO from(Inquiry inquiry) {
                 .content(inquiry.getContent())
                 .email(inquiry.getEmail())
                 .status(inquiry.getStatus().name())
+                .answer(inquiry.getAnswer())
                 .createdAt(inquiry.getCreatedAt())
                 .build();
     }
diff --git a/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java b/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java
index 4e9568c..e32cce4 100644
--- a/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java
+++ b/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java
@@ -35,12 +35,15 @@ public class Inquiry extends BaseEntity {
     @Column(nullable = false, length = 30)
     private Status status;
 
+    @Column(nullable = true, columnDefinition = "TEXT")
+    private String answer;
+
     private LocalDateTime answeredAt;
 
     public enum Status { WAITING, ANSWERED }
 
-    // 상태 전환
-    public void markAnswered() {
+    public void answer(String answerText) {
+        this.answer = answerText;
         this.status = Status.ANSWERED;
         this.answeredAt = LocalDateTime.now();
     }
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
index 77af896..f210f7a 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
@@ -12,5 +12,5 @@ public interface InquiryService {
     Long create(InquiryCreateRequestDTO req, Long memberId);
     Map getInquiries(String status, int page, int size, Long memberId);
     InquiryResponseDTO get(Long id, Long memberId);
-    void markAnswered(Long id);
+    void answer(Long inquiryId, String answerText);
 }
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
index 5aa332b..974233a 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
@@ -8,6 +8,7 @@
 import com.assu.server.domain.inquiry.entity.Inquiry;
 import com.assu.server.domain.inquiry.entity.Inquiry.Status;
 import com.assu.server.domain.inquiry.repository.InquiryRepository;
+import jakarta.persistence.EntityNotFoundException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
@@ -95,12 +96,17 @@ public InquiryResponseDTO get(Long id, Long memberId) {
         return InquiryConverter.toDto(inquiry);
     }
 
-    /** 운영자가 답변 완료 처리 */
+    // InquiryServiceImpl.java
     @Transactional
     @Override
-    public void markAnswered(Long id) {
-        Inquiry i = inquiryRepository.findById(id).orElseThrow();
-        i.markAnswered();
-        // TODO: 필요 시 '답변 완료' 알림 발송
+    public void answer(Long inquiryId, String answerText) {
+        Inquiry inquiry = inquiryRepository.findById(inquiryId)
+                .orElseThrow(() -> new EntityNotFoundException("Inquiry not found: " + inquiryId));
+
+        if (inquiry.getStatus() == Inquiry.Status.ANSWERED) {
+            throw new IllegalStateException("이미 답변 완료된 문의입니다.");
+        }
+
+        inquiry.answer(answerText); // 도메인 메서드
     }
 }
\ No newline at end of file

From d81dbe2873b7d8c3d4646c9bbfb91d3c08336f5b Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 15 Aug 2025 23:46:06 +1000
Subject: [PATCH 061/270] =?UTF-8?q?[FEAT/#22]=20=EB=AC=B8=EC=9D=98=20?=
 =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/inquiry/dto/InquiryAnswerRequestDTO.java    | 10 ++++++++++
 1 file changed, 10 insertions(+)
 create mode 100644 src/main/java/com/assu/server/domain/inquiry/dto/InquiryAnswerRequestDTO.java

diff --git a/src/main/java/com/assu/server/domain/inquiry/dto/InquiryAnswerRequestDTO.java b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryAnswerRequestDTO.java
new file mode 100644
index 0000000..0d2de37
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/inquiry/dto/InquiryAnswerRequestDTO.java
@@ -0,0 +1,10 @@
+package com.assu.server.domain.inquiry.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+
+@Getter
+public class InquiryAnswerRequestDTO {
+    @NotBlank(message = "answer는 비어 있을 수 없습니다.")
+    private String answer;
+}

From a31a4b0e46d35bbca4ebda91e627a61805d7c183 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 15 Aug 2025 23:56:15 +1000
Subject: [PATCH 062/270] =?UTF-8?q?[FEAT/#22]=20=EC=97=90=EB=9F=AC=20?=
 =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../inquiry/service/InquiryService.java       |  1 +
 .../inquiry/service/InquiryServiceImpl.java   | 42 +++++++++----------
 .../apiPayload/code/status/ErrorStatus.java   |  8 ++++
 3 files changed, 28 insertions(+), 23 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
index f210f7a..f267e4a 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryService.java
@@ -13,4 +13,5 @@ public interface InquiryService {
     Map getInquiries(String status, int page, int size, Long memberId);
     InquiryResponseDTO get(Long id, Long memberId);
     void answer(Long inquiryId, String answerText);
+    Page list(String status, Pageable pageable, Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
index 974233a..17dac4c 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
@@ -8,12 +8,10 @@
 import com.assu.server.domain.inquiry.entity.Inquiry;
 import com.assu.server.domain.inquiry.entity.Inquiry.Status;
 import com.assu.server.domain.inquiry.repository.InquiryRepository;
-import jakarta.persistence.EntityNotFoundException;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.exception.exception.DatabaseException;
 import lombok.RequiredArgsConstructor;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.*;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -31,7 +29,8 @@ public class InquiryServiceImpl implements InquiryService {
     @Transactional
     @Override
     public Long create(InquiryCreateRequestDTO req, Long memberId) {
-        Member member = memberRepository.getReferenceById(memberId);
+        Member member = memberRepository.findById(memberId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
 
         Inquiry inquiry = Inquiry.builder()
                 .member(member)
@@ -49,16 +48,12 @@ public Long create(InquiryCreateRequestDTO req, Long memberId) {
     @Transactional(readOnly = true)
     @Override
     public Map getInquiries(String status, int page, int size, Long memberId) {
-        if (page < 1) {
-            throw new IllegalArgumentException("page는 1 이상이어야 합니다.");
-        }
-        if (size < 1 || size > 200) {
-            throw new IllegalArgumentException("size는 1~200 사이여야 합니다.");
-        }
+        if (page < 1) throw new DatabaseException(ErrorStatus.PAGE_UNDER_ONE);
+        if (size < 1 || size > 200) throw new DatabaseException(ErrorStatus.PAGE_SIZE_INVALID);
 
         String s = status.toLowerCase();
         if (!s.equals("all") && !s.equals("waiting") && !s.equals("answered")) {
-            throw new IllegalArgumentException("status는 [all, waiting, answered] 중 하나여야 합니다.");
+            throw new DatabaseException(ErrorStatus.INVALID_INQUIRY_STATUS_FILTER);
         }
 
         Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "id"));
@@ -70,18 +65,17 @@ public Map getInquiries(String status, int page, int size, Long
         body.put("size", p.getSize());
         body.put("totalPages", p.getTotalPages());
         body.put("totalElements", p.getTotalElements());
-
         return body;
     }
 
+    @Override
     public Page list(String status, Pageable pageable, Long memberId) {
         Page page = switch (status.toLowerCase()) {
-            case "waiting" -> inquiryRepository.findByMemberIdAndStatus(memberId, Status.WAITING, pageable);
+            case "waiting"  -> inquiryRepository.findByMemberIdAndStatus(memberId, Status.WAITING, pageable);
             case "answered" -> inquiryRepository.findByMemberIdAndStatus(memberId, Status.ANSWERED, pageable);
             case "all"      -> inquiryRepository.findByMemberId(memberId, pageable);
-            default         -> throw new IllegalArgumentException("status must be one of [all, waiting, answered]");
+            default         -> throw new DatabaseException(ErrorStatus.INVALID_INQUIRY_STATUS_FILTER);
         };
-
         return page.map(InquiryConverter::toDto);
     }
 
@@ -89,24 +83,26 @@ public Page list(String status, Pageable pageable, Long memb
     @Transactional(readOnly = true)
     @Override
     public InquiryResponseDTO get(Long id, Long memberId) {
-        Inquiry inquiry = inquiryRepository.findById(id).orElseThrow();
+        Inquiry inquiry = inquiryRepository.findById(id)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_INQUIRY));
+
         if (!inquiry.getMember().getId().equals(memberId)) {
-            throw new IllegalArgumentException("not yours");
+            throw new DatabaseException(ErrorStatus.FORBIDDEN_INQUIRY);
         }
         return InquiryConverter.toDto(inquiry);
     }
 
-    // InquiryServiceImpl.java
+    /** 답변 저장(상태 ANSWERED 전환) */
     @Transactional
     @Override
     public void answer(Long inquiryId, String answerText) {
         Inquiry inquiry = inquiryRepository.findById(inquiryId)
-                .orElseThrow(() -> new EntityNotFoundException("Inquiry not found: " + inquiryId));
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_INQUIRY));
 
         if (inquiry.getStatus() == Inquiry.Status.ANSWERED) {
-            throw new IllegalStateException("이미 답변 완료된 문의입니다.");
+            throw new DatabaseException(ErrorStatus.ALREADY_ANSWERED);
         }
 
-        inquiry.answer(answerText); // 도메인 메서드
+        inquiry.answer(answerText);
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 8bf2f46..40cc033 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -18,6 +18,8 @@ public enum ErrorStatus implements BaseErrorCode {
 
     //페이징 에러
     PAGE_UNDER_ONE(HttpStatus.BAD_REQUEST,"PAGE_4001","페이지는 1이상이여야 합니다."),
+    PAGE_SIZE_INVALID(HttpStatus.BAD_REQUEST,"PAGE_4002","size는 1~200 사이여야 합니다."),
+
 
     // 멤버 에러
     NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."),
@@ -31,6 +33,12 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
 
+    // 문의(Inquiry)
+    INVALID_INQUIRY_STATUS_FILTER(HttpStatus.BAD_REQUEST,"INQUIRY_4001","status는 [all, waiting, answered] 중 하나여야 합니다."),
+    NO_SUCH_INQUIRY(HttpStatus.NOT_FOUND,"INQUIRY_4002","존재하지 않는 문의입니다."),
+    FORBIDDEN_INQUIRY(HttpStatus.FORBIDDEN,"INQUIRY_4003","해당 문의에 접근 권한이 없습니다."),
+    ALREADY_ANSWERED(HttpStatus.CONFLICT,"INQUIRY_4091","이미 답변 완료된 문의입니다."),
+
     ;
 
     private final HttpStatus httpStatus;

From e8918e94583c4435d814c4024aa704f583777aac Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Sat, 16 Aug 2025 08:56:46 +0900
Subject: [PATCH 063/270] =?UTF-8?q?[FEAT/#27]=20=ED=9A=8C=EC=9B=90?=
 =?UTF-8?q?=EA=B0=80=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=EB=A1=9C?=
 =?UTF-8?q?=EA=B7=B8=EC=95=84=EC=9B=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |  15 +
 .../server/domain/admin/entity/Admin.java     |   2 +-
 .../auth/controller/AuthController.java       | 281 +++++++++++++++++-
 .../AesGcmSchoolCredentialEncryptor.java      |  58 ++++
 .../crypto/SchoolCredentialEncryptor.java     |   6 +
 .../auth/crypto/StudentPasswordEncoder.java   |  35 +++
 .../domain/auth/dto/AuthResponseDTO.java      |   4 -
 .../domain/auth/dto/login/LoginRequest.java   |  28 ++
 .../domain/auth/dto/login/LoginResponse.java  |  32 ++
 .../auth/dto/login/RefreshResponse.java       |   4 +
 .../auth/dto/login/StudentLoginRequest.java   |  27 ++
 .../PhoneAuthRequestDTO.java}                 |   4 +-
 .../auth/dto/phone/PhoneAuthResponseDTO.java  |   4 +
 .../auth/dto/signup/AdminSignUpRequest.java   |  26 ++
 .../auth/dto/signup/PartnerSignUpRequest.java |  28 ++
 .../auth/dto/signup/SignUpResponse.java       |  23 ++
 .../auth/dto/signup/StudentSignUpRequest.java |  25 ++
 .../server/domain/auth/dto/signup/Tokens.java |  11 +
 .../dto/signup/common/CommonAuthPayload.java  |  22 ++
 .../dto/signup/common/CommonInfoPayload.java  |  23 ++
 .../signup/common/CommonSignUpRequest.java    |  27 ++
 .../signup/student/StudentAuthPayload.java    |  23 ++
 .../signup/student/StudentInfoPayload.java    |  36 +++
 .../server/domain/auth/entity/CommonAuth.java |  42 +++
 .../server/domain/auth/entity/Member.java     |  76 +++++
 .../server/domain/auth/entity/SSUAuth.java    |  44 +++
 .../domain/auth/exception/AuthException.java  |  11 -
 .../auth/exception/CustomAuthException.java   |  30 ++
 .../exception/annotation/PasswordMatches.java |  20 ++
 .../validator/PasswordMatchesValidator.java   |  36 +++
 .../auth/repository/CommonAuthRepository.java |  14 +
 .../auth/repository/MemberRepository.java     |  10 +
 .../auth/repository/SSUAuthRepository.java    |  14 +
 .../domain/auth/security/JwtAuthFilter.java   | 118 ++++++++
 .../server/domain/auth/security/JwtUtil.java  | 183 ++++++++++++
 .../domain/auth/security/SecurityUtil.java    |  23 ++
 .../common/CommonUserDetailsService.java      |  48 +++
 ...onUsernamePasswordAuthenticationToken.java |   9 +
 .../student/StudentUserDetailsService.java    |  38 +++
 ...ntUsernamePasswordAuthenticationToken.java |  10 +
 .../domain/auth/service/LoginService.java     |  12 +
 .../domain/auth/service/LoginServiceImpl.java | 103 +++++++
 .../domain/auth/service/LogoutService.java    |   5 +
 .../auth/service/LogoutServiceImpl.java       |  45 +++
 .../auth/service/PhoneAuthServiceImpl.java    |   4 +-
 .../domain/auth/service/SSUAuthService.java   |   6 +
 .../auth/service/SSUAuthServiceImpl.java      | 171 +++++++++++
 .../domain/auth/service/SignUpService.java    |  13 +
 .../auth/service/SignUpServiceImpl.java       | 230 ++++++++++++++
 .../domain/chat/converter/ChatConverter.java  |   3 +-
 .../server/domain/chat/entity/Message.java    |   4 +-
 .../domain/chat/service/ChatServiceImpl.java  |   6 +-
 .../domain/common/entity/CommonAuth.java      |  36 ---
 .../server/domain/common/entity/Member.java   |  47 ---
 .../server/domain/common/entity/SSUAuth.java  |  34 ---
 .../server/domain/common/enums/UserRole.java  |   2 +-
 .../common/repository/MemberRepository.java   |   9 -
 .../server/domain/partner/entity/Partner.java |   3 +-
 .../term/entity/mapping/TermAgreement.java    |   2 +-
 .../server/domain/user/entity/Student.java    |  11 +-
 .../domain/user/entity/enums/Department.java  |   5 +
 .../domain/user/entity/enums/Major.java       |   2 +-
 .../domain/user/entity/enums/University.java  |   5 +
 .../user/repository/StudentRepository.java    |   5 +-
 .../apiPayload/code/status/ErrorStatus.java   |  25 +-
 .../server/global/config/AmazonConfig.java    |  52 ++++
 .../global/config/AuthProviderConfig.java     |  59 ++++
 .../server/global/config/ProjectConfig.java   |  29 ++
 .../server/global/config/SecurityConfig.java  |  28 +-
 .../assu/server/global/config/WebConfig.java  |   8 -
 .../{exception => }/DatabaseException.java    |   2 +-
 .../{exception => }/GeneralException.java     |   2 +-
 .../GlobalExceptionAdvice.java                |   2 +-
 .../{exception => }/annotation/CheckPage.java |   4 +-
 .../validator/CheckPageValidator.java         |   4 +-
 .../global/util/AuthUserArgumentResolver.java |  44 ---
 .../server/global/util/PrincipalDetails.java  |  61 ----
 .../assu/server/infra/s3/AmazonS3Manager.java | 138 +++++++++
 ...MultipartJackson2HttpMessageConverter.java |  33 ++
 79 files changed, 2426 insertions(+), 298 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/dto/AuthResponseDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/login/LoginRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponse.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
 rename src/main/java/com/assu/server/domain/auth/dto/{AuthRequestDTO.java => phone/PhoneAuthRequestDTO.java} (90%)
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthResponseDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/Tokens.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonSignUpRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/entity/Member.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/exception/AuthException.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/exception/annotation/PasswordMatches.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/exception/validator/PasswordMatchesValidator.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/repository/CommonAuthRepository.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/repository/SSUAuthRepository.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/LoginService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/LogoutService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/SignUpService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java
 delete mode 100644 src/main/java/com/assu/server/domain/common/entity/CommonAuth.java
 delete mode 100644 src/main/java/com/assu/server/domain/common/entity/Member.java
 delete mode 100644 src/main/java/com/assu/server/domain/common/entity/SSUAuth.java
 delete mode 100644 src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
 create mode 100644 src/main/java/com/assu/server/domain/user/entity/enums/Department.java
 create mode 100644 src/main/java/com/assu/server/domain/user/entity/enums/University.java
 create mode 100644 src/main/java/com/assu/server/global/config/AmazonConfig.java
 create mode 100644 src/main/java/com/assu/server/global/config/AuthProviderConfig.java
 create mode 100644 src/main/java/com/assu/server/global/config/ProjectConfig.java
 rename src/main/java/com/assu/server/global/exception/{exception => }/DatabaseException.java (79%)
 rename src/main/java/com/assu/server/global/exception/{exception => }/GeneralException.java (88%)
 rename src/main/java/com/assu/server/global/exception/{exception => }/GlobalExceptionAdvice.java (99%)
 rename src/main/java/com/assu/server/global/exception/{exception => }/annotation/CheckPage.java (77%)
 rename src/main/java/com/assu/server/global/exception/{exception => }/validator/CheckPageValidator.java (87%)
 delete mode 100644 src/main/java/com/assu/server/global/util/AuthUserArgumentResolver.java
 delete mode 100644 src/main/java/com/assu/server/global/util/PrincipalDetails.java
 create mode 100644 src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
 create mode 100644 src/main/java/com/assu/server/infra/s3/MultipartJackson2HttpMessageConverter.java

diff --git a/build.gradle b/build.gradle
index 3d1eacb..8bbeed4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -45,6 +45,21 @@ dependencies {
 	implementation 'org.springframework.boot:spring-boot-starter-batch'
 	testImplementation 'org.springframework.batch:spring-batch-test'
 
+	//Jwt
+	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
+	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+
+	// GSON
+	implementation 'com.google.code.gson:gson:2.11.0'
+
+	// S3
+	implementation platform('software.amazon.awssdk:bom:2.25.33')
+	implementation 'software.amazon.awssdk:s3'
+
+	// jsoup (crawl)
+	implementation 'org.jsoup:jsoup:1.21.1'
+
 	// lombok
 	compileOnly 'org.projectlombok:lombok'
 	annotationProcessor 'org.projectlombok:lombok'
diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
index 6356573..085b9fc 100644
--- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java
+++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.admin.entity;
 
-import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.auth.entity.Member;
 import jakarta.persistence.Entity;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.MapsId;
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 0155147..21354f2 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,33 +1,84 @@
 package com.assu.server.domain.auth.controller;
 
-import com.assu.server.domain.auth.dto.AuthRequestDTO;
+import com.assu.server.domain.auth.dto.login.LoginRequest;
+import com.assu.server.domain.auth.dto.login.LoginResponse;
+import com.assu.server.domain.auth.dto.login.RefreshResponse;
+import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
+import com.assu.server.domain.auth.dto.phone.PhoneAuthRequestDTO;
+import com.assu.server.domain.auth.dto.signup.AdminSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.SignUpResponse;
+import com.assu.server.domain.auth.dto.signup.StudentSignUpRequest;
+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.SignUpService;
+import com.assu.server.domain.common.enums.UserRole;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+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 io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
-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 org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartRequest;
 
+@Tag(name = "Auth", description = "인증/회원가입 API")
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/auth")
 public class AuthController {
+
     private final PhoneAuthService phoneAuthService;
+    private final SignUpService signUpService;
+    private final LoginService loginService;
+    private final LogoutService logoutService;
 
+    @Operation(
+            summary = "휴대폰 인증번호 발송 API (추후 개발)",
+            description = "# v1.0\n" +
+                    "- 입력한 휴대폰 번호로 1회용 인증번호(OTP)를 발송합니다.\n" +
+                    "- 유효시간/재요청 제한 정책은 서버 설정에 따릅니다."
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "인증번호 발송 성공",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "400", description = "잘못된 요청 (형식/필수값 오류)",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
     @PostMapping("/phone-numbers/send")
     public BaseResponse sendAuthNumber(
-            @RequestBody @Valid AuthRequestDTO.PhoneAuthSendRequest request
+            @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthSendRequest request
     ) {
         phoneAuthService.sendAuthNumber(request.getPhoneNumber());
         return BaseResponse.onSuccess(SuccessStatus.SEND_AUTH_NUMBER_SUCCESS, null);
     }
 
+    @Operation(
+            summary = "휴대폰 인증번호 검증 API (추후 개발)",
+            description = "# v1.0\n" +
+                    "- 발송된 인증번호(OTP)를 검증합니다.\n" +
+                    "- 성공 시 서버에 휴대폰 인증 상태가 기록됩니다."
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "인증번호 검증 성공",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "400", description = "인증 실패/만료/형식 오류",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
     @PostMapping("/phone-numbers/verify")
     public BaseResponse checkAuthNumber(
-            @RequestBody @Valid AuthRequestDTO.PhoneAuthVerifyRequest request
+            @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthVerifyRequest request
     ) {
         phoneAuthService.verifyAuthNumber(
                 request.getPhoneNumber(),
@@ -35,4 +86,220 @@ public BaseResponse checkAuthNumber(
         );
         return BaseResponse.onSuccess(SuccessStatus.VERIFY_AUTH_NUMBER_SUCCESS, null);
     }
+
+    @Operation(
+            summary = "학생 회원가입 API",
+            description = "# v1.0 (2025-08-15)\n" +
+                    "- `application/json` 요청 바디를 사용합니다.\n" +
+                    "- 처리: users + ssu_auth 등 가입 레코드 생성, 휴대폰 인증 여부 확인.\n" +
+                    "- 성공 시 201(Created)과 생성된 memberId 반환."
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "201", description = "회원가입 성공",
+                    content = @Content(schema = @Schema(implementation = SignUpResponse.class))),
+            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "409", description = "중복(전화번호 등)으로 인한 충돌",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
+    @PostMapping(value = "/signup/student", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public BaseResponse signupStudent(
+            @Valid @RequestBody StudentSignUpRequest request) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupStudent(request));
+    }
+
+    @Operation(
+            summary = "제휴업체 회원가입 API",
+            description = "# v1.0 (2025-08-15)\n" +
+                    "- `multipart/form-data`로 호출합니다.\n" +
+                    "- 파트: `payload`(JSON, PartnerSignUpRequest) + `licenseImage`(파일, 사업자등록증).\n" +
+                    "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
+                    "- 성공 시 201(Created)과 생성된 memberId 반환."
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "201", description = "회원가입 성공",
+                    content = @Content(schema = @Schema(implementation = SignUpResponse.class))),
+            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류(비밀번호 불일치 등)",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "409", description = "중복(전화/이메일)으로 인한 충돌",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
+    @PostMapping(value = "/signup/partner", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public BaseResponse signupPartner(
+            @Valid @RequestPart("request")
+            @Parameter(
+                    description = "JSON 형식의 제휴업체 가입 정보",
+                    // 'request' 파트의 content type을 명시적으로 지정
+                    content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
+                            schema = @Schema(implementation = PartnerSignUpRequest.class))
+            )
+            PartnerSignUpRequest request,
+
+            @RequestPart("licenseImage")
+            @Parameter(
+                    description = "사업자등록증 이미지 파일 (Multipart Part)",
+                    required = true,
+                    content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                            schema = @Schema(type = "string", format = "binary"))
+            )
+            MultipartFile licenseImage
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupPartner(request, licenseImage));
+    }
+
+    @Operation(
+            summary = "관리자 회원가입 API",
+            description = "# v1.0 (2025-08-15)\n" +
+                    "- `multipart/form-data`로 호출합니다.\n" +
+                    "- 파트: `payload`(JSON, AdminSignUpRequest) + `signImage`(파일, 신분증).\n" +
+                    "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
+                    "- 성공 시 201(Created)과 생성된 memberId 반환."
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "201", description = "회원가입 성공",
+                    content = @Content(schema = @Schema(implementation = SignUpResponse.class))),
+            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류(비밀번호 불일치 등)",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "409", description = "중복(전화/이메일)으로 인한 충돌",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
+    @PostMapping(value = "/signup/admin", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public BaseResponse signupAdmin(
+            @Valid @RequestPart("request")
+            @Parameter(
+                    description = "JSON 형식의 관리자 가입 정보",
+                    // 'request' 파트의 content type을 명시적으로 지정
+                    content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
+                            schema = @Schema(implementation = AdminSignUpRequest.class))
+            )
+            AdminSignUpRequest request,
+            @RequestPart("signImage")
+            @Parameter(
+                    description = "인감 이미지 파일 (Multipart Part)",
+                    required = true,
+                    content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                            schema = @Schema(type = "string", format = "binary"))
+            )
+            MultipartFile signImage) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupAdmin(request, signImage));
+    }
+
+
+    // 로그인 (파트너/관리자 공통)
+    @Operation(
+            summary = "로그인 API",
+            description = "# v1.0 (2025-08-15)\n" +
+                    "- `application/json`로 호출합니다.\n" +
+                    "- 바디: `LoginRequest(email, password)`.\n" +
+                    "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
+                    "- 성공 시 200(OK)과 토큰/만료시각 반환."
+    )
+    @io.swagger.v3.oas.annotations.parameters.RequestBody(
+            required = true,
+            content = @Content(schema = @Schema(implementation = LoginRequest.class))
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "로그인 성공",
+                    content = @Content(schema = @Schema(implementation = LoginResponse.class))),
+            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "401", description = "인증 실패(자격 증명 오류)",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
+    @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public BaseResponse login(
+            @RequestBody @Valid LoginRequest request
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, loginService.login(request));
+    }
+
+
+    // 학생 로그인
+    @Operation(
+            summary = "학생 로그인 API",
+            description = "# v1.0 (2025-08-15)\n" +
+                    "- `application/json`로 호출합니다.\n" +
+                    "- 바디: `StudentLoginRequest(email, password)`.\n" +
+                    "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
+                    "- 성공 시 200(OK)과 토큰/만료시각 반환."
+    )
+    @io.swagger.v3.oas.annotations.parameters.RequestBody(
+            required = true,
+            content = @Content(schema = @Schema(implementation = StudentLoginRequest.class))
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "로그인 성공",
+                    content = @Content(schema = @Schema(implementation = LoginResponse.class))),
+            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "401", description = "인증 실패(자격 증명 오류)",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
+    @PostMapping(value = "/login/student", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public BaseResponse login(
+            @RequestBody @Valid StudentLoginRequest request
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, loginService.loginStudent(request));
+    }
+
+
+    // 액세스 토큰 갱신
+    @Operation(
+            summary = "Access Token 갱신 API",
+            description = "# v1.0 (2025-08-15)\n" +
+                    "- 헤더로 호출합니다.\n" +
+                    "- 헤더: `Authorization: Bearer `(만료 허용), `RefreshToken: `.\n" +
+                    "- 처리: Refresh 검증/회전 후 신규 Access/Refresh 발급 및 저장.\n" +
+                    "- 성공 시 200(OK)과 새 토큰/만료시각 반환."
+    )
+    @Parameters({
+            @Parameter(name = "Authorization", description = "Access Token (만료 허용). 형식: `Bearer `", required = true,
+                    in = ParameterIn.HEADER, schema = @Schema(type = "string")),
+            @Parameter(name = "RefreshToken", description = "Refresh Token", required = true,
+                    in = ParameterIn.HEADER, schema = @Schema(type = "string"))
+    })
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "토큰 재발급 성공",
+                    content = @Content(schema = @Schema(implementation = RefreshResponse.class))),
+            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "401", description = "인증 실패(토큰 오류/만료)",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "403", description = "접근 거부",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
+    @PostMapping("/refresh")
+    public BaseResponse refreshToken(
+            @RequestHeader("RefreshToken") String refreshToken
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, loginService.refresh(refreshToken));
+    }
+
+
+    // 로그아웃
+    @Operation(
+            summary = "로그아웃 API",
+            description = "# v1.0 (2025-08-15)\n" +
+                    "- 헤더로 호출합니다.\n" +
+                    "- 헤더: `Authorization: Bearer `.\n" +
+                    "- 처리: Refresh 무효화(선택), Access 블랙리스트 등록.\n" +
+                    "- 성공 시 200(OK)."
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "로그아웃 성공",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
+            @ApiResponse(responseCode = "401", description = "인증 실패(토큰 오류/만료)",
+                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
+    })
+    @DeleteMapping("/logout")
+    public BaseResponse logout(
+            @RequestHeader("Authorization")
+            @Parameter(name = "Authorization", description = "Access Token. 형식: `Bearer `", required = true,
+                            in = ParameterIn.HEADER, schema = @Schema(type = "string"))
+            String accessToken
+    ) {
+        logoutService.logout(accessToken);
+        return BaseResponse.onSuccess(SuccessStatus._OK, null);
+    }
+
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java b/src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java
new file mode 100644
index 0000000..f75ea2a
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java
@@ -0,0 +1,58 @@
+package com.assu.server.domain.auth.crypto;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Base64;
+
+public class AesGcmSchoolCredentialEncryptor implements SchoolCredentialEncryptor {
+
+    private static final String TRANSFORMATION = "AES/GCM/NoPadding";
+    private static final int GCM_TAG_BITS = 128;   // 16 bytes tag
+    private static final int IV_BYTES = 12;        // 96-bit IV (권장)
+    private final SecretKey key;
+    private final SecureRandom random = new SecureRandom();
+
+    public AesGcmSchoolCredentialEncryptor(byte[] keyBytes) {
+        this.key = new SecretKeySpec(keyBytes, "AES");
+    }
+
+    @Override
+    public String encrypt(String plain) {
+        try {
+            byte[] iv = new byte[IV_BYTES];
+            random.nextBytes(iv);
+
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+            cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
+            byte[] ct = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
+
+            byte[] out = new byte[iv.length + ct.length];
+            System.arraycopy(iv, 0, out, 0, iv.length);
+            System.arraycopy(ct, 0, out, iv.length, ct.length);
+            return Base64.getEncoder().encodeToString(out);
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to encrypt school credential", e);
+        }
+    }
+
+    @Override
+    public String decrypt(String cipherB64) {
+        try {
+            byte[] all = Base64.getDecoder().decode(cipherB64);
+            byte[] iv = Arrays.copyOfRange(all, 0, IV_BYTES);
+            byte[] ct = Arrays.copyOfRange(all, IV_BYTES, all.length);
+
+            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+            cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
+            byte[] pt = cipher.doFinal(ct);
+            return new String(pt, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to decrypt school credential", e);
+        }
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java b/src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java
new file mode 100644
index 0000000..547c9d4
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java
@@ -0,0 +1,6 @@
+package com.assu.server.domain.auth.crypto;
+
+public interface SchoolCredentialEncryptor {
+    String encrypt(String plain);   // -> Base64(iv+ciphertext)
+    String decrypt(String cipher);  // Base64(iv+ciphertext) -> plain
+}
diff --git a/src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java b/src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java
new file mode 100644
index 0000000..50ee46a
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java
@@ -0,0 +1,35 @@
+package com.assu.server.domain.auth.crypto;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class StudentPasswordEncoder implements PasswordEncoder {
+
+    private final SchoolCredentialEncryptor encryptor;
+
+    @Override
+    public String encode(CharSequence rawPassword) {
+        // 회원가입/갱신 시 암호문 저장이 필요하면 사용 (AES-GCM 암호화)
+        return encryptor.encrypt(rawPassword.toString());
+    }
+
+    @Override
+    public boolean matches(CharSequence rawPassword, String encodedCipher) {
+        try {
+            String plain = encryptor.decrypt(encodedCipher);
+            return constantTimeEquals(plain, rawPassword.toString());
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private boolean constantTimeEquals(String a, String b) {
+        if (a == null || b == null || a.length() != b.length()) return false;
+        int r = 0;
+        for (int i = 0; i < a.length(); i++) r |= a.charAt(i) ^ b.charAt(i);
+        return r == 0;
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/AuthResponseDTO.java b/src/main/java/com/assu/server/domain/auth/dto/AuthResponseDTO.java
deleted file mode 100644
index 458a3b1..0000000
--- a/src/main/java/com/assu/server/domain/auth/dto/AuthResponseDTO.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.auth.dto;
-
-public class AuthResponseDTO {
-}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/LoginRequest.java b/src/main/java/com/assu/server/domain/auth/dto/login/LoginRequest.java
new file mode 100644
index 0000000..f5930b1
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/login/LoginRequest.java
@@ -0,0 +1,28 @@
+package com.assu.server.domain.auth.dto.login;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.*;
+
+/** 파트너/관리자 공통 로그인 요청 */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Schema(description = "파트너/관리자 공통 로그인 요청")
+public class LoginRequest {
+
+    @Schema(description = "로그인 이메일", example = "user@example.com")
+    @NotBlank(message = "이메일은 필수입니다.")
+    @Email(message = "올바른 이메일 형식이 아닙니다.")
+    @Size(max = 255, message = "이메일은 255자를 넘을 수 없습니다.")
+    private String email;
+
+    @Schema(description = "로그인 비밀번호(평문)", example = "P@ssw0rd!")
+    @NotBlank(message = "비밀번호는 필수입니다.")
+    @Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.")
+    private String password;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java b/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java
new file mode 100644
index 0000000..d456177
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java
@@ -0,0 +1,32 @@
+package com.assu.server.domain.auth.dto.login;
+
+import com.assu.server.domain.auth.dto.signup.Tokens;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.common.enums.UserRole;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Schema(description = "로그인 성공 응답")
+public class LoginResponse {
+
+    @Schema(description = "회원 ID", example = "123")
+    private Long memberId;
+
+    @Schema(description = "회원 역할", example = "STUDENT")
+    private UserRole role;
+
+    @Schema(description = "회원 상태", example = "SUSPEND")
+    private ActivationStatus status;
+
+    @Schema(description = "액세스 토큰/리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
+    private Tokens tokens;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponse.java b/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponse.java
new file mode 100644
index 0000000..4a983b5
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/login/RefreshResponse.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.auth.dto.login;
+
+public record RefreshResponse(Long memberId, String newAccess, String newRefresh) {
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java b/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
new file mode 100644
index 0000000..c6b334e
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
@@ -0,0 +1,27 @@
+package com.assu.server.domain.auth.dto.login;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.*;
+
+/** 학생 로그인 요청 (현재 서비스 로직이 이메일/비밀번호를 사용 중이면 LoginRequest를 그대로 사용해도 됩니다) */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Schema(description = "학생 로그인 요청")
+public class StudentLoginRequest {
+
+    @Schema(description = "학번", example = "student@example.com")
+    @NotBlank(message = "학번은 필수입니다.")
+    @Size(max = 10, message = "이메일은 10자를 넘을 수 없습니다.")
+    private String studentNumber;
+
+    @Schema(description = "로그인 비밀번호(평문)", example = "P@ssw0rd!")
+    @NotBlank(message = "비밀번호는 필수입니다.")
+    @Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.")
+    private String studentPassword;
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/dto/AuthRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthRequestDTO.java
similarity index 90%
rename from src/main/java/com/assu/server/domain/auth/dto/AuthRequestDTO.java
rename to src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthRequestDTO.java
index 9fe4319..d4179f0 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/AuthRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthRequestDTO.java
@@ -1,4 +1,4 @@
-package com.assu.server.domain.auth.dto;
+package com.assu.server.domain.auth.dto.phone;
 
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Pattern;
@@ -7,7 +7,7 @@
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-public class AuthRequestDTO {
+public class PhoneAuthRequestDTO {
 
     @Builder
     @Getter
diff --git a/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthResponseDTO.java b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthResponseDTO.java
new file mode 100644
index 0000000..5c26bc8
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/phone/PhoneAuthResponseDTO.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.auth.dto.phone;
+
+public class PhoneAuthResponseDTO {
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequest.java
new file mode 100644
index 0000000..65458db
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/AdminSignUpRequest.java
@@ -0,0 +1,26 @@
+package com.assu.server.domain.auth.dto.signup;
+
+import com.assu.server.domain.auth.dto.signup.common.CommonAuthPayload;
+import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
+import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+/** 관리자 가입: multipart payload(JSON) */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder
+public class AdminSignUpRequest extends CommonSignUpRequest {
+
+    @Valid
+    @NotNull
+    private CommonAuthPayload commonAuth;
+
+    @Valid
+    @NotNull
+    private CommonInfoPayload commonInfo;
+    // signImage는 @RequestPart MultipartFile 로 별도 수신
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequest.java
new file mode 100644
index 0000000..0c30e7a
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/PartnerSignUpRequest.java
@@ -0,0 +1,28 @@
+package com.assu.server.domain.auth.dto.signup;
+
+import com.assu.server.domain.auth.dto.signup.common.CommonAuthPayload;
+import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
+import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+/** 제휴업체 가입: multipart payload(JSON) */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder
+public class PartnerSignUpRequest extends CommonSignUpRequest {
+
+    @Valid
+    @NotNull
+    private CommonAuthPayload commonAuth;
+
+    @Valid
+    @NotNull
+    private CommonInfoPayload commonInfo;
+    // licenseImage는 @RequestPart MultipartFile 로 별도 수신
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java b/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java
new file mode 100644
index 0000000..617d501
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java
@@ -0,0 +1,23 @@
+package com.assu.server.domain.auth.dto.signup;
+
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SignUpResponse {
+    private Long memberId;
+    private UserRole role;
+    private ActivationStatus status;
+    private Tokens tokens;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java
new file mode 100644
index 0000000..b4e378c
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java
@@ -0,0 +1,25 @@
+package com.assu.server.domain.auth.dto.signup;
+
+import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.student.StudentAuthPayload;
+import com.assu.server.domain.auth.dto.signup.student.StudentInfoPayload;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+/** 학생 가입: JSON */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder
+public class StudentSignUpRequest extends CommonSignUpRequest {
+
+    @Valid
+    @NotNull
+    private StudentAuthPayload studentAuth;
+
+    @Valid
+    @NotNull
+    private StudentInfoPayload studentInfo;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/Tokens.java b/src/main/java/com/assu/server/domain/auth/dto/signup/Tokens.java
new file mode 100644
index 0000000..e414d77
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/Tokens.java
@@ -0,0 +1,11 @@
+package com.assu.server.domain.auth.dto.signup;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class Tokens {
+    private String accessToken;
+    private String refreshToken;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java
new file mode 100644
index 0000000..fcdccd9
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java
@@ -0,0 +1,22 @@
+package com.assu.server.domain.auth.dto.signup.common;
+
+import com.assu.server.domain.auth.exception.annotation.PasswordMatches;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Email;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class CommonAuthPayload {
+    @Email @NotBlank
+    private String email;
+
+    @Size(min = 8, max = 72) @NotBlank
+    private String password;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java
new file mode 100644
index 0000000..f9b8cb0
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java
@@ -0,0 +1,23 @@
+package com.assu.server.domain.auth.dto.signup.common;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class CommonInfoPayload {
+    @Size(min = 1, max = 50) @NotBlank
+    private String name;
+
+    @Size(min = 1, max = 255) @NotBlank
+    private String address;
+
+    @Size(max = 255)
+    private String detailAddress;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonSignUpRequest.java
new file mode 100644
index 0000000..80b7570
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonSignUpRequest.java
@@ -0,0 +1,27 @@
+package com.assu.server.domain.auth.dto.signup.common;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+
+/** 공통 필드 */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder
+public class CommonSignUpRequest {
+
+    @Pattern(regexp = "^(01[016789])\\d{3,4}\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다.")
+    @NotBlank
+    private String phoneNumber;
+
+    @NotNull
+    private Boolean marketingAgree;
+
+    @NotNull
+    private Boolean locationAgree;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java
new file mode 100644
index 0000000..89eeef2
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java
@@ -0,0 +1,23 @@
+package com.assu.server.domain.auth.dto.signup.student;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class StudentAuthPayload {
+    @Pattern(regexp = "^\\d{8,10}$", message = "학번은 숫자 8~10자리여야 합니다.")
+    @NotBlank
+    private String studentNumber;
+
+    @Size(min = 4, max = 64, message = "비밀번호 길이가 올바르지 않습니다.")
+    @NotBlank
+    private String studentPassword; // 저장 전 대칭키 암호화 권장
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java
new file mode 100644
index 0000000..d1477a7
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java
@@ -0,0 +1,36 @@
+package com.assu.server.domain.auth.dto.signup.student;
+
+import com.assu.server.domain.user.entity.enums.Department;
+import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
+import com.assu.server.domain.user.entity.enums.Major;
+import com.assu.server.domain.user.entity.enums.University;
+import jakarta.validation.constraints.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class StudentInfoPayload {
+
+    @NotNull
+    private Department department;          // 단과대
+
+    @NotNull
+    private EnrollmentStatus enrollmentStatus; // 재학 상태: ENROLLED, LEAVE, GRADUATED
+
+    @NotBlank
+    @Pattern(regexp = "^[1-5]{1}-[1-2]$", message = "yearSemester는 Y-N 형식이어야 합니다. 예: 4-1")
+    @Size(max = 10)
+    private String yearSemester;        // 예: 2025-1
+
+    @NotNull
+    private University university;          // 학교명
+
+    @NotBlank
+    @Size(max = 50)
+    private String major;               // 전공
+}
diff --git a/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java b/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java
new file mode 100644
index 0000000..00d6dbb
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java
@@ -0,0 +1,42 @@
+package com.assu.server.domain.auth.entity;
+
+import com.assu.server.domain.common.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(
+        name = "common_auth",
+        uniqueConstraints = {
+                @UniqueConstraint(name = "ux_common_auth_email", columnNames = {"email"})
+        }
+)
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class CommonAuth extends BaseEntity {
+
+    @Id
+    @Column(name = "member_id")
+    private Long id;
+
+    @OneToOne @MapsId
+    @JoinColumn(name = "member_id", referencedColumnName = "id")
+    private Member member;
+
+    @Column(name = "email", length = 255, nullable = false)
+    private String email;
+
+    @Column(name = "password", length = 255, nullable = false)
+    private String password; // 해시 저장
+
+    @Column(name = "is_email_verified", nullable = false)
+    private Boolean isEmailVerified = Boolean.FALSE;
+
+    @Column(name = "last_login_at")
+    private LocalDateTime lastLoginAt;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/entity/Member.java b/src/main/java/com/assu/server/domain/auth/entity/Member.java
new file mode 100644
index 0000000..375a1a8
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/entity/Member.java
@@ -0,0 +1,76 @@
+package com.assu.server.domain.auth.entity;
+
+import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.common.entity.BaseEntity;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.domain.partner.entity.Partner;
+import com.assu.server.domain.user.entity.Student;
+import jakarta.persistence.*;
+import lombok.*;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+
+import java.time.LocalDateTime;
+
+
+@Getter
+@Setter
+@DynamicUpdate
+@DynamicInsert
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Entity
+public class Member extends BaseEntity {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String phoneNum;
+
+    private Boolean isPhoneVerified;
+
+    private LocalDateTime phoneVerifiedAt;
+
+    private String profileUrl;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "role", nullable = false)
+    @JdbcTypeCode(SqlTypes.VARCHAR)
+    private UserRole role;  // STUDENT, ADMIN, PARTNER
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "is_activated", nullable = false)
+    @JdbcTypeCode(SqlTypes.VARCHAR)
+    private ActivationStatus isActivated;  // ACTIVE, INACTIVE, SUSPEND
+
+    @Column(nullable = true, unique = true)
+    private String refreshToken;
+
+    @Column(nullable = true, unique = true)
+    private String accessToken;
+
+    // 역할별 프로필 - 선택적으로 연관
+    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
+    private Student studentProfile;
+
+    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
+    private Admin adminProfile;
+
+    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
+    private Partner partnerProfile;
+
+    // 스키마가 BIGINT라서 Long 사용 (필요 시 VARCHAR로 변경)
+    @Column(name = "fcm_token")
+    private Long fcmToken;
+
+    // 연관관계 (1:1) — 양방향 필요 없으면 아래 필드 제거해도 됨
+    @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
+    private SSUAuth ssuAuth;
+
+    @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
+    private CommonAuth commonAuth;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
new file mode 100644
index 0000000..4c820bd
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
@@ -0,0 +1,44 @@
+package com.assu.server.domain.auth.entity;
+
+import com.assu.server.domain.common.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(
+        name = "ssu_auth",
+        indexes = {
+                @Index(name = "ux_ssu_auth_student_id", columnList = "student_id", unique = true)
+        }
+)
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SSUAuth extends BaseEntity {
+
+    @Id
+    @Column(name = "member_id")
+    private Long id;
+
+    @OneToOne @MapsId
+    @JoinColumn(name = "member_id", referencedColumnName = "id")
+    private Member member;
+
+    @Column(name = "student_number", length = 20, nullable = false)
+    private String studentNumber;
+
+    // TEXT 컬럼
+    @Lob
+    @Column(name = "password_cipher", columnDefinition = "TEXT", nullable = false)
+    private String passwordCipher;
+
+    @Column(name = "is_authenticated", nullable = false)
+    private Boolean isAuthenticated = Boolean.FALSE;
+
+    @Column(name = "authenticated_at")
+    private LocalDateTime authenticatedAt;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/exception/AuthException.java b/src/main/java/com/assu/server/domain/auth/exception/AuthException.java
deleted file mode 100644
index fe63a1c..0000000
--- a/src/main/java/com/assu/server/domain/auth/exception/AuthException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.assu.server.domain.auth.exception;
-
-import com.assu.server.global.apiPayload.code.BaseErrorCode;
-import com.assu.server.global.exception.exception.GeneralException;
-
-public class AuthException extends GeneralException {
-
-    public AuthException(BaseErrorCode code) {
-        super(code);
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
new file mode 100644
index 0000000..e98e8a2
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
@@ -0,0 +1,30 @@
+package com.assu.server.domain.auth.exception;
+
+import com.assu.server.global.apiPayload.code.BaseErrorCode;
+import org.springframework.http.HttpStatus;
+
+public class CustomAuthException extends RuntimeException {
+
+    private final BaseErrorCode errorCode;
+    private final String code;
+    private final HttpStatus httpStatus;
+
+    public CustomAuthException(BaseErrorCode errorCode) {
+        super(errorCode.getReasonHttpStatus().getMessage());
+        this.errorCode = errorCode;
+        this.code = errorCode.getReasonHttpStatus().getCode();
+        this.httpStatus = errorCode.getReasonHttpStatus().getHttpStatus();
+    }
+
+    public BaseErrorCode getErrorCode() {
+        return errorCode;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public HttpStatus getHttpStatus() {
+        return httpStatus;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/exception/annotation/PasswordMatches.java b/src/main/java/com/assu/server/domain/auth/exception/annotation/PasswordMatches.java
new file mode 100644
index 0000000..5b8edb5
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/exception/annotation/PasswordMatches.java
@@ -0,0 +1,20 @@
+package com.assu.server.domain.auth.exception.annotation;
+
+import com.assu.server.domain.auth.exception.validator.PasswordMatchesValidator;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = PasswordMatchesValidator.class)
+public @interface PasswordMatches {
+    String message() default "Password fields do not match";
+    Class[] groups() default {};
+    Class[] payload() default {};
+
+    String password();
+    String confirm();
+}
diff --git a/src/main/java/com/assu/server/domain/auth/exception/validator/PasswordMatchesValidator.java b/src/main/java/com/assu/server/domain/auth/exception/validator/PasswordMatchesValidator.java
new file mode 100644
index 0000000..fbb5faf
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/exception/validator/PasswordMatchesValidator.java
@@ -0,0 +1,36 @@
+package com.assu.server.domain.auth.exception.validator;
+
+import com.assu.server.domain.auth.exception.annotation.PasswordMatches;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.lang.reflect.Field;
+
+public class PasswordMatchesValidator implements ConstraintValidator {
+
+    private String passwordField;
+    private String confirmField;
+
+    @Override
+    public void initialize(PasswordMatches constraintAnnotation) {
+        this.passwordField = constraintAnnotation.password();
+        this.confirmField = constraintAnnotation.confirm();
+    }
+
+    @Override
+    public boolean isValid(Object value, ConstraintValidatorContext context) {
+        try {
+            Field pw = value.getClass().getDeclaredField(passwordField);
+            Field cf = value.getClass().getDeclaredField(confirmField);
+            pw.setAccessible(true);
+            cf.setAccessible(true);
+            Object p = pw.get(value);
+            Object c = cf.get(value);
+            if (p == null || c == null) return false;
+            return p.equals(c);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/repository/CommonAuthRepository.java b/src/main/java/com/assu/server/domain/auth/repository/CommonAuthRepository.java
new file mode 100644
index 0000000..26ed3f1
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/repository/CommonAuthRepository.java
@@ -0,0 +1,14 @@
+package com.assu.server.domain.auth.repository;
+
+import com.assu.server.domain.auth.entity.CommonAuth;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface CommonAuthRepository extends JpaRepository {
+    boolean existsByEmail(String email);
+
+    Optional findByEmail(String email);
+}
diff --git a/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
new file mode 100644
index 0000000..fa2352a
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
@@ -0,0 +1,10 @@
+package com.assu.server.domain.auth.repository;
+
+import com.assu.server.domain.auth.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface MemberRepository extends JpaRepository {
+    boolean existsByPhoneNum(String phoneNum);
+}
diff --git a/src/main/java/com/assu/server/domain/auth/repository/SSUAuthRepository.java b/src/main/java/com/assu/server/domain/auth/repository/SSUAuthRepository.java
new file mode 100644
index 0000000..09079d3
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/repository/SSUAuthRepository.java
@@ -0,0 +1,14 @@
+package com.assu.server.domain.auth.repository;
+
+import com.assu.server.domain.auth.entity.SSUAuth;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface SSUAuthRepository extends JpaRepository {
+    boolean existsByStudentNumber(String studentNumber);
+
+    Optional findByStudentNumber(String studentNumber);
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
new file mode 100644
index 0000000..f2a54e5
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
@@ -0,0 +1,118 @@
+package com.assu.server.domain.auth.security;
+
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+    @Value("${jwt.header}")
+    private String jwtHeader;
+    private final JwtUtil jwtUtil;
+    private final RedisTemplate redisTemplate;
+
+    private static final AntPathMatcher PATH = new AntPathMatcher();
+    private static final String[] WHITELIST = {
+            "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
+            "/swagger-resources/**", "/webjars/**",
+            "/auth/**",           // ← 로그인/회원가입/인증 등은 토큰 없이 접근
+            "/chat/**", "/suggestion/**", "/review/**",
+            "/ws/**", "/pub/**", "/sub/**"
+    };
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; // CORS preflight 우회
+        if (PATH.match("/auth/refresh", uri)) return false;               // 토큰 재발급은 필터 적용
+        for (String p : WHITELIST) if (PATH.match(p, uri)) return true;   // 나머지 공개 경로 우회
+        return false;                                                     // 보호 자원은 필터 적용
+    }
+
+    private static void checkAuthorizationHeader(String header) {
+        log.info("-------------------#@@@@@------------------");
+        if(header == null) {
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+        } else if (!header.startsWith("Bearer ")) {
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
+        }
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+
+        final String authHeader = request.getHeader(jwtHeader);
+
+        // Refresh 전용 처리
+        if (PATH.match("/auth/refresh", request.getRequestURI())) {
+            final String refreshToken = request.getHeader("refreshToken");
+            try {
+                // 둘 다 필수
+                checkAuthorizationHeader(authHeader);
+                if (refreshToken == null) throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+
+                String accessToken = JwtUtil.getTokenFromHeader(authHeader);
+                Claims claims = jwtUtil.validateTokenOnlySignature(accessToken); // 서명만 검증(만료 허용)
+                Authentication authentication = jwtUtil.getAuthenticationFromExpiredAccessToken(accessToken);
+                SecurityContextHolder.getContext().setAuthentication(authentication);
+
+                jwtUtil.validateRefreshToken(refreshToken); // RT는 만료 허용 X
+                chain.doFilter(request, response);
+                return;
+            } catch (Exception e) {
+                // EntryPoint로 넘겨 통일 처리
+                if (e instanceof CustomAuthException ce) {
+                    request.setAttribute("exceptionCode", ce.getCode());
+                    request.setAttribute("exceptionMessage", ce.getMessage());
+                    request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
+                }
+                throw new InsufficientAuthenticationException(e.getMessage(), e);
+            }
+        }
+
+        // 그 외(보호 자원): Authorization 헤더가 없으면 그냥 통과
+        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            String accessToken = JwtUtil.getTokenFromHeader(authHeader);
+            jwtUtil.validateToken(accessToken);
+            jwtUtil.isTokenBlacklisted(accessToken); // accessToken 전달
+
+            Authentication authentication = jwtUtil.getAuthentication(accessToken);
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+
+            chain.doFilter(request, response);
+        } catch (Exception e) {
+            if (e instanceof CustomAuthException ce) {
+                request.setAttribute("exceptionCode", ce.getCode());
+                request.setAttribute("exceptionMessage", ce.getMessage());
+                request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
+            }
+            throw new InsufficientAuthenticationException(e.getMessage(), e);
+        }
+    }
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
new file mode 100644
index 0000000..5837aae
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
@@ -0,0 +1,183 @@
+package com.assu.server.domain.auth.security;
+
+import com.assu.server.domain.auth.dto.signup.Tokens;
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+@Component
+@RequiredArgsConstructor
+public class JwtUtil {
+
+    @Value("${jwt.secret}")
+    public String secretKey;
+
+    @Value("${jwt.access-valid-seconds:3600}")      // 1시간 기본
+    private int accessValidSeconds;
+
+    @Value("${jwt.refresh-valid-seconds:1209600}")  // 14일 기본
+    private int refreshValidSeconds;
+
+    private final RedisTemplate redisTemplate;
+    private final MemberRepository memberRepository;
+
+    // 헤더에 "Bearer XXX" 형식으로 담겨온 토큰을 추출한다
+    public static String getTokenFromHeader(String header) {
+        return header.split(" ")[1];
+    }
+
+    public String generateToken(Map valueMap, int validTime) { // static 제거
+        SecretKey key = null;
+        try {
+            key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
+        } catch(Exception e){
+            throw new RuntimeException(e.getMessage());
+        }
+        return Jwts.builder()
+                .setHeader(Map.of("typ","JWT"))
+                .setClaims(valueMap)
+                .setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
+                .setExpiration(Date.from(ZonedDateTime.now().plusSeconds(validTime).toInstant()))
+                .signWith(key)
+                .compact();
+    }
+
+    public Tokens issueAndPersistTokens(Member member, UserRole role) {
+        // 공통 Claims
+        Map claims = new HashMap<>();
+        claims.put("userId", member.getId());
+        claims.put("role", role.name());
+
+        // access / refresh 발급
+        String access = generateToken(claims, accessValidSeconds);
+        String refresh = generateToken(claims, refreshValidSeconds);
+
+        // 멤버 엔티티에 저장 (unique=true 컬럼)
+        member.setAccessToken(access);
+        member.setRefreshToken(refresh);
+        memberRepository.save(member);
+
+        return Tokens.builder()
+                .accessToken(access)
+                .refreshToken(refresh)
+                .build();
+    }
+
+    public Authentication getAuthentication(String token) { // context에 넣을 Authentication를 jwt의 userId를 넣어 생성 // static 제거
+        Map claims = validateToken(token);
+        System.out.println("userId type: " + (claims.get("userId") != null ? claims.get("userId").getClass().getName() : "null"));
+
+        Number uid = (Number) claims.get("userId");
+        Long userId = uid.longValue();
+
+        return new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
+    }
+
+    public Map validateToken(String token) { // static 제거
+        Map claim = null;
+        try {
+            SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
+            claim = Jwts.parserBuilder()
+                    .setSigningKey(key)
+                    .build()
+                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
+                    .getBody();
+        } catch(ExpiredJwtException expiredJwtException){
+            throw new CustomAuthException(ErrorStatus.JWT_ACCESS_TOKEN_EXPIRED);
+        } catch(Exception e){
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
+        }
+        return claim;
+    }
+
+    public Authentication getAuthenticationFromExpiredAccessToken(String token) { // context에 넣을 Authentication를 jwt의 userId를 넣어 생성 // static 제거
+        Map claims = validateTokenOnlySignature(token);
+        System.out.println("userId type: " + (claims.get("userId") != null ? claims.get("userId").getClass().getName() : "null"));
+
+        Number uid = (Number) claims.get("userId");
+        Long userId = uid.longValue();
+
+        return new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
+    }
+
+    public Claims validateTokenOnlySignature(String token) { // static 제거
+        Claims claims = null;
+        try {
+            SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
+            claims = Jwts.parserBuilder()
+                    .setSigningKey(key)
+                    .build()
+                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
+                    .getBody();
+        } catch(ExpiredJwtException expiredJwtException){
+            return expiredJwtException.getClaims(); // ✅ 만료된 토큰에서도 Claims 추출
+        } catch(Exception e){
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
+        }
+        return claims;
+    }
+
+    public void validateRefreshToken(String token) { // static 제거
+        Map claim = null;
+        try {
+            SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
+            claim = Jwts.parserBuilder()
+                    .setSigningKey(key)
+                    .build()
+                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
+                    .getBody();
+        } catch(ExpiredJwtException expiredJwtException){
+            throw new CustomAuthException(ErrorStatus.JWT_REFRESH_TOKEN_EXPIRED);
+        } catch(Exception e){
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
+        }
+    }
+
+    // 토큰의 남은 만료시간 계산
+    public long tokenRemainTimeSecond(String header) { // static 제거
+        String accessToken = getTokenFromHeader(header);
+        SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
+        Claims claims = Jwts.parserBuilder()
+                .setSigningKey(key)
+                .build()
+                .parseClaimsJws(accessToken)
+                .getBody();
+
+        Date expDate = claims.getExpiration(); // 만료 시간 반환 (Date 타입)
+        long remainMs = expDate.getTime() - System.currentTimeMillis();
+        return remainMs / 1000;
+    }
+
+    // access token redis의 블랙리스트에서 확인
+    public void isTokenBlacklisted(String accessToken) {
+        Set keys = redisTemplate.keys("blackList:*"); // "blackList:*" 패턴의 모든 Key 검색
+        if (keys == null || keys.isEmpty()) {
+            return; // 블랙리스트가 비어있다면 return
+        }
+
+        // 모든 Key에 대해 해당 Token이 Value로 존재하는지 확인
+        for (String key : keys) {
+            String value = redisTemplate.opsForValue().get(key);
+            if (accessToken.equals(value)) {
+                throw new CustomAuthException(ErrorStatus.LOGOUT_USER);
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java b/src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java
new file mode 100644
index 0000000..7dc7d06
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java
@@ -0,0 +1,23 @@
+package com.assu.server.domain.auth.security;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+@Slf4j
+public class SecurityUtil {
+
+    private SecurityUtil() { }
+
+    // SecurityContext 에 유저 정보가 저장되는 시점
+    // Request 가 들어올 때 JwtAuthenticationFilter 의 doFilterInternal 에서 저장
+    public static Long getCurrentUserId() {
+        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        if (authentication == null || authentication.getName() == null) {
+            throw  new RuntimeException("Security Context 에 인증 정보가 없습니다.");
+        }
+
+        return Long.parseLong(authentication.getName());
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java b/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
new file mode 100644
index 0000000..8d50b19
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
@@ -0,0 +1,48 @@
+package com.assu.server.domain.auth.security.common;
+
+import com.assu.server.domain.auth.entity.CommonAuth;
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.auth.repository.CommonAuthRepository;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CommonUserDetailsService implements UserDetailsService {
+
+    private final CommonAuthRepository commonAuthRepository;
+
+    @Override
+    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
+        // CommonAuth: email/password 해시 저장 테이블
+        CommonAuth commonAuth = commonAuthRepository.findByEmail(email)
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+
+        // 연관된 Member에서 역할/상태를 가져옴
+        Member member = commonAuth.getMember();
+        UserRole role = member.getRole();
+        boolean enabled = member.getIsActivated().equals(ActivationStatus.ACTIVE); // ACTIVE면 true
+
+        // 권한명은 스프링 시큐리티 규약에 따라 ROLE_ 접두를 붙임
+        String authority = "ROLE_" + role.name();
+
+        return org.springframework.security.core.userdetails.User
+                .withUsername(commonAuth.getEmail())
+                .password(commonAuth.getPassword()) // 반드시 BCrypt 등 해시
+                .authorities(authority)
+                .accountExpired(false)
+                .accountLocked(false)
+                .credentialsExpired(false)
+                .disabled(!enabled)
+                .build();
+    }
+
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java b/src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java
new file mode 100644
index 0000000..296429b
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java
@@ -0,0 +1,9 @@
+package com.assu.server.domain.auth.security.common;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+
+public class CommonUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
+    public CommonUsernamePasswordAuthenticationToken(String email, String password) {
+        super(email, password);
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java b/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
new file mode 100644
index 0000000..7b999d8
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
@@ -0,0 +1,38 @@
+package com.assu.server.domain.auth.security.student;
+
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.auth.entity.SSUAuth;
+import com.assu.server.domain.auth.repository.SSUAuthRepository;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class StudentUserDetailsService implements UserDetailsService {
+    private final SSUAuthRepository ssuAuthRepository;
+
+    @Override
+    public UserDetails loadUserByUsername(String studentNumber) throws UsernameNotFoundException {
+        SSUAuth ssuAuth = ssuAuthRepository.findByStudentNumber(studentNumber)
+                .orElseThrow(() -> new UsernameNotFoundException(studentNumber));
+
+        Member member = ssuAuth.getMember();
+
+        String authority = "ROLE_" + member.getRole().name();
+        boolean enabled = member.getIsActivated().equals(ActivationStatus.ACTIVE);
+
+        // username = 학번, password = "암호문" (Decoder가 복호화/비교)
+        return org.springframework.security.core.userdetails.User
+                .withUsername(ssuAuth.getStudentNumber())
+                .password(ssuAuth.getPasswordCipher()) // 평문 아님! cipher 그대로
+                .authorities(authority)
+                .disabled(!enabled)
+                .accountLocked(false).accountExpired(false).credentialsExpired(false)
+                .build();
+    }
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java b/src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java
new file mode 100644
index 0000000..698465b
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java
@@ -0,0 +1,10 @@
+package com.assu.server.domain.auth.security.student;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+
+public class StudentUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
+    public StudentUsernamePasswordAuthenticationToken(String studentNumber, String studentPassword) {
+        super(studentNumber, studentPassword);
+    }
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginService.java b/src/main/java/com/assu/server/domain/auth/service/LoginService.java
new file mode 100644
index 0000000..b61edaf
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginService.java
@@ -0,0 +1,12 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.auth.dto.login.LoginRequest;
+import com.assu.server.domain.auth.dto.login.LoginResponse;
+import com.assu.server.domain.auth.dto.login.RefreshResponse;
+import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
+
+public interface LoginService {
+    LoginResponse login(LoginRequest request);
+    LoginResponse loginStudent(StudentLoginRequest request);
+    RefreshResponse refresh(String refreshToken);
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
new file mode 100644
index 0000000..fa2df45
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -0,0 +1,103 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.auth.dto.login.LoginRequest;
+import com.assu.server.domain.auth.dto.login.LoginResponse;
+import com.assu.server.domain.auth.dto.login.RefreshResponse;
+import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
+import com.assu.server.domain.auth.dto.signup.Tokens;
+import com.assu.server.domain.auth.entity.CommonAuth;
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.auth.entity.SSUAuth;
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.auth.repository.CommonAuthRepository;
+import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.auth.repository.SSUAuthRepository;
+import com.assu.server.domain.auth.security.JwtUtil;
+import com.assu.server.domain.auth.security.SecurityUtil;
+import com.assu.server.domain.auth.security.common.CommonUsernamePasswordAuthenticationToken;
+import com.assu.server.domain.auth.security.student.StudentUsernamePasswordAuthenticationToken;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Service
+@RequiredArgsConstructor
+public class LoginServiceImpl implements LoginService {
+
+    private final CommonAuthRepository commonAuthRepository;
+    private final SSUAuthRepository ssuAuthRepository;
+    private final MemberRepository memberRepository;
+
+    private final AuthenticationManager authenticationManager;
+    private final JwtUtil jwtUtil;
+
+    @Override
+    public LoginResponse login(LoginRequest request) {
+        // 공통(파트너/관리자) 로그인: 이메일/비번
+        Authentication auth = authenticationManager.authenticate(
+                new CommonUsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
+        );
+
+        CommonAuth commonAuth = commonAuthRepository.findByEmail(auth.getName())
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+
+        Member member = commonAuth.getMember();
+        Tokens tokens = jwtUtil.issueAndPersistTokens(member, member.getRole());
+
+        return LoginResponse.builder()
+                .memberId(member.getId())
+                .role(member.getRole())
+                .status(member.getIsActivated())
+                .tokens(tokens)
+                .build();
+    }
+
+    @Override
+    public LoginResponse loginStudent(StudentLoginRequest request) {
+        // 학생 로그인: 학번/학교 비번
+        Authentication auth = authenticationManager.authenticate(
+                new StudentUsernamePasswordAuthenticationToken(request.getStudentNumber(), request.getStudentPassword())
+        );
+
+        SSUAuth ssuAuth = ssuAuthRepository.findByStudentNumber(auth.getName())
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+
+        Member member = ssuAuth.getMember();
+        Tokens tokens = jwtUtil.issueAndPersistTokens(member, member.getRole());
+
+        return LoginResponse.builder()
+                .memberId(member.getId())
+                .role(member.getRole())
+                .status(member.getIsActivated())
+                .tokens(tokens)
+                .build();
+    }
+
+    @Override
+    public RefreshResponse refresh(String refreshToken) {
+        Long userId = SecurityUtil.getCurrentUserId();
+        Member member = memberRepository.findById(userId)
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+
+        jwtUtil.validateRefreshToken(refreshToken);
+        String savedRt = member.getRefreshToken();
+        if (savedRt == null || !savedRt.equals(refreshToken)) {
+            throw new CustomAuthException(ErrorStatus.REFRESH_TOKEN_NOT_EQUAL);
+        }
+
+        // 회전: JwtUtil에 위임(내부의 valid-seconds 사용)
+        Tokens rotated = jwtUtil.issueAndPersistTokens(member, member.getRole());
+
+        return new RefreshResponse(
+                userId,
+                rotated.getAccessToken(),
+                rotated.getRefreshToken()
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/service/LogoutService.java b/src/main/java/com/assu/server/domain/auth/service/LogoutService.java
new file mode 100644
index 0000000..ff6019d
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/LogoutService.java
@@ -0,0 +1,5 @@
+package com.assu.server.domain.auth.service;
+
+public interface LogoutService {
+    void logout(String rawAccessToken);
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
new file mode 100644
index 0000000..533c817
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
@@ -0,0 +1,45 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.auth.security.JwtUtil;
+import com.assu.server.domain.auth.security.SecurityUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
+@Service
+@RequiredArgsConstructor
+public class LogoutServiceImpl implements LogoutService {
+
+    private MemberRepository memberRepository;
+
+    private final JwtUtil jwtUtil;
+    private final RedisTemplate redisTemplate;
+
+    @Override
+    public void logout(String rawAccessToken) {
+        Long userId = SecurityUtil.getCurrentUserId();
+
+        // RT 무효화: DB에서 제거
+        memberRepository.findById(userId).ifPresent(m -> {
+            m.setRefreshToken(null);
+            m.setAccessToken(null);
+            memberRepository.save(m);
+        });
+
+        // Access 토큰 블랙리스트 등록
+        long remainSec;
+        try {
+            // JwtUtil에 같은 로직의 오버로드를 추가해도 됨
+            remainSec = jwtUtil.tokenRemainTimeSecond("Bearer " + rawAccessToken);
+        } catch (Exception e) {
+            remainSec = 0L;
+        }
+        if (remainSec > 0) {
+            String key = "blackList:" + userId;
+            redisTemplate.opsForValue().set(key, rawAccessToken, remainSec, TimeUnit.SECONDS);
+        }
+    }
+}
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 6c97198..bd73185 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
@@ -2,7 +2,7 @@
 
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.util.RandomNumberUtil;
-import com.assu.server.domain.auth.exception.AuthException;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.core.ValueOperations;
@@ -38,7 +38,7 @@ public void verifyAuthNumber(String phoneNumber, String authNumber) {
         String stored = valueOps.get(phoneNumber);
 
         if (stored == null || !stored.equals(authNumber)) {
-            throw new AuthException(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
+            throw new CustomAuthException(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
         }
 
         // 인증 성공 시 Redis에서 삭제(Optional)
diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java
new file mode 100644
index 0000000..d5278b5
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java
@@ -0,0 +1,6 @@
+package com.assu.server.domain.auth.service;
+
+
+public interface SSUAuthService {
+    // UsaintAuthReturnDto uSaintAuth(UsaintAuthParamDto usaintAuthParamDto) throws APIRequestFailedException, AuthFailedException, HTMLParseFailedException, UnsupportedMajorException;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
new file mode 100644
index 0000000..2cbb056
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
@@ -0,0 +1,171 @@
+package com.assu.server.domain.auth.service;
+
+
+import com.assu.server.domain.user.entity.enums.Major;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class SSUAuthServiceImpl implements SSUAuthService {
+    /*
+    @NotNull
+    @Override
+    public UsaintAuthReturnDto uSaintAuth(@NotNull UsaintAuthParamDto usaintAuthParamDto) throws APIRequestFailedException, AuthFailedException, HTMLParseFailedException, UnsupportedMajorException {
+        String sToken = usaintAuthParamDto.getSToken();
+        Integer sIdno = usaintAuthParamDto.getSIdno();
+
+        // Phase 1 : uSaint SSO
+
+        String uSaintSSORequestUrl = globalVariable.uSaintSSOUrl + "?sToken=" + sToken + "&sIdno=" + sIdno;
+
+        HashMap uSaintSSORequestHeaders = new HashMap<>();
+        uSaintSSORequestHeaders.put("Cookie", "sToken=" + sToken + "; sIdno=" + sIdno);
+
+        APIRequestDto uSaintSSORequestDto = APIRequestDto.builder()
+                .headers(uSaintSSORequestHeaders)
+                .url(uSaintSSORequestUrl)
+                .build();
+
+        APIResponseDto uSaintSSOResponseDto;
+        try {
+            uSaintSSOResponseDto = apiProvider.get(uSaintSSORequestDto);
+        }
+        catch (Exception e){
+            log.error("API request to uSaint SSO failed.", e);
+            throw new APIRequestFailedException();
+        }
+
+        if(!uSaintSSOResponseDto.getBody().contains("location.href = \"/irj/portal\";")){
+            log.error("Student authentication with sToken {} and sIdno {} failed.", sToken, sIdno);
+            throw new AuthFailedException();
+        }
+
+        Map> uSaintSSOResponseHeaders = uSaintSSOResponseDto.getHeaders();
+        List setCookieList = uSaintSSOResponseHeaders.get("set-cookie");
+        StringBuilder uSaintPortalCookie = new StringBuilder();
+
+        for(String setCookie : setCookieList){
+            setCookie = setCookie.split(";")[0];
+            uSaintPortalCookie.append(setCookie).append("; ");
+        }
+
+        // Phase 2 : uSaint Portal
+
+        String uSaintPortalRequestUrl = globalVariable.uSaintPortalUrl;
+
+        HashMap uSaintPortalRequestHeaders = new HashMap<>();
+        uSaintPortalRequestHeaders.put("Cookie", uSaintPortalCookie.toString());
+
+        APIRequestDto uSaintPortalRequestDto = APIRequestDto.builder()
+                .headers(uSaintPortalRequestHeaders)
+                .url(uSaintPortalRequestUrl)
+                .build();
+
+        APIResponseDto uSaintPortalResponseDto;
+        try {
+            uSaintPortalResponseDto = apiProvider.get(uSaintPortalRequestDto);
+        }
+        catch (Exception e){
+            log.error("API request to uSaint Portal failed.", e);
+            throw new APIRequestFailedException();
+        }
+
+        String uSaintPortalResponseBody = uSaintPortalResponseDto.getBody();
+        UsaintAuthReturnDto usaintAuthReturnDto = UsaintAuthReturnDto.builder().build();
+
+        Document uSaintPortalDocument = Jsoup.parse(uSaintPortalResponseBody);
+        Element uSaintPortalNameBox = uSaintPortalDocument.getElementsByClass("main_box09").first();
+        Element uSaintPortalInfoBox = uSaintPortalDocument.getElementsByClass("main_box09_con").first();
+        if(uSaintPortalNameBox == null){
+            log.error("uSaintPortalNameBox is null.");
+            log.debug(uSaintPortalResponseBody);
+            throw new HTMLParseFailedException();
+        }
+        if(uSaintPortalInfoBox == null){
+            log.error("uSaintPortalInfoBox is null.");
+            log.debug(uSaintPortalResponseBody);
+            throw new HTMLParseFailedException();
+        }
+
+        Element uSaintPortalNameBoxSpan = uSaintPortalNameBox.getElementsByTag("span").first();
+        if(uSaintPortalNameBoxSpan == null || uSaintPortalNameBoxSpan.text().equals("")){
+            log.error("uSaintPortalNameBoxSpan is null or empty.");
+            log.debug(uSaintPortalResponseBody);
+            throw new HTMLParseFailedException();
+        }
+        String studentName = uSaintPortalNameBoxSpan.text();
+        studentName = studentName.split("님")[0];
+        usaintAuthReturnDto.setName(studentName);
+
+        Elements uSaintPortalInfoBoxLis = uSaintPortalInfoBox.getElementsByTag("li");
+
+        for(Element uSaintPortalInfoBoxLi : uSaintPortalInfoBoxLis){
+            Element dt = uSaintPortalInfoBoxLi.getElementsByTag("dt").first();
+            if(dt == null){
+                log.error("dt in uSaintPortalInfoBoxLi is null.");
+                log.debug(uSaintPortalResponseBody);
+                throw new HTMLParseFailedException();
+            }
+
+            Element strong = uSaintPortalInfoBoxLi.getElementsByTag("strong").first();
+            if(strong == null || strong.text().equals("")){
+                log.error("strong in uSaintPortalInfoBoxLi is null or empty.");
+                log.debug(uSaintPortalResponseBody);
+                throw new HTMLParseFailedException();
+            }
+
+            if(dt.text().equals("학번")){
+                try{
+                    usaintAuthReturnDto.setId(Integer.valueOf(strong.text()));
+                }
+                catch(NumberFormatException e){
+                    log.error("studentId in strong is not an integer.");
+                    log.debug(uSaintPortalResponseBody);
+                    throw new HTMLParseFailedException();
+                }
+            }
+            else if(dt.text().equals("소속")){
+                usaintAuthReturnDto.setMajor(strong.text());
+            }
+            else if(dt.text().equals("과정/학적")){
+                usaintAuthReturnDto.setStatus(strong.text());
+            }
+
+        }
+
+        if(usaintAuthReturnDto.getMajor().contains("전자정보공학부")){
+            usaintAuthReturnDto.setMajor("infocom");
+            return usaintAuthReturnDto;
+        }
+
+        switch (usaintAuthReturnDto.getMajor()) {
+            case "컴퓨터학부" -> usaintAuthReturnDto.setMajor(Major.COM);
+            case "소프트웨어학부" -> usaintAuthReturnDto.setMajor(Major.SW);
+            case "글로벌미디어학부" -> usaintAuthReturnDto.setMajor(Major.GM);
+            case "미디어경영학과" -> usaintAuthReturnDto.setMajor(Major.MB);
+            case "AI융합학부" -> usaintAuthReturnDto.setMajor(Major.AI);
+            case "전자정보공학부" -> usaintAuthReturnDto.setMajor(Major.EE);
+            case "정보보호학과" -> usaintAuthReturnDto.setMajor(Major.IP);
+            default -> {
+                log.debug("{} is not a supported major.", usaintAuthReturnDto.getMajor());
+                throw new UnsupportedMajorException();
+            }
+        }
+
+        return usaintAuthReturnDto;
+    }
+    */
+
+}
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
new file mode 100644
index 0000000..7ae7e70
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/SignUpService.java
@@ -0,0 +1,13 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.auth.dto.signup.AdminSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.SignUpResponse;
+import com.assu.server.domain.auth.dto.signup.StudentSignUpRequest;
+import org.springframework.web.multipart.MultipartFile;
+
+public interface SignUpService {
+    SignUpResponse signupStudent(StudentSignUpRequest req);
+    SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage);
+    SignUpResponse signupAdmin(AdminSignUpRequest 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
new file mode 100644
index 0000000..bf829b7
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java
@@ -0,0 +1,230 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.admin.repository.AdminRepository;
+import com.assu.server.domain.auth.crypto.SchoolCredentialEncryptor;
+import com.assu.server.domain.auth.dto.signup.*;
+import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
+import com.assu.server.domain.auth.dto.signup.student.StudentInfoPayload;
+import com.assu.server.domain.auth.entity.CommonAuth;
+import com.assu.server.domain.auth.entity.SSUAuth;
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.auth.repository.CommonAuthRepository;
+import com.assu.server.domain.auth.repository.SSUAuthRepository;
+import com.assu.server.domain.auth.security.JwtUtil;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.partner.entity.Partner;
+import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.user.entity.Student;
+import com.assu.server.domain.user.entity.enums.Major;
+import com.assu.server.domain.user.repository.StudentRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.infra.s3.AmazonS3Manager;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+
+@Service
+@RequiredArgsConstructor
+public class SignUpServiceImpl implements SignUpService {
+
+    private final MemberRepository memberRepository;
+    private final SSUAuthRepository ssuAuthRepository;
+    private final CommonAuthRepository commonAuthRepository;
+    private final StudentRepository studentRepository;
+    private final PartnerRepository partnerRepository;
+    private final AdminRepository adminRepository;
+
+    private final PasswordEncoder passwordEncoder;           // 공통(파트너/관리자)용 BCrypt
+    private final SchoolCredentialEncryptor schoolEncryptor; // 학생용 AES-GCM
+    private final AmazonS3Manager amazonS3Manager;
+    private final JwtUtil jwtUtil;
+
+    /* 학생: JSON */
+    @Override
+    @Transactional
+    public SignUpResponse signupStudent(StudentSignUpRequest req) {
+        // 중복 체크
+        if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
+        }
+        if (ssuAuthRepository.existsByStudentNumber(req.getStudentAuth().getStudentNumber())) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
+        }
+
+        // member 생성
+        Member member = memberRepository.save(
+                Member.builder()
+                        .phoneNum(req.getPhoneNumber())
+                        .isPhoneVerified(true)
+                        .role(UserRole.STUDENT)
+                        .isActivated(ActivationStatus.ACTIVE)
+                        .build()
+        );
+
+        // ssu_auth 생성(학생번호/암호화 PW(AES-GCM))
+        String cipher = schoolEncryptor.encrypt(req.getStudentAuth().getStudentPassword());
+        ssuAuthRepository.save(
+                SSUAuth.builder()
+                        .member(member)
+                        .studentNumber(req.getStudentAuth().getStudentNumber())
+                        .passwordCipher(cipher)
+                        .isAuthenticated(true) // 초기값(유세인트 검증 완료 여부에 맞게 조정)
+                        .build()
+        );
+
+        // student 프로필 생성
+        StudentInfoPayload info = req.getStudentInfo();
+        Major major;
+        switch (info.getMajor()) {
+            case "컴퓨터학부" -> major = Major.COM;
+            case "소프트웨어학부" -> major = Major.SW;
+            case "글로벌미디어학부" -> major = Major.GM;
+            case "미디어경영학과" -> major = Major.MB;
+            case "AI융합학부" -> major = Major.AI;
+            case "전자정보공학부" -> major = Major.EE;
+            case "정보보호학과" -> major = Major.IP;
+            default -> major = null;
+        }
+
+        studentRepository.save(
+                Student.builder()
+                        .member(member)
+                        .department(info.getDepartment())
+                        .enrollmentStatus(info.getEnrollmentStatus())
+                        .yearSemester(info.getYearSemester())
+                        .university(info.getUniversity())
+                        .stamp(0)
+                        .major(major)
+                        .build()
+        );
+
+        // JWT 발급 및 저장
+        Tokens tokens = jwtUtil.issueAndPersistTokens(member, UserRole.STUDENT);
+
+        return SignUpResponse.builder()
+                .memberId(member.getId())
+                .role(UserRole.STUDENT)
+                .status(member.getIsActivated())
+                .tokens(tokens)
+                .build();
+    }
+
+    /* 제휴업체: MULTIPART(payload JSON + licenseImage) */
+    @Override
+    @Transactional
+    public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage) {
+        if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
+        }
+        if (commonAuthRepository.existsByEmail(req.getCommonAuth().getEmail())) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL);
+        }
+
+        Member member = memberRepository.save(
+                Member.builder()
+                        .phoneNum(req.getPhoneNumber())
+                        .isPhoneVerified(true)
+                        .role(UserRole.PARTNER)
+                        .isActivated(ActivationStatus.SUSPEND) // 사업자 등록증 확인 후 활성화
+                        .build()
+        );
+
+        // CommonAuth: BCrypt 해시 저장
+        String pwHash = passwordEncoder.encode(req.getCommonAuth().getPassword());
+        commonAuthRepository.save(
+                CommonAuth.builder()
+                        .member(member)
+                        .email(req.getCommonAuth().getEmail())
+                        .password(pwHash)
+                        .isEmailVerified(false)
+                        .build()
+        );
+
+        // 파일 업로드 + 파트너 정보
+        String keyPath = "partners/" + member.getId() + "/" + licenseImage.getOriginalFilename();
+        String keyName = amazonS3Manager.generateKeyName(keyPath);
+        String licenseUrl = amazonS3Manager.uploadFile(keyName, licenseImage);
+
+        CommonInfoPayload info = req.getCommonInfo();
+        partnerRepository.save(
+                Partner.builder()
+                        .member(member)
+                        .name(info.getName())
+                        .address(info.getAddress())
+                        .detailAddress(info.getDetailAddress())
+                        .licenseUrl(licenseUrl)
+                        .build()
+        );
+
+        Tokens tokens = jwtUtil.issueAndPersistTokens(member, UserRole.PARTNER);
+
+        return SignUpResponse.builder()
+                .memberId(member.getId())
+                .role(UserRole.PARTNER)
+                .status(member.getIsActivated())
+                .tokens(tokens)
+                .build();
+    }
+
+    /* 관리자: MULTIPART(payload JSON + signImage) */
+    @Override
+    @Transactional
+    public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImage) {
+        if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
+        }
+        if (commonAuthRepository.existsByEmail(req.getCommonAuth().getEmail())) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL);
+        }
+
+        Member member = memberRepository.save(
+                Member.builder()
+                        .phoneNum(req.getPhoneNumber())
+                        .isPhoneVerified(true)
+                        .role(UserRole.ADMIN)
+                        .isActivated(ActivationStatus.SUSPEND) // 인감 확인 후 활성화
+                        .build()
+        );
+
+        String pwHash = passwordEncoder.encode(req.getCommonAuth().getPassword());
+        commonAuthRepository.save(
+                CommonAuth.builder()
+                        .member(member)
+                        .email(req.getCommonAuth().getEmail())
+                        .password(pwHash)
+                        .isEmailVerified(false)
+                        .build()
+        );
+
+        String keyPath = "admins/" + member.getId() + "/" + signImage.getOriginalFilename();
+        String keyName = amazonS3Manager.generateKeyName(keyPath);
+        String signUrl = amazonS3Manager.uploadFile(keyName, signImage);
+
+        CommonInfoPayload info = req.getCommonInfo();
+        adminRepository.save(
+                Admin.builder()
+                        .member(member)
+                        .name(info.getName())
+                        .officeAddress(info.getAddress())
+                        .detailAddress(info.getDetailAddress())
+                        .signUrl(signUrl)
+                        .build()
+        );
+
+        Tokens tokens = jwtUtil.issueAndPersistTokens(member, UserRole.ADMIN);
+
+        return SignUpResponse.builder()
+                .memberId(member.getId())
+                .role(UserRole.ADMIN)
+                .status(member.getIsActivated())
+                .tokens(tokens)
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
index 0e89da0..803a077 100644
--- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
+++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
@@ -1,13 +1,14 @@
 package com.assu.server.domain.chat.converter;
 
 import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.chat.dto.ChatMessageDTO;
 import com.assu.server.domain.chat.dto.ChatRequestDTO;
 import com.assu.server.domain.chat.dto.ChatResponseDTO;
 import com.assu.server.domain.chat.dto.ChatRoomListResultDTO;
 import com.assu.server.domain.chat.entity.ChattingRoom;
 import com.assu.server.domain.chat.entity.Message;
-import com.assu.server.domain.common.entity.Member;
+
 import com.assu.server.domain.partner.entity.Partner;
 
 import java.util.List;
diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java
index f45fb77..6998121 100644
--- a/src/main/java/com/assu/server/domain/chat/entity/Message.java
+++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java
@@ -1,10 +1,10 @@
 package com.assu.server.domain.chat.entity;
-import java.time.LocalDateTime;
 
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.chat.entity.enums.MessageType;
 
 import com.assu.server.domain.common.entity.BaseEntity;
-import com.assu.server.domain.common.entity.Member;
+
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index c662852..df68e6b 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -11,13 +11,13 @@
 import com.assu.server.domain.chat.entity.Message;
 import com.assu.server.domain.chat.repository.ChatRepository;
 import com.assu.server.domain.chat.repository.MessageRepository;
-import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.common.enums.ActivationStatus;
-import com.assu.server.domain.common.repository.MemberRepository;
+import com.assu.server.domain.auth.repository.MemberRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/assu/server/domain/common/entity/CommonAuth.java b/src/main/java/com/assu/server/domain/common/entity/CommonAuth.java
deleted file mode 100644
index f12bd70..0000000
--- a/src/main/java/com/assu/server/domain/common/entity/CommonAuth.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.assu.server.domain.common.entity;
-import java.time.LocalDateTime;
-
-import jakarta.persistence.Entity;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.OneToOne;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-
-@Entity
-@Getter
-@NoArgsConstructor
-@Builder
-@AllArgsConstructor
-public class CommonAuth extends BaseEntity {
-	@Id
-	@GeneratedValue(strategy = GenerationType.IDENTITY)
-	private Long id;
-
-	@OneToOne(fetch = FetchType.LAZY)
-	@JoinColumn(name = "member_id")
-	private Member member;
-
-	private String email;
-	private String password;
-	private Boolean isEmailVerified;
-	private LocalDateTime lastLoginAt;
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java
deleted file mode 100644
index 161e5cc..0000000
--- a/src/main/java/com/assu/server/domain/common/entity/Member.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.assu.server.domain.common.entity;
-
-import com.assu.server.domain.common.enums.ActivationStatus;
-import com.assu.server.domain.common.enums.UserRole;
-import com.assu.server.domain.admin.entity.Admin;
-import com.assu.server.domain.partner.entity.Partner;
-import com.assu.server.domain.user.entity.Student;
-import jakarta.persistence.*;
-import lombok.Getter;
-
-import java.time.LocalDateTime;
-
-@Getter
-@Entity
-public class Member extends BaseEntity {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private Long id;
-
-    private String phoneNum;
-
-    private Boolean isPhoneVerified;
-
-    private LocalDateTime phoneVerifiedAt;
-
-    private String profileUrl;
-
-    @Enumerated(EnumType.STRING)
-    private UserRole role;  // User, ADMIN, PARTNER
-
-    @Enumerated(EnumType.STRING)
-    private ActivationStatus isActivated;  // ACTIVE, INACTIVE, SUSPEND
-
-    // 역할별 프로필 - 선택적으로 연관
-    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
-    private Student studentProfile;
-
-    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
-    private Admin adminProfile;
-
-    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
-    private Partner partnerProfile;
-
-    // 편의 메서드 및 Builder 등 생략
-}
-
diff --git a/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java
deleted file mode 100644
index 20b3461..0000000
--- a/src/main/java/com/assu/server/domain/common/entity/SSUAuth.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.assu.server.domain.common.entity;
-
-import java.time.LocalDateTime;
-
-import jakarta.persistence.Entity;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.OneToOne;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Entity
-@Getter
-@NoArgsConstructor
-@Builder
-@AllArgsConstructor
-public class SSUAuth extends BaseEntity {
-	@Id
-	@GeneratedValue(strategy = GenerationType.IDENTITY)
-	private Long id;
-
-	@OneToOne(fetch = FetchType.LAZY)
-	@JoinColumn(name="member_id")
-	private Member member;
-
-	private String passwordCipher;
-	private Boolean isAuthenticated;
-	private LocalDateTime authenticated_at;
-}
diff --git a/src/main/java/com/assu/server/domain/common/enums/UserRole.java b/src/main/java/com/assu/server/domain/common/enums/UserRole.java
index 7b18c01..c505d56 100644
--- a/src/main/java/com/assu/server/domain/common/enums/UserRole.java
+++ b/src/main/java/com/assu/server/domain/common/enums/UserRole.java
@@ -1,5 +1,5 @@
 package com.assu.server.domain.common.enums;
 
 public enum UserRole {
-    USER, ADMIN, PARTNER
+    STUDENT, ADMIN, PARTNER
 }
diff --git a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
deleted file mode 100644
index 207a939..0000000
--- a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.assu.server.domain.common.repository;
-
-import com.assu.server.domain.common.entity.Member;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-
-
-public interface MemberRepository extends JpaRepository {
-}
diff --git a/src/main/java/com/assu/server/domain/partner/entity/Partner.java b/src/main/java/com/assu/server/domain/partner/entity/Partner.java
index efc423c..0f5994e 100644
--- a/src/main/java/com/assu/server/domain/partner/entity/Partner.java
+++ b/src/main/java/com/assu/server/domain/partner/entity/Partner.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.partner.entity;
 
-import com.assu.server.domain.common.entity.Member;
+
+import com.assu.server.domain.auth.entity.Member;
 import jakarta.persistence.Entity;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.MapsId;
diff --git a/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java b/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java
index 4bd281c..32b9e05 100644
--- a/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java
+++ b/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.term.entity.mapping;
 import com.assu.server.domain.common.entity.BaseEntity;
-import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.term.entity.Term;
 
 import jakarta.persistence.Entity;
diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java
index 89544b0..0da6b4b 100644
--- a/src/main/java/com/assu/server/domain/user/entity/Student.java
+++ b/src/main/java/com/assu/server/domain/user/entity/Student.java
@@ -1,9 +1,13 @@
 package com.assu.server.domain.user.entity;
 
-import com.assu.server.domain.common.entity.Member;
+
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.user.entity.enums.Department;
 import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
 import com.assu.server.domain.user.entity.enums.Major;
+import com.assu.server.domain.user.entity.enums.University;
 import jakarta.persistence.*;
+import jakarta.validation.constraints.Pattern;
 import lombok.*;
 
 @Entity
@@ -20,14 +24,15 @@ public class Student {
     @MapsId
     private Member member;
 
-    private String department;
+    private Department department;
 
     @Enumerated(EnumType.STRING)
     private EnrollmentStatus enrollmentStatus;
 
+    @Pattern(regexp = "^[0-9]{1}-[1-2]$", message = "yearSemester는 Y-N 형식이어야 합니다. 예: 3-1")
     private String yearSemester;
 
-    private String university;
+    private University university;
 
     private int stamp;
 
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Department.java b/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
new file mode 100644
index 0000000..66ce387
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
@@ -0,0 +1,5 @@
+package com.assu.server.domain.user.entity.enums;
+
+public enum Department {
+    IT
+}
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
index 3dea912..e87380f 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
@@ -1,5 +1,5 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum Major {
-    SW, GM, COM, EE, IP
+    SW, GM, COM, EE, IP, AI, MB
 }
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/University.java b/src/main/java/com/assu/server/domain/user/entity/enums/University.java
new file mode 100644
index 0000000..1270336
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/University.java
@@ -0,0 +1,5 @@
+package com.assu.server.domain.user.entity.enums;
+
+public enum University {
+    SSU
+}
diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java
index e042199..9a4c6ce 100644
--- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java
+++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java
@@ -1,4 +1,7 @@
 package com.assu.server.domain.user.repository;
 
-public class StudentRepository {
+import com.assu.server.domain.user.entity.Student;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface StudentRepository extends JpaRepository {
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index ef685a6..cc95565 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -2,7 +2,6 @@
 
 import com.assu.server.global.apiPayload.code.BaseErrorCode;
 import com.assu.server.global.apiPayload.code.ErrorReasonDTO;
-import com.sun.net.httpserver.HttpsServer;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import org.springframework.http.HttpStatus;
@@ -16,23 +15,39 @@ public enum ErrorStatus implements BaseErrorCode {
     _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
     _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
 
+    // 인가 관련 에러
+    AUTHORIZATION_EXCEPTION(HttpStatus.UNAUTHORIZED, "AUTH4001", "인증에 실패하였습니다."),
+    JWT_ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "AUTH4002", "AccessToken이 만료되었습니다."),
+    JWT_REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "AUTH4003", "RefreshToken이 만료되었습니다."),
+    LOGOUT_USER(HttpStatus.UNAUTHORIZED, "AUTH4004", "로그아웃된 유저입니다."),
+    JWT_TOKEN_NOT_RECEIVED(HttpStatus.UNAUTHORIZED, "AUTH4005", "JWT 토큰이 전달되지 않았습니다."),
+    JWT_TOKEN_OUT_OF_FORM(HttpStatus.UNAUTHORIZED, "AUTH4006", "JWT 토큰의 형식이 올바르지 않습니다."),
+    REFRESH_TOKEN_NOT_EQUAL(HttpStatus.UNAUTHORIZED, "AUTH4007", "Refreash 토큰이 일치하지 않습니다."),
+
+    // 인증 에러
+    NOT_VERIFIED_PHONE_NUMBER(HttpStatus.BAD_REQUEST,"AUTH_4007","전화번호 인증에 실패했습니다."),
+
     //페이징 에러
     PAGE_UNDER_ONE(HttpStatus.BAD_REQUEST,"PAGE_4001","페이지는 1이상이여야 합니다."),
 
     // 멤버 에러
     NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."),
     NO_SUCH_ADMIN(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 admin ID 입니다."),
-    NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 partner ID 입니다."),
+    NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4003","존재하지 않는 partner ID 입니다."),
+    NO_SUCH_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4004","존재하지 않는 student ID 입니다."),
+    EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4005","이미 존재하는 전화번호입니다."),
+    EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."),
+    EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."),
+
 
     // 채팅 에러
     NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."),
     NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."),
     NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."),
-    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
+    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다.")
+
 
 
-    // 인증 에러
-    NOT_VERIFIED_PHONE_NUMBER(HttpStatus.BAD_REQUEST,"AUTH_4001","전화번호 인증에 실패했습니다.")
     ;
 
     private final HttpStatus httpStatus;
diff --git a/src/main/java/com/assu/server/global/config/AmazonConfig.java b/src/main/java/com/assu/server/global/config/AmazonConfig.java
new file mode 100644
index 0000000..8235d4a
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/AmazonConfig.java
@@ -0,0 +1,52 @@
+package com.assu.server.global.config;
+
+import lombok.Getter;
+import java.net.URI;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+
+@Configuration
+@Getter
+public class AmazonConfig {
+
+    @Value("${cloud.aws.credentials.accessKey}")
+    private String accessKey;
+
+    @Value("${cloud.aws.credentials.secretKey}")
+    private String secretKey;
+
+    @Value("${cloud.aws.region.static}")
+    private String region;
+
+    @Value("${cloud.aws.s3.bucket}")
+    private String bucket;
+
+    @Bean
+    public StaticCredentialsProvider awsCredentialsProvider() {
+        return StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey));
+    }
+
+    @Bean
+    public S3Client s3Client() {
+        return S3Client.builder()
+                .region(Region.of(region))
+                .credentialsProvider(awsCredentialsProvider())
+                .endpointOverride(URI.create("https://s3." + region + ".amazonaws.com"))
+                .build();
+    }
+
+    @Bean
+    public S3Presigner s3Presigner() {
+        return S3Presigner.builder()
+                .region(Region.of(region))
+                .credentialsProvider(awsCredentialsProvider())
+                .endpointOverride(URI.create("https://s3." + region + ".amazonaws.com"))
+                .build();
+    }
+}
diff --git a/src/main/java/com/assu/server/global/config/AuthProviderConfig.java b/src/main/java/com/assu/server/global/config/AuthProviderConfig.java
new file mode 100644
index 0000000..27d320c
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/AuthProviderConfig.java
@@ -0,0 +1,59 @@
+package com.assu.server.global.config;
+
+import com.assu.server.domain.auth.crypto.StudentPasswordEncoder;
+import com.assu.server.domain.auth.security.*;
+import com.assu.server.domain.auth.security.common.CommonUserDetailsService;
+import com.assu.server.domain.auth.security.common.CommonUsernamePasswordAuthenticationToken;
+import com.assu.server.domain.auth.security.student.StudentUserDetailsService;
+import com.assu.server.domain.auth.security.student.StudentUsernamePasswordAuthenticationToken;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.List;
+
+@Configuration
+@RequiredArgsConstructor
+public class AuthProviderConfig {
+
+    private final CommonUserDetailsService commonUserDetailsService;
+    private final StudentUserDetailsService studentUserDetailsService;
+    private final PasswordEncoder passwordEncoder;                 // BCrypt (공통)
+    private final StudentPasswordEncoder studentPasswordEncoder;   // 학생 전용(AES-GCM 복호화 비교)
+
+    @Bean
+    public DaoAuthenticationProvider commonAuthProvider() {
+        DaoAuthenticationProvider p = new DaoAuthenticationProvider() {
+            @Override public boolean supports(Class authentication) {
+                return CommonUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
+            }
+        };
+        p.setUserDetailsService(commonUserDetailsService);
+        p.setPasswordEncoder(passwordEncoder);
+        return p;
+    }
+
+    @Bean
+    public DaoAuthenticationProvider studentAuthProvider() {
+        DaoAuthenticationProvider p = new DaoAuthenticationProvider() {
+            @Override public boolean supports(Class authentication) {
+                return StudentUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
+            }
+        };
+        p.setUserDetailsService(studentUserDetailsService);
+        p.setPasswordEncoder(studentPasswordEncoder);
+        return p;
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(
+            DaoAuthenticationProvider studentAuthProvider,
+            DaoAuthenticationProvider commonAuthProvider
+    ) {
+        return new ProviderManager(List.of(studentAuthProvider, commonAuthProvider));
+    }
+}
diff --git a/src/main/java/com/assu/server/global/config/ProjectConfig.java b/src/main/java/com/assu/server/global/config/ProjectConfig.java
new file mode 100644
index 0000000..2efd491
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/ProjectConfig.java
@@ -0,0 +1,29 @@
+package com.assu.server.global.config;
+
+import com.assu.server.domain.auth.crypto.AesGcmSchoolCredentialEncryptor;
+import com.assu.server.domain.auth.crypto.SchoolCredentialEncryptor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Base64;
+
+@Configuration
+public class ProjectConfig {
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    @Bean
+    public SchoolCredentialEncryptor schoolCredentialEncryptor(@Value("${assu.security.school-crypto.base64-key}") String base64key) {
+        byte[] keyBytes = Base64.getDecoder().decode(base64key);
+        int len = keyBytes.length; // 16, 24, 32만 허용
+        if (len != 16 && len != 24 && len != 32) {
+            throw new IllegalStateException("AES key must be 16/24/32 bytes after Base64 decoding");
+        }
+        return new AesGcmSchoolCredentialEncryptor(keyBytes);
+    }
+}
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 c50ebd9..c9affe7 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -1,35 +1,51 @@
 package com.assu.server.global.config;
 
+import com.assu.server.domain.auth.security.JwtAuthFilter;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import java.util.List;
 
 @Configuration
 public class SecurityConfig {
 
     @Bean
-    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+    public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
         http
+                .csrf(csrf -> csrf.disable())
+                .cors(cors -> {}) // 기본 CORS 구성 사용(필요하면 CorsConfigurationSource 빈 추가)
+                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                 .authorizeHttpRequests(auth -> auth
+                        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                         .requestMatchers(
                                 "/chat/**",
                                 "/suggestion/**",
                                 "/review/**",
                                 "/ws/**",
-                                "/pub/**",     // STOMP 메시지 전송
-                                "/sub/**",     // STOMP 메시지 구독
+                                "/pub/**",
+                                "/sub/**",
                                 "/v3/api-docs/**",
                                 "/swagger-ui/**",
                                 "/swagger-ui.html",
                                 "/swagger-resources/**",
-                                "/webjars/**"
+                                "/webjars/**",
+                                "/auth/**"
                         ).permitAll()
                         .anyRequest().authenticated()
                 )
-                .csrf(csrf -> csrf.disable()) // websocket은 csrf 필요 없음
                 .formLogin(login -> login.disable())
-                .httpBasic(basic  -> basic.disable());
+                .httpBasic(basic -> basic.disable())
+                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
 
         return http.build();
     }
diff --git a/src/main/java/com/assu/server/global/config/WebConfig.java b/src/main/java/com/assu/server/global/config/WebConfig.java
index bf51854..a861bd7 100644
--- a/src/main/java/com/assu/server/global/config/WebConfig.java
+++ b/src/main/java/com/assu/server/global/config/WebConfig.java
@@ -1,6 +1,5 @@
 package com.assu.server.global.config;
 
-import com.assu.server.global.util.AuthUserArgumentResolver;
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -13,8 +12,6 @@
 @RequiredArgsConstructor
 public class WebConfig implements WebMvcConfigurer {
 
-    private final AuthUserArgumentResolver authUserArgumentResolver;
-
     @Override
     public void addCorsMappings(CorsRegistry registry) {
         registry.addMapping("/**")
@@ -24,9 +21,4 @@ public void addCorsMappings(CorsRegistry registry) {
                 .allowCredentials(false)
                 .maxAge(6000);
     }
-
-    @Override
-    public void addArgumentResolvers(List resolvers) {
-        resolvers.add(authUserArgumentResolver);
-    }
 }
diff --git a/src/main/java/com/assu/server/global/exception/exception/DatabaseException.java b/src/main/java/com/assu/server/global/exception/DatabaseException.java
similarity index 79%
rename from src/main/java/com/assu/server/global/exception/exception/DatabaseException.java
rename to src/main/java/com/assu/server/global/exception/DatabaseException.java
index 90718b2..6704662 100644
--- a/src/main/java/com/assu/server/global/exception/exception/DatabaseException.java
+++ b/src/main/java/com/assu/server/global/exception/DatabaseException.java
@@ -1,4 +1,4 @@
-package com.assu.server.global.exception.exception;
+package com.assu.server.global.exception;
 
 
 import com.assu.server.global.apiPayload.code.BaseErrorCode;
diff --git a/src/main/java/com/assu/server/global/exception/exception/GeneralException.java b/src/main/java/com/assu/server/global/exception/GeneralException.java
similarity index 88%
rename from src/main/java/com/assu/server/global/exception/exception/GeneralException.java
rename to src/main/java/com/assu/server/global/exception/GeneralException.java
index c2a3bed..86dd303 100644
--- a/src/main/java/com/assu/server/global/exception/exception/GeneralException.java
+++ b/src/main/java/com/assu/server/global/exception/GeneralException.java
@@ -1,4 +1,4 @@
-package com.assu.server.global.exception.exception;
+package com.assu.server.global.exception;
 
 
 import com.assu.server.global.apiPayload.code.BaseErrorCode;
diff --git a/src/main/java/com/assu/server/global/exception/exception/GlobalExceptionAdvice.java b/src/main/java/com/assu/server/global/exception/GlobalExceptionAdvice.java
similarity index 99%
rename from src/main/java/com/assu/server/global/exception/exception/GlobalExceptionAdvice.java
rename to src/main/java/com/assu/server/global/exception/GlobalExceptionAdvice.java
index 2ffc115..0794633 100644
--- a/src/main/java/com/assu/server/global/exception/exception/GlobalExceptionAdvice.java
+++ b/src/main/java/com/assu/server/global/exception/GlobalExceptionAdvice.java
@@ -1,4 +1,4 @@
-package com.assu.server.global.exception.exception;
+package com.assu.server.global.exception;
 
 
 import com.assu.server.global.apiPayload.BaseResponse;
diff --git a/src/main/java/com/assu/server/global/exception/exception/annotation/CheckPage.java b/src/main/java/com/assu/server/global/exception/annotation/CheckPage.java
similarity index 77%
rename from src/main/java/com/assu/server/global/exception/exception/annotation/CheckPage.java
rename to src/main/java/com/assu/server/global/exception/annotation/CheckPage.java
index 959c6ca..92e2c41 100644
--- a/src/main/java/com/assu/server/global/exception/exception/annotation/CheckPage.java
+++ b/src/main/java/com/assu/server/global/exception/annotation/CheckPage.java
@@ -1,7 +1,7 @@
-package com.assu.server.global.exception.exception.annotation;
+package com.assu.server.global.exception.annotation;
 
 
-import com.assu.server.global.exception.exception.validator.CheckPageValidator;
+import com.assu.server.global.exception.validator.CheckPageValidator;
 import jakarta.validation.Constraint;
 import jakarta.validation.Payload;
 
diff --git a/src/main/java/com/assu/server/global/exception/exception/validator/CheckPageValidator.java b/src/main/java/com/assu/server/global/exception/validator/CheckPageValidator.java
similarity index 87%
rename from src/main/java/com/assu/server/global/exception/exception/validator/CheckPageValidator.java
rename to src/main/java/com/assu/server/global/exception/validator/CheckPageValidator.java
index abdcc84..fe7aff6 100644
--- a/src/main/java/com/assu/server/global/exception/exception/validator/CheckPageValidator.java
+++ b/src/main/java/com/assu/server/global/exception/validator/CheckPageValidator.java
@@ -1,8 +1,8 @@
-package com.assu.server.global.exception.exception.validator;
+package com.assu.server.global.exception.validator;
 
 
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.annotation.CheckPage;
+import com.assu.server.global.exception.annotation.CheckPage;
 import jakarta.validation.ConstraintValidator;
 import jakarta.validation.ConstraintValidatorContext;
 import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/assu/server/global/util/AuthUserArgumentResolver.java b/src/main/java/com/assu/server/global/util/AuthUserArgumentResolver.java
deleted file mode 100644
index 16aec12..0000000
--- a/src/main/java/com/assu/server/global/util/AuthUserArgumentResolver.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.assu.server.global.util;
-
-import com.assu.server.domain.common.entity.Member;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.GeneralException;
-import lombok.RequiredArgsConstructor;
-import org.springframework.core.MethodParameter;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.stereotype.Component;
-import org.springframework.web.bind.support.WebDataBinderFactory;
-import org.springframework.web.context.request.NativeWebRequest;
-import org.springframework.web.method.support.HandlerMethodArgumentResolver;
-import org.springframework.web.method.support.ModelAndViewContainer;
-
-@Component
-@RequiredArgsConstructor
-public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {
-
-    @Override
-    public boolean supportsParameter(MethodParameter parameter) {
-        return parameter.getParameterType().equals(Member.class);
-    }
-
-    @Override
-    public Object resolveArgument(
-            MethodParameter parameter,
-            ModelAndViewContainer mavContainer,
-            NativeWebRequest webRequest,
-            WebDataBinderFactory binderFactory)
-            throws GeneralException {
-        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-
-        if (authentication == null || !authentication.isAuthenticated()) {
-            return null; // 로그인하지 않은 사용자
-        }
-
-        if (authentication.getPrincipal() instanceof PrincipalDetails principalDetails) {
-            return principalDetails.getMember();
-        }
-
-        throw new GeneralException(ErrorStatus.NO_SUCH_MEMBER);
-    }
-}
diff --git a/src/main/java/com/assu/server/global/util/PrincipalDetails.java b/src/main/java/com/assu/server/global/util/PrincipalDetails.java
deleted file mode 100644
index bc6bd23..0000000
--- a/src/main/java/com/assu/server/global/util/PrincipalDetails.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.assu.server.global.util;
-
-import com.assu.server.domain.common.entity.Member;
-import lombok.RequiredArgsConstructor;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.userdetails.UserDetails;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.stream.Collectors;
-
-@RequiredArgsConstructor
-public class PrincipalDetails implements UserDetails {
-
-    private final Member member;
-
-    @Override
-    public Collection getAuthorities() {
-        List roles = new ArrayList<>();
-        roles.add("ROLE_USER");
-
-        return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
-    }
-
-    @Override
-    public String getPassword() {
-        return null;
-    }
-
-    @Override
-    public String getUsername() {
-        return member.getId().toString();
-    }
-
-    public Member getMember() {
-        return member;
-    }
-
-    @Override
-    public boolean isAccountNonExpired() {
-        return true;
-    }
-
-    @Override
-    public boolean isAccountNonLocked() {
-        return true;
-    }
-
-    @Override
-    public boolean isCredentialsNonExpired() {
-        return true;
-    }
-
-    @Override
-    public boolean isEnabled() {
-        return true;
-    }
-}
-
diff --git a/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java b/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
new file mode 100644
index 0000000..1d0d622
--- /dev/null
+++ b/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
@@ -0,0 +1,138 @@
+package com.assu.server.infra.s3;
+
+import com.assu.server.global.config.AmazonConfig;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+import software.amazon.awssdk.core.ResponseBytes;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
+import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.UUID;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AmazonS3Manager{
+
+    private final S3Client s3Client;
+    private final S3Presigner s3Presigner;
+    private final AmazonConfig amazonConfig;
+
+    //MultipartFile S3에 비공개 업로드
+    public String uploadFile(String keyName, MultipartFile file) {
+        try {
+            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+                    .bucket(amazonConfig.getBucket())
+                    .key(keyName)
+                    .contentType(file.getContentType())
+                    .contentLength(file.getSize())
+                    .build();
+
+            s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
+
+        } catch (Exception e) {
+            log.error("S3 파일 업로드에 실패했습니다. key: {}", keyName, e);
+            throw new RuntimeException("S3 upload failed", e);
+        }
+        return keyName;
+    }
+
+    // fileBytes 를 위한 uploadFile
+    public String uploadFile(String keyName, byte[] fileBytes, String contentType) {
+        if (fileBytes == null || fileBytes.length == 0) {
+            throw new IllegalArgumentException("업로드할 파일이 비어있습니다.");
+        }
+        try {
+            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+                    .bucket(amazonConfig.getBucket())
+                    .key(keyName)
+                    .contentType(contentType)
+                    .contentLength((long) fileBytes.length)
+                    .build();
+
+            s3Client.putObject(putObjectRequest, RequestBody.fromBytes(fileBytes));
+        } catch (Exception e) {
+            log.error("S3 파일 업로드에 실패했습니다. key: {}", keyName, e);
+            throw new RuntimeException("S3 upload failed", e);
+        }
+        return keyName;
+    }
+
+
+    // FE로 url을 보내기 위해 사용하는 메서드
+    public String generatePresignedUrl(String keyName) {
+        if (keyName == null || keyName.isBlank()) return null;
+
+        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+                .bucket(amazonConfig.getBucket())
+                .key(keyName)
+                .build();
+
+        GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
+                .signatureDuration(Duration.ofMinutes(10))
+                .getObjectRequest(getObjectRequest)
+                .build();
+
+        PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest);
+        return presignedRequest.url().toString();
+    }
+
+    // FE로 수정한 파일명으로 다운로드 가능한 url을 보내기 위해 사용하는 메서드
+    public String generatePresignedUrlForDownloadPdfAndWord(String keyName, String fileName) {
+        if (keyName == null || keyName.isBlank()) return null;
+
+        // RFC 5987 인코딩
+        String encodedFilename = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
+                .replace("+", "%20"); // 공백 처리
+        String contentDisposition = "attachment; filename*=UTF-8''" + encodedFilename;
+
+        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+                .bucket(amazonConfig.getBucket())
+                .key(keyName)
+                .responseContentDisposition(contentDisposition)
+                .build();
+
+        GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
+                .signatureDuration(Duration.ofMinutes(10))
+                .getObjectRequest(getObjectRequest)
+                .build();
+
+        PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest);
+        return presignedRequest.url().toString();
+    }
+
+    // S3에서 key에 해당하는 파일을 다운로드하여 byte 배열로 반환
+    public byte[] downloadFile(String keyName) {
+        if (keyName == null || keyName.isBlank()) {
+            throw new IllegalArgumentException("파일 키가 유효하지 않습니다.");
+        }
+        try {
+            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+                    .bucket(amazonConfig.getBucket())
+                    .key(keyName)
+                    .build();
+
+            ResponseBytes objectBytes = s3Client.getObjectAsBytes(getObjectRequest);
+            return objectBytes.asByteArray();
+        } catch (Exception e) {
+            log.error("S3 파일 다운로드에 실패했습니다. key: {}", keyName, e);
+            throw new RuntimeException("S3 file download failed", e);
+        }
+    }
+
+    public String generateKeyName(String path) {
+        return path + '/' + UUID.randomUUID();
+    }
+
+}
diff --git a/src/main/java/com/assu/server/infra/s3/MultipartJackson2HttpMessageConverter.java b/src/main/java/com/assu/server/infra/s3/MultipartJackson2HttpMessageConverter.java
new file mode 100644
index 0000000..da2b766
--- /dev/null
+++ b/src/main/java/com/assu/server/infra/s3/MultipartJackson2HttpMessageConverter.java
@@ -0,0 +1,33 @@
+package com.assu.server.infra.s3;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Type;
+
+@Component
+public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
+
+    /** "Content-Type: multipart/form-data" 헤더를 지원하는 HTTP 요청 변환기 */
+    public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
+        super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
+    }
+
+    @Override
+    public boolean canWrite(Class clazz, MediaType mediaType) {
+        return false;
+    }
+
+    @Override
+    public boolean canWrite(Type type, Class clazz, MediaType mediaType) {
+        return false;
+    }
+
+    @Override
+    protected boolean canWrite(MediaType mediaType) {
+        return false;
+    }
+}
+

From a15b452ec1c8a5cbfbb98035663982174bc73739 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 17 Aug 2025 00:43:08 +1000
Subject: [PATCH 064/270] =?UTF-8?q?[FEAT/#20]=20=ED=85=8C=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=EC=9A=A9=20=EC=95=8C=EB=A6=BC=20API=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |  2 +-
 .../com/assu/server/ServerApplication.java    |  2 +
 .../common/repository/MemberRepository.java   |  1 +
 .../controller/DeviceTokenController.java     | 36 +++++++---
 .../deviceToken/dto/DeviceTokenRequest.java   |  8 +++
 .../service/DeviceTokenService.java           |  4 +-
 .../service/DeviceTokenServiceImpl.java       | 17 +++--
 .../controller/NotificationController.java    | 55 ++++++++++----
 .../dto/QueueNotificationRequest.java         | 37 ++++++++++
 .../service/NotificationCommandService.java   |  5 +-
 .../NotificationCommandServiceImpl.java       | 71 ++++++++++++++++++-
 .../service/NotificationQueryService.java     |  4 +-
 .../service/NotificationQueryServiceImpl.java | 38 ++++++++--
 .../apiPayload/code/status/ErrorStatus.java   |  1 +
 .../server/global/config/SecurityConfig.java  |  2 +
 15 files changed, 238 insertions(+), 45 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java

diff --git a/build.gradle b/build.gradle
index 287c62a..5e5edba 100644
--- a/build.gradle
+++ b/build.gradle
@@ -53,7 +53,7 @@ dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-websocket'
 
 	// maria db
-	runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
+	implementation 'org.mariadb.jdbc:mariadb-java-client:3.4.1'
 
 	// spring test
 	testImplementation 'org.springframework.boot:spring-boot-starter-test'
diff --git a/src/main/java/com/assu/server/ServerApplication.java b/src/main/java/com/assu/server/ServerApplication.java
index 3864e11..27c7b81 100644
--- a/src/main/java/com/assu/server/ServerApplication.java
+++ b/src/main/java/com/assu/server/ServerApplication.java
@@ -3,9 +3,11 @@
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
 @EnableJpaAuditing
+@EnableScheduling
 public class ServerApplication {
 
 	public static void main(String[] args) {
diff --git a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
index 207a939..2775b46 100644
--- a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
+++ b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
@@ -6,4 +6,5 @@
 
 
 public interface MemberRepository extends JpaRepository {
+    Member findMemberById(Long id);
 }
diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index f51b642..a715eba 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -1,7 +1,11 @@
 package com.assu.server.domain.deviceToken.controller;
 
 import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.deviceToken.dto.DeviceTokenRequest;
 import com.assu.server.domain.deviceToken.service.DeviceTokenService;
+import com.assu.server.global.apiPayload.BaseResponse;
+import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
@@ -10,18 +14,30 @@
 @RequiredArgsConstructor
 public class DeviceTokenController {
     private final DeviceTokenService service;
-
-    public record RegisterTokenReq(String token) {}
-
+    @Operation(
+            summary = "device Token 등록 API",
+            description = "멤버 아이디와 fcm Token을 보내주세요."
+    )
     @PostMapping("/register")
-    public void register(@RequestBody RegisterTokenReq req,
-                         @RequestParam Long MemberId)
-    {
-        service.register(req.token(), MemberId);
+    public BaseResponse register(@RequestBody DeviceTokenRequest req,
+                                         @RequestParam Long memberId) {
+        service.register(req.getToken(), memberId);
+        return BaseResponse.onSuccess(
+                SuccessStatus._OK,
+                "Device token registered successfully. memberId=" + memberId
+        );
     }
 
-    @DeleteMapping("/unregister/{token_id}")
-    public void unregister(@PathVariable String token_id){
-        service.unregister(token_id);
+    @Operation(
+            summary = "device Token 등록 해제 API",
+            description = "로그아웃, 회원 탈퇴시 호출하시면 됩니다. 멤버의 tokenId를 보내주세요!"
+    )
+    @DeleteMapping("/unregister/{tokenId}")
+    public BaseResponse unregister(@PathVariable Long tokenId) {
+        service.unregister(tokenId);
+        return BaseResponse.onSuccess(
+                SuccessStatus._OK,
+                "Device token unregistered successfully. tokenId=" + tokenId
+        );
     }
 }
diff --git a/src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java b/src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java
new file mode 100644
index 0000000..77df7f4
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java
@@ -0,0 +1,8 @@
+package com.assu.server.domain.deviceToken.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceTokenRequest {
+    private String token;
+}
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
index cb4172a..0e03b3d 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
@@ -3,6 +3,6 @@
 import com.assu.server.domain.common.entity.Member;
 
 public interface DeviceTokenService {
-    void register(String token, Long memberId);
-    void unregister(String token);
+    void register(String tokenId, Long memberId);
+    void unregister(Long tokenId);
 }
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
index 5aedc84..830d3c3 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.deviceToken.service;
 
 import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.common.repository.MemberRepository;
 import com.assu.server.domain.deviceToken.entity.DeviceToken;
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
 import jakarta.transaction.Transactional;
@@ -11,22 +12,24 @@
 @RequiredArgsConstructor
 public class DeviceTokenServiceImpl implements DeviceTokenService {
     private final DeviceTokenRepository deviceTokenRepository;
-    //private final MemberRepository memberRepository;
+    private final MemberRepository memberRepository;
 
     @Transactional
     @Override
-    public void register(String token, Long memberId) {
-        Member member = memberRepository.findById(memberId);
+    public void register(String tokenId, Long memberId) {
+        Member member = memberRepository.findMemberById(memberId);
 
-        DeviceToken dt = deviceTokenRepository.findByToken(token)
+        DeviceToken dt = deviceTokenRepository.findByToken(tokenId)
                 .map(deviceToken -> { deviceToken.setActive(true); return deviceToken; })
-                .orElse(DeviceToken.builder().member(member).token(token).active(true).build());
+                .orElse(DeviceToken.builder().member(member).token(tokenId).active(true).build());
         deviceTokenRepository.save(dt);
     }
 
     @Override
     @Transactional
-    public void unregister(String token) {
-        deviceTokenRepository.findByToken(token).ifPresent(deviceToken -> deviceToken.setActive(false));
+    public void unregister(Long tokenId) {
+        deviceTokenRepository.findById(tokenId).ifPresent(
+                deviceToken -> deviceToken.setActive(false)
+        );
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 03a17f1..4581236 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -1,8 +1,13 @@
 package com.assu.server.domain.notification.controller;
 
-import com.assu.server.domain.notification.dto.NotificationResponseDTO;
+import com.assu.server.domain.notification.dto.*;
+import com.assu.server.domain.notification.entity.NotificationType;
 import com.assu.server.domain.notification.service.NotificationCommandService;
 import com.assu.server.domain.notification.service.NotificationQueryService;
+import com.assu.server.global.apiPayload.BaseResponse;
+import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Sort;
@@ -10,6 +15,8 @@
 import org.springframework.web.bind.annotation.*;
 import org.springframework.data.domain.Pageable;
 import java.nio.file.AccessDeniedException;
+import java.util.HashMap;
+import java.util.Map;
 
 @RestController
 @RequestMapping("notifications")
@@ -18,22 +25,42 @@ public class NotificationController {
     private final NotificationQueryService query;
     private final NotificationCommandService command;
 
+    @Operation(
+            summary = "알림 목록 조회 API",
+            description = "page는 1 이상이어야 합니다."
+    )
     @GetMapping
-    public Page list(
-            @RequestParam(defaultValue = "all") String status,
-            @PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
-            @RequestParam Long memberId) {
-
-        if (!"all".equalsIgnoreCase(status) && !"unread".equalsIgnoreCase(status)) {
-            throw new IllegalArgumentException("status must be one of [all, unread]");
-        }
-        return query.listByStatus(status, pageable, memberId);
+    public BaseResponse> list(
+            @RequestParam(defaultValue = "all") String status,   // all | unread
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "20") Integer size,
+            @RequestParam Long memberId
+    ) {
+        Map body = query.getNotifications(status, page, size, memberId);
+        return BaseResponse.onSuccess(SuccessStatus._OK, body);
     }
 
 
-    @PostMapping("/{notification_id}/read")
-    public void markRead(@PathVariable Long id,
+    @Operation(
+            summary = "알림 읽음 처리 API",
+            description = "알림 아이디를 보내주세요"
+    )
+    @PostMapping("/{notificationId}/read")
+    public BaseResponse markRead(@PathVariable Long notificationId,
                          @RequestParam Long memberId) throws AccessDeniedException {
-        command.markRead(id, memberId);
-    }U
+        command.markRead(notificationId, memberId);
+        return BaseResponse.onSuccess(SuccessStatus._OK,"The notification has been marked as read successfully." + notificationId);
+    }
+
+    @Operation(
+            summary = "알림 전송 테스트 API",
+            description = "API 명세서의 [notification > 알림 보내기 테스트] 페이지의 예시 request를 참고해서 테스트 해주세요!"+
+                    "deviceToken을 등록하신 이후에 확인 가능합니다."
+    )
+    @PostMapping("/queue")
+    public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest req) {
+        command.queue(req); // 서비스로 위임
+        return BaseResponse.onSuccess(SuccessStatus._OK, "Notification delivery succeeded.");
+    }
+
 }
diff --git a/src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java b/src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java
new file mode 100644
index 0000000..6f7a041
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java
@@ -0,0 +1,37 @@
+package com.assu.server.domain.notification.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class QueueNotificationRequest {
+    @NotNull private Long receiverId;
+    @NotNull private String type;
+
+    // 공통(선택)
+    private String content;
+    private String title;
+    private String deeplink;
+
+    // CHAT
+    private Long roomId;
+    private String senderName;
+    private String message;
+
+    // PARTNER_SUGGESTION
+    private Long suggestionId;
+
+    // ORDER
+    private Long orderId;
+    private String table_num;
+    private String paper_content;
+
+    // PARTNER_PROPOSAL
+    private Long proposalId;
+    private String partner_name;
+
+    // 기타 타입 공용으로 쓰고 싶으면 유지
+    private Long refId; // 있으면 우선 사용 (없을 때는 타입별 필드에서 채움)
+}
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
index 597494f..dbb3b48 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.notification.service;
 
+import com.assu.server.domain.notification.dto.QueueNotificationRequest;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.entity.NotificationType;
 
@@ -7,6 +8,8 @@
 import java.util.Map;
 
 public interface NotificationCommandService {
-    Notification createAndQueue(com.assu.server.domain.common.entity.Member receiver, NotificationType type, Long refId, Map ctx);
+    Notification createAndQueue(Long receiverId, NotificationType type, Long refId, Map ctx);
     void markRead(Long notificationId, Long currentMemberId) throws AccessDeniedException;
+    void queue(QueueNotificationRequest req);
+
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index 01feba3..ff911b8 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -1,5 +1,8 @@
 package com.assu.server.domain.notification.service;
 
+import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.common.repository.MemberRepository;
+import com.assu.server.domain.notification.dto.QueueNotificationRequest;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.entity.NotificationOutbox;
 import com.assu.server.domain.notification.entity.NotificationType;
@@ -11,7 +14,10 @@
 import org.springframework.stereotype.Service;
 
 import java.nio.file.AccessDeniedException;
+import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 
 @Service
 @RequiredArgsConstructor
@@ -19,11 +25,14 @@ public class NotificationCommandServiceImpl implements NotificationCommandServic
     private final NotificationRepository notificationRepository;
     private final NotificationOutboxRepository outboxRepository;
     private final NotificationFactory notificationFactory;
+    private final MemberRepository memberRepository;
 
     @Transactional
     @Override
-    public Notification createAndQueue(com.assu.server.domain.common.entity.Member receiver, NotificationType type, Long refId, Map ctx) {
-        Notification notification = notificationFactory.create(receiver, type, refId, ctx);
+    public Notification createAndQueue(Long receiverId, NotificationType type, Long refId, Map ctx) {
+        Member member = memberRepository.findMemberById(receiverId);
+        Notification notification = notificationFactory.create(member, type, refId, ctx);
+
         notificationRepository.save(notification);
         outboxRepository.save(NotificationOutbox.builder()
                 .notification(notification)
@@ -43,4 +52,62 @@ public void markRead(Long notificationId, Long currentMemberId) throws AccessDen
         }
         n.markRead();
     }
+
+    @Transactional
+    @Override
+    public void queue(QueueNotificationRequest req) {
+        NotificationType type;
+        try {
+            type = NotificationType.valueOf(req.getType().toUpperCase(Locale.ROOT));
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException("Unsupported type: " + req.getType());
+        }
+
+        Map ctx = new HashMap<>();
+        // 공통 컨텍스트(있으면 추가)
+        if (req.getContent()  != null) ctx.put("content",  req.getContent());
+        if (req.getTitle()    != null) ctx.put("title",    req.getTitle());
+        if (req.getDeeplink() != null) ctx.put("deeplink", req.getDeeplink());
+
+        Long refId = req.getRefId(); // 우선 refId가 넘어오면 사용
+
+        switch (type) {
+            case CHAT -> {
+                // refId 우선순위: refId 필드 → roomId
+                if (refId == null) {
+                    refId = Objects.requireNonNull(req.getRoomId(), "roomId is required for CHAT");
+                }
+                ctx.put("senderName", Objects.requireNonNull(req.getSenderName(), "senderName is required for CHAT"));
+                ctx.put("message",    Objects.requireNonNull(req.getMessage(),    "message is required for CHAT"));
+            }
+            case PARTNER_SUGGESTION -> {
+                if (refId == null) {
+                    refId = Objects.requireNonNull(req.getSuggestionId(), "suggestionId is required for PARTNER_SUGGESTION");
+                }
+                // 추가 ctx 없음
+            }
+            case ORDER -> {
+                if (refId == null) {
+                    refId = Objects.requireNonNull(req.getOrderId(), "orderId is required for ORDER");
+                }
+                ctx.put("table_num",     Objects.requireNonNull(req.getTable_num(),     "table_num is required for ORDER"));
+                ctx.put("paper_content", Objects.requireNonNull(req.getPaper_content(), "paper_content is required for ORDER"));
+            }
+            case PARTNER_PROPOSAL -> {
+                if (refId == null) {
+                    refId = Objects.requireNonNull(req.getProposalId(), "proposalId is required for PARTNER_PROPOSAL");
+                }
+                ctx.put("partner_name", Objects.requireNonNull(req.getPartner_name(), "partner_name is required for PARTNER_PROPOSAL"));
+            }
+            default -> throw new IllegalArgumentException("Unsupported type: " + type);
+        }
+
+        // 최종 큐 적재 (Outbox → Dispatcher가 발송)
+        createAndQueue(
+                Objects.requireNonNull(req.getReceiverId(), "receiverId is required"),
+                type,
+                Objects.requireNonNull(refId, "refId is required"),
+                ctx
+        );
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java
index fc45a22..9ff02af 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java
@@ -4,6 +4,8 @@
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 
+import java.util.Map;
+
 public interface NotificationQueryService {
-    Page listByStatus(String status, Pageable pageable, Long memberId);
+    Map getNotifications(String status, int page, int size, Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
index 39c8d8e..c4efb86 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
@@ -1,33 +1,57 @@
 package com.assu.server.domain.notification.service;
 
-import com.assu.server.domain.common.entity.Member;
 import com.assu.server.domain.notification.converter.NotificationConverter;
 import com.assu.server.domain.notification.dto.NotificationResponseDTO;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.repository.NotificationRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.exception.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 
-import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 
 @Service
 @RequiredArgsConstructor
 public class NotificationQueryServiceImpl implements NotificationQueryService {
     private final NotificationRepository notificationRepository;
-    //private final MemberRepository memberRepository;
 
     @Transactional
     @Override
-    public Page listByStatus(String status, Pageable pageable, Long memberId) {
-        boolean unreadOnly = "unread".equalsIgnoreCase(status);
+    public Map getNotifications(String status, int page, int size, Long memberId) {
+        // 입력 검증
+        if (page < 1)  throw new DatabaseException(ErrorStatus.PAGE_UNDER_ONE);
+        if (size < 1 || size > 200) throw new DatabaseException(ErrorStatus.PAGE_SIZE_INVALID);
+
+        String s = status == null ? "all" : status.toLowerCase();
+        if (!s.equals("all") && !s.equals("unread")) {
+            // 필요 시 ErrorStatus에 INVALID_NOTIFICATION_STATUS_FILTER 추가해서 사용 가능
+            throw new IllegalArgumentException("status must be one of [all, unread]");
+        }
 
-        Page page = unreadOnly
+        Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "id"));
+
+        // 상태별 조회 (여기서 바로 분기하고 변환까지)
+        Page rawPage = s.equals("unread")
                 ? notificationRepository.findByReceiverIdAndIsReadFalse(memberId, pageable)
                 : notificationRepository.findByReceiverId(memberId, pageable);
 
-        return page.map(NotificationConverter::toDto);
+        Page p = rawPage.map(NotificationConverter::toDto);
+
+        // 응답 포맷 구성
+        Map body = new LinkedHashMap<>();
+        body.put("items", p.getContent());
+        body.put("page", p.getNumber() + 1);      // 1-base로 반환
+        body.put("size", p.getSize());
+        body.put("totalPages", p.getTotalPages());
+        body.put("totalElements", p.getTotalElements());
+        return body;
     }
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 8bf2f46..ebca508 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -18,6 +18,7 @@ public enum ErrorStatus implements BaseErrorCode {
 
     //페이징 에러
     PAGE_UNDER_ONE(HttpStatus.BAD_REQUEST,"PAGE_4001","페이지는 1이상이여야 합니다."),
+    PAGE_SIZE_INVALID(HttpStatus.BAD_REQUEST,"PAGE_4002","size는 1~200 사이여야 합니다."),
 
     // 멤버 에러
     NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."),
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 c50ebd9..770f99f 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -13,6 +13,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
                 .authorizeHttpRequests(auth -> auth
                         .requestMatchers(
+                                "/deviceTokens/**",
+                                "/notifications/**",
                                 "/chat/**",
                                 "/suggestion/**",
                                 "/review/**",

From 07057d4421d89f8041b32a7b0d08b5120f3bcec8 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 17 Aug 2025 00:53:23 +1000
Subject: [PATCH 065/270] =?UTF-8?q?[FEAT/#20]=20=EC=97=90=EB=9F=AC=20?=
 =?UTF-8?q?=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/DeviceTokenServiceImpl.java       | 15 +++-
 .../dto/QueueNotificationRequest.java         | 35 ++++++++-
 .../NotificationCommandServiceImpl.java       | 75 +++++++++++--------
 .../service/NotificationQueryServiceImpl.java | 22 +++---
 .../apiPayload/code/status/ErrorStatus.java   | 10 +++
 5 files changed, 110 insertions(+), 47 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
index 830d3c3..1d5925e 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
@@ -4,6 +4,8 @@
 import com.assu.server.domain.common.repository.MemberRepository;
 import com.assu.server.domain.deviceToken.entity.DeviceToken;
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.exception.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -18,6 +20,9 @@ public class DeviceTokenServiceImpl implements DeviceTokenService {
     @Override
     public void register(String tokenId, Long memberId) {
         Member member = memberRepository.findMemberById(memberId);
+        if (member == null) {
+            throw new DatabaseException(ErrorStatus.NO_SUCH_MEMBER);
+        }
 
         DeviceToken dt = deviceTokenRepository.findByToken(tokenId)
                 .map(deviceToken -> { deviceToken.setActive(true); return deviceToken; })
@@ -25,11 +30,13 @@ public void register(String tokenId, Long memberId) {
         deviceTokenRepository.save(dt);
     }
 
-    @Override
     @Transactional
+    @Override
     public void unregister(Long tokenId) {
-        deviceTokenRepository.findById(tokenId).ifPresent(
-                deviceToken -> deviceToken.setActive(false)
-        );
+        deviceTokenRepository.findById(tokenId)
+                .ifPresentOrElse(
+                        deviceToken -> deviceToken.setActive(false),
+                        () -> { throw new DatabaseException(ErrorStatus.DEVICE_TOKEN_NOT_FOUND); }
+                );
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java b/src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java
index 6f7a041..11eec78 100644
--- a/src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java
+++ b/src/main/java/com/assu/server/domain/notification/dto/QueueNotificationRequest.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.notification.dto;
 
+import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.Getter;
 import lombok.Setter;
@@ -7,31 +8,57 @@
 @Getter
 @Setter
 public class QueueNotificationRequest {
-    @NotNull private Long receiverId;
-    @NotNull private String type;
+
+    @Schema(description = "알림을 받을 멤버 ID", example = "1")
+    @NotNull
+    private Long receiverId;
+
+    @Schema(description = "알림 타입 (CHAT, PARTNER_SUGGESTION, ORDER, PARTNER_PROPOSAL)", example = "CHAT")
+    @NotNull
+    private String type;
 
     // 공통(선택)
+    @Schema(description = "알림 내용", example = "새로운 메시지가 있습니다.")
     private String content;
+
+    @Schema(description = "알림 제목", example = "채팅 알림")
     private String title;
+
+    @Schema(description = "앱 내 이동할 경로 (deeplink)", example = "app://chat/10")
     private String deeplink;
 
     // CHAT
+    @Schema(description = "채팅방 ID", example = "101")
     private Long roomId;
+
+    @Schema(description = "보낸 사람 이름", example = "홍길동")
     private String senderName;
+
+    @Schema(description = "메시지 내용", example = "안녕하세요! 오늘 일정 확인 부탁드려요.")
     private String message;
 
     // PARTNER_SUGGESTION
+    @Schema(description = "제휴 제안 ID", example = "2001")
     private Long suggestionId;
 
     // ORDER
+    @Schema(description = "주문 ID", example = "3001")
     private Long orderId;
+
+    @Schema(description = "테이블 번호", example = "11")
     private String table_num;
+
+    @Schema(description = "전단지 내용", example = "20,000원 이상 구매 시 10% 할인")
     private String paper_content;
 
     // PARTNER_PROPOSAL
+    @Schema(description = "제휴 제안 ID", example = "4001")
     private Long proposalId;
+
+    @Schema(description = "파트너 이름", example = "역전할머니맥주 송신대점")
     private String partner_name;
 
     // 기타 타입 공용으로 쓰고 싶으면 유지
-    private Long refId; // 있으면 우선 사용 (없을 때는 타입별 필드에서 채움)
-}
+    @Schema(description = "참조 ID (타입별로 roomId/orderId 등 대신 사용할 수 있음)", example = "9999")
+    private Long refId;
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index ff911b8..196ad7f 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -8,6 +8,8 @@
 import com.assu.server.domain.notification.entity.NotificationType;
 import com.assu.server.domain.notification.repository.NotificationOutboxRepository;
 import com.assu.server.domain.notification.repository.NotificationRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.exception.exception.DatabaseException;
 import com.assu.server.infra.NotificationFactory;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
@@ -31,6 +33,10 @@ public class NotificationCommandServiceImpl implements NotificationCommandServic
     @Override
     public Notification createAndQueue(Long receiverId, NotificationType type, Long refId, Map ctx) {
         Member member = memberRepository.findMemberById(receiverId);
+        if (member == null) {
+            throw new DatabaseException(ErrorStatus.NO_SUCH_MEMBER);
+        }
+
         Notification notification = notificationFactory.create(member, type, refId, ctx);
 
         notificationRepository.save(notification);
@@ -42,13 +48,14 @@ public Notification createAndQueue(Long receiverId, NotificationType type, Long
         return notification;
     }
 
-
     @Transactional
     @Override
-    public void markRead(Long notificationId, Long currentMemberId) throws AccessDeniedException {
-        Notification n = notificationRepository.findById(notificationId).orElseThrow();
+    public void markRead(Long notificationId, Long currentMemberId) {
+        Notification n = notificationRepository.findById(notificationId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NOTIFICATION_NOT_FOUND));
+
         if (!n.getReceiver().getId().equals(currentMemberId)) {
-            throw new AccessDeniedException("not yours");
+            throw new DatabaseException(ErrorStatus.NOTIFICATION_ACCESS_DENIED);
         }
         n.markRead();
     }
@@ -60,54 +67,62 @@ public void queue(QueueNotificationRequest req) {
         try {
             type = NotificationType.valueOf(req.getType().toUpperCase(Locale.ROOT));
         } catch (IllegalArgumentException e) {
-            throw new IllegalArgumentException("Unsupported type: " + req.getType());
+            throw new DatabaseException(ErrorStatus.INVALID_NOTIFICATION_TYPE);
         }
 
         Map ctx = new HashMap<>();
-        // 공통 컨텍스트(있으면 추가)
         if (req.getContent()  != null) ctx.put("content",  req.getContent());
         if (req.getTitle()    != null) ctx.put("title",    req.getTitle());
         if (req.getDeeplink() != null) ctx.put("deeplink", req.getDeeplink());
 
-        Long refId = req.getRefId(); // 우선 refId가 넘어오면 사용
+        Long refId = req.getRefId();
 
         switch (type) {
             case CHAT -> {
-                // refId 우선순위: refId 필드 → roomId
-                if (refId == null) {
-                    refId = Objects.requireNonNull(req.getRoomId(), "roomId is required for CHAT");
+                if (refId == null && req.getRoomId() == null) {
+                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+                }
+                refId = (refId != null) ? refId : req.getRoomId();
+                if (req.getSenderName() == null || req.getMessage() == null) {
+                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                ctx.put("senderName", Objects.requireNonNull(req.getSenderName(), "senderName is required for CHAT"));
-                ctx.put("message",    Objects.requireNonNull(req.getMessage(),    "message is required for CHAT"));
+                ctx.put("senderName", req.getSenderName());
+                ctx.put("message", req.getMessage());
             }
             case PARTNER_SUGGESTION -> {
-                if (refId == null) {
-                    refId = Objects.requireNonNull(req.getSuggestionId(), "suggestionId is required for PARTNER_SUGGESTION");
+                if (refId == null && req.getSuggestionId() == null) {
+                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                // 추가 ctx 없음
+                refId = (refId != null) ? refId : req.getSuggestionId();
             }
             case ORDER -> {
-                if (refId == null) {
-                    refId = Objects.requireNonNull(req.getOrderId(), "orderId is required for ORDER");
+                if (refId == null && req.getOrderId() == null) {
+                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                ctx.put("table_num",     Objects.requireNonNull(req.getTable_num(),     "table_num is required for ORDER"));
-                ctx.put("paper_content", Objects.requireNonNull(req.getPaper_content(), "paper_content is required for ORDER"));
+                refId = (refId != null) ? refId : req.getOrderId();
+                if (req.getTable_num() == null || req.getPaper_content() == null) {
+                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+                }
+                ctx.put("table_num", req.getTable_num());
+                ctx.put("paper_content", req.getPaper_content());
             }
             case PARTNER_PROPOSAL -> {
-                if (refId == null) {
-                    refId = Objects.requireNonNull(req.getProposalId(), "proposalId is required for PARTNER_PROPOSAL");
+                if (refId == null && req.getProposalId() == null) {
+                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+                }
+                refId = (refId != null) ? refId : req.getProposalId();
+                if (req.getPartner_name() == null) {
+                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                ctx.put("partner_name", Objects.requireNonNull(req.getPartner_name(), "partner_name is required for PARTNER_PROPOSAL"));
+                ctx.put("partner_name", req.getPartner_name());
             }
-            default -> throw new IllegalArgumentException("Unsupported type: " + type);
+            default -> throw new DatabaseException(ErrorStatus.INVALID_NOTIFICATION_TYPE);
+        }
+
+        if (req.getReceiverId() == null) {
+            throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
         }
 
-        // 최종 큐 적재 (Outbox → Dispatcher가 발송)
-        createAndQueue(
-                Objects.requireNonNull(req.getReceiverId(), "receiverId is required"),
-                type,
-                Objects.requireNonNull(refId, "refId is required"),
-                ctx
-        );
+        createAndQueue(req.getReceiverId(), type, refId, ctx);
     }
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
index c4efb86..99dc5df 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.notification.service;
 
+import com.assu.server.domain.common.repository.MemberRepository;
 import com.assu.server.domain.notification.converter.NotificationConverter;
 import com.assu.server.domain.notification.dto.NotificationResponseDTO;
 import com.assu.server.domain.notification.entity.Notification;
@@ -22,33 +23,36 @@
 @RequiredArgsConstructor
 public class NotificationQueryServiceImpl implements NotificationQueryService {
     private final NotificationRepository notificationRepository;
+    private final MemberRepository memberRepository;
 
     @Transactional
     @Override
     public Map getNotifications(String status, int page, int size, Long memberId) {
-        // 입력 검증
-        if (page < 1)  throw new DatabaseException(ErrorStatus.PAGE_UNDER_ONE);
+        // 1) 파라미터 검증
+        if (page < 1) throw new DatabaseException(ErrorStatus.PAGE_UNDER_ONE);
         if (size < 1 || size > 200) throw new DatabaseException(ErrorStatus.PAGE_SIZE_INVALID);
 
-        String s = status == null ? "all" : status.toLowerCase();
+        if (!memberRepository.existsById(memberId)) {
+            throw new DatabaseException(ErrorStatus.NO_SUCH_MEMBER);
+        }
+
+        String s = (status == null ? "all" : status.toLowerCase());
         if (!s.equals("all") && !s.equals("unread")) {
-            // 필요 시 ErrorStatus에 INVALID_NOTIFICATION_STATUS_FILTER 추가해서 사용 가능
-            throw new IllegalArgumentException("status must be one of [all, unread]");
+            throw new DatabaseException(ErrorStatus.INVALID_NOTIFICATION_STATUS_FILTER);
         }
 
+        // 2) 조회
         Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "id"));
-
-        // 상태별 조회 (여기서 바로 분기하고 변환까지)
         Page rawPage = s.equals("unread")
                 ? notificationRepository.findByReceiverIdAndIsReadFalse(memberId, pageable)
                 : notificationRepository.findByReceiverId(memberId, pageable);
 
         Page p = rawPage.map(NotificationConverter::toDto);
 
-        // 응답 포맷 구성
+        // 3) 응답 포맷
         Map body = new LinkedHashMap<>();
         body.put("items", p.getContent());
-        body.put("page", p.getNumber() + 1);      // 1-base로 반환
+        body.put("page", p.getNumber() + 1);
         body.put("size", p.getSize());
         body.put("totalPages", p.getTotalPages());
         body.put("totalElements", p.getTotalElements());
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index ebca508..a3816a7 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -31,6 +31,16 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."),
     NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
+    // 알림(Notification) 에러
+    INVALID_NOTIFICATION_STATUS_FILTER(HttpStatus.BAD_REQUEST,"NOTIFICATION_4001","유효하지 않은 알림 status 필터입니다. (all | unread 만 허용)"),
+    INVALID_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST,"NOTIFICATION_4002","지원하지 않는 알림 타입입니다."),
+    NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND,"NOTIFICATION_4003","존재하지 않는 알림입니다."),
+    NOTIFICATION_ACCESS_DENIED(HttpStatus.FORBIDDEN,"NOTIFICATION_4004","해당 알림에 접근할 권한이 없습니다."),
+    MISSING_NOTIFICATION_FIELD(HttpStatus.BAD_REQUEST,"NOTIFICATION_4005","알림 생성에 필요한 필드가 누락되었습니다."),
+
+    // 디바이스 토큰(DeviceToken) 에러
+    DEVICE_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND,"DEVICE_4001","존재하지 않는 Device Token 입니다."),
+    DEVICE_TOKEN_REGISTER_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"DEVICE_5001","Device Token 등록에 실패했습니다.")
 
     ;
 

From 96a5d11268dd0e70838d2f86f9e989682c404bc5 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 17 Aug 2025 00:56:03 +1000
Subject: [PATCH 066/270] =?UTF-8?q?[MOD/#20]=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?=
 =?UTF-8?q?=EB=9F=AC=20=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/inquiry/controller/InquiryController.java      | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index f15203d..6e3fc4a 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -25,7 +25,7 @@ public class InquiryController {
     private final InquiryService inquiryService;
 
     @Operation(
-            summary = "문의를 생성하는 API입니다.",
+            summary = "문의를 생성하는 API",
             description = "셍성 성공시 생성된 문의의 ID를 반환합니다."
     )
     @PostMapping
@@ -38,7 +38,7 @@ public BaseResponse create(
     }
 
     @Operation(
-            summary = "문의 목록을 조회하는 API 입니다.",
+            summary = "문의 목록을 조회하는 API",
             description = "page는 1 이상이어야 합니다."
     )
     @GetMapping
@@ -54,7 +54,7 @@ public BaseResponse> list(
 
     /** 단건 상세 조회*/
     @Operation(
-            summary = "문의 단건 상세 조회 API 입니다.",
+            summary = "문의 단건 상세 조회 API",
             description = "문의 ID를 보내주세요."
     )
     @GetMapping("/{inquiryId}")
@@ -68,7 +68,7 @@ public BaseResponse get(
 
     /** 문의 답변*/
     @Operation(
-            summary = "운영자 답변 API입니다.",
+            summary = "운영자 답변 API",
             description = "문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다."
     )
     @PatchMapping("/{inquiryId}/answer")

From bc8452068f28549054337e47bf66f32044d0cbb5 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 17 Aug 2025 16:29:19 +1000
Subject: [PATCH 067/270] =?UTF-8?q?[FEAT/#20]=20=EC=95=8C=EB=A6=BC=20?=
 =?UTF-8?q?=EC=9C=A0=ED=98=95=EB=B3=84=20ON/OFF=20=ED=86=A0=EA=B8=80=20API?=
 =?UTF-8?q?=20=EA=B0=9C=EB=B0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/NotificationController.java    | 11 +++++-
 .../dto/NotificationSettingRequestDTO.java    | 12 ++++++
 .../entity/NotificationSetting.java           | 32 +++++++++++++++
 .../repository/NotificationRepository.java    |  8 ++--
 .../NotificationSettingRepository.java        | 12 ++++++
 .../service/NotificationCommandService.java   |  3 +-
 .../NotificationCommandServiceImpl.java       | 39 +++++++++++++++++++
 7 files changed, 111 insertions(+), 6 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/notification/dto/NotificationSettingRequestDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
 create mode 100644 src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java

diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 4581236..7ad1c4f 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -59,8 +59,17 @@ public BaseResponse markRead(@PathVariable Long notificationId,
     )
     @PostMapping("/queue")
     public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest req) {
-        command.queue(req); // 서비스로 위임
+        command.queue(req);
         return BaseResponse.onSuccess(SuccessStatus._OK, "Notification delivery succeeded.");
     }
 
+    @Operation(summary = "알림 유형별 ON/OFF 토글 API")
+    @PutMapping("/{memberId}/{type}/toggle")
+    public BaseResponse toggle(@PathVariable Long memberId,
+                                       @PathVariable NotificationType type) {
+        boolean newValue = command.toggle(memberId, type);
+        return BaseResponse.onSuccess(SuccessStatus._OK,
+                "Notification setting toggled: now enabled=" + newValue);
+    }
+
 }
diff --git a/src/main/java/com/assu/server/domain/notification/dto/NotificationSettingRequestDTO.java b/src/main/java/com/assu/server/domain/notification/dto/NotificationSettingRequestDTO.java
new file mode 100644
index 0000000..3ba6c88
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/dto/NotificationSettingRequestDTO.java
@@ -0,0 +1,12 @@
+package com.assu.server.domain.notification.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class NotificationSettingRequestDTO {
+    @NotNull
+    private Boolean enabled;
+}
diff --git a/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java b/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
new file mode 100644
index 0000000..590e21c
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
@@ -0,0 +1,32 @@
+package com.assu.server.domain.notification.entity;
+
+import com.assu.server.domain.common.entity.Member;
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Builder
+@Table(name = "notification_setting",
+        uniqueConstraints = @UniqueConstraint(columnNames = {"member_id", "type"}))
+public class NotificationSetting {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @ManyToOne(fetch = FetchType.LAZY, optional = false)
+    @JoinColumn(name = "member_id")
+    private Member member;
+
+    // CHAT, PARTNER_SUGGESTION, PARTNER_PROPOSAL ...
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false, length = 50)
+    private com.assu.server.domain.notification.entity.NotificationType type;
+
+    @Column(nullable = false)
+    private Boolean enabled;
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java
index d44bbae..1e4933b 100644
--- a/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java
+++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java
@@ -2,6 +2,8 @@
 
 import com.assu.server.domain.deviceToken.entity.DeviceToken;
 import com.assu.server.domain.notification.entity.Notification;
+import com.assu.server.domain.notification.entity.NotificationSetting;
+import com.assu.server.domain.notification.entity.NotificationType;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
@@ -10,12 +12,10 @@
 import org.springframework.data.repository.query.Param;
 
 import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
 
 public interface NotificationRepository extends JpaRepository {
     Page findByReceiverId(Long receiverId, Pageable pageable);
     Page findByReceiverIdAndIsReadFalse(Long receiverId, Pageable pageable);
-
-    @Modifying
-    @Query("update Notification n set n.isRead=true, n.readAt=:now where n.receiver.id=:memberId and n.isRead=false")
-    void markAllRead(@Param("memberId") Long memberId, @Param("now") LocalDateTime now);
 }
diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java
new file mode 100644
index 0000000..155d789
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java
@@ -0,0 +1,12 @@
+package com.assu.server.domain.notification.repository;
+
+import com.assu.server.domain.notification.entity.NotificationSetting;
+import com.assu.server.domain.notification.entity.NotificationType;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface NotificationSettingRepository extends JpaRepository {
+    Optional findByMemberIdAndType(Long memberId, NotificationType type);
+    boolean existsByMemberIdAndTypeAndEnabledTrue(Long memberId, NotificationType type);
+}
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
index dbb3b48..936cbb1 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
@@ -11,5 +11,6 @@ public interface NotificationCommandService {
     Notification createAndQueue(Long receiverId, NotificationType type, Long refId, Map ctx);
     void markRead(Long notificationId, Long currentMemberId) throws AccessDeniedException;
     void queue(QueueNotificationRequest req);
-
+    boolean toggle(Long memberId, NotificationType type);
+    boolean isEnabled(Long memberId, NotificationType type);
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index 196ad7f..ad7ca9c 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -5,9 +5,11 @@
 import com.assu.server.domain.notification.dto.QueueNotificationRequest;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.entity.NotificationOutbox;
+import com.assu.server.domain.notification.entity.NotificationSetting;
 import com.assu.server.domain.notification.entity.NotificationType;
 import com.assu.server.domain.notification.repository.NotificationOutboxRepository;
 import com.assu.server.domain.notification.repository.NotificationRepository;
+import com.assu.server.domain.notification.repository.NotificationSettingRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.exception.DatabaseException;
 import com.assu.server.infra.NotificationFactory;
@@ -26,6 +28,7 @@
 public class NotificationCommandServiceImpl implements NotificationCommandService {
     private final NotificationRepository notificationRepository;
     private final NotificationOutboxRepository outboxRepository;
+    private final NotificationSettingRepository notificationSettingRepository;
     private final NotificationFactory notificationFactory;
     private final MemberRepository memberRepository;
 
@@ -123,6 +126,42 @@ public void queue(QueueNotificationRequest req) {
             throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
         }
 
+        // OFF면 Outbox 적재 없이 Notification만 저장하고 종료
+        boolean enabled = isEnabled(req.getReceiverId(), type);
+
+        if (!enabled) {
+            // 기록만 남기고 발송은 스킵
+            var member = memberRepository.findMemberById(req.getReceiverId());
+            var notification = notificationFactory.create(member, type, refId, ctx);
+            notificationRepository.save(notification);
+            return;
+        }
+
         createAndQueue(req.getReceiverId(), type, refId, ctx);
     }
+
+    @Transactional
+    @Override
+    public boolean toggle(Long memberId, NotificationType type) {
+        NotificationSetting setting = notificationSettingRepository
+                .findByMemberIdAndType(memberId, type)
+                .orElse(NotificationSetting.builder()
+                        .member(memberRepository.findMemberById(memberId))
+                        .type(type)
+                        .enabled(true) // 기본값
+                        .build());
+
+        setting.setEnabled(!setting.getEnabled()); // 토글
+        notificationSettingRepository.save(setting);
+
+        return setting.getEnabled(); // 변경된 값 반환
+    }
+
+    @Transactional
+    @Override
+    public boolean isEnabled(Long memberId, NotificationType type) {
+        return notificationSettingRepository.findByMemberIdAndType(memberId, type)
+                .map(ns -> Boolean.TRUE.equals(ns.getEnabled())) // null → false 처리
+                .orElse(true); // 설정 없으면 기본 허용
+    }
 }

From d308e7d6763783e364afc6f53bdc916816e7b7c6 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Sun, 17 Aug 2025 17:57:55 +0900
Subject: [PATCH 068/270] =?UTF-8?q?refactor/#35-=EB=A9=A4=EB=B2=84=20?=
 =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=84=EB=8F=84=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=20-=20=EB=A9=A4=EB=B2=84=20=ED=8C=A8=ED=82=A4?=
 =?UTF-8?q?=EC=A7=80=20=EB=B3=84=EB=8F=84=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/assu/server/domain/admin/entity/Admin.java   | 2 +-
 .../com/assu/server/domain/auth/entity/CommonAuth.java    | 1 +
 .../java/com/assu/server/domain/auth/entity/SSUAuth.java  | 1 +
 .../com/assu/server/domain/auth/security/JwtUtil.java     | 4 ++--
 .../auth/security/common/CommonUserDetailsService.java    | 2 +-
 .../auth/security/student/StudentUserDetailsService.java  | 2 +-
 .../assu/server/domain/auth/service/LoginServiceImpl.java | 8 ++------
 .../server/domain/auth/service/LogoutServiceImpl.java     | 2 +-
 .../server/domain/auth/service/SignUpServiceImpl.java     | 4 ++--
 .../assu/server/domain/chat/converter/ChatConverter.java  | 2 +-
 .../java/com/assu/server/domain/chat/entity/Message.java  | 2 +-
 .../assu/server/domain/chat/service/ChatServiceImpl.java  | 4 ++--
 .../server/domain/member/controller/MemberController.java | 4 ++++
 .../server/domain/member/converter/MemberConverter.java   | 4 ++++
 .../assu/server/domain/member/dto/MemberRequestDTO.java   | 4 ++++
 .../assu/server/domain/member/dto/MemberResponseDTO.java  | 4 ++++
 .../server/domain/{auth => member}/entity/Member.java     | 4 +++-
 .../{auth => member}/repository/MemberRepository.java     | 4 ++--
 .../assu/server/domain/member/service/MemberService.java  | 4 ++++
 .../server/domain/member/service/MemberServiceImpl.java   | 4 ++++
 .../com/assu/server/domain/partner/entity/Partner.java    | 2 +-
 .../server/domain/term/entity/mapping/TermAgreement.java  | 2 +-
 .../java/com/assu/server/domain/user/entity/Student.java  | 2 +-
 23 files changed, 48 insertions(+), 24 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/member/controller/MemberController.java
 create mode 100644 src/main/java/com/assu/server/domain/member/converter/MemberConverter.java
 create mode 100644 src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java
 rename src/main/java/com/assu/server/domain/{auth => member}/entity/Member.java (93%)
 rename src/main/java/com/assu/server/domain/{auth => member}/repository/MemberRepository.java (70%)
 create mode 100644 src/main/java/com/assu/server/domain/member/service/MemberService.java
 create mode 100644 src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java

diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
index 085b9fc..613554d 100644
--- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java
+++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.admin.entity;
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.Entity;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.MapsId;
diff --git a/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java b/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java
index 00d6dbb..561f7d2 100644
--- a/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java
+++ b/src/main/java/com/assu/server/domain/auth/entity/CommonAuth.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.auth.entity;
 
 import com.assu.server.domain.common.entity.BaseEntity;
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
index 4c820bd..fddee5f 100644
--- a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
+++ b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.auth.entity;
 
 import com.assu.server.domain.common.entity.BaseEntity;
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
index 5837aae..9d6fdec 100644
--- a/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
+++ b/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
@@ -1,9 +1,9 @@
 package com.assu.server.domain.auth.security;
 
 import com.assu.server.domain.auth.dto.signup.Tokens;
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.common.enums.UserRole;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import io.jsonwebtoken.Claims;
diff --git a/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java b/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
index 8d50b19..e8ac262 100644
--- a/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
+++ b/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.auth.security.common;
 
 import com.assu.server.domain.auth.entity.CommonAuth;
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.repository.CommonAuthRepository;
 import com.assu.server.domain.common.enums.ActivationStatus;
diff --git a/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java b/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
index 7b999d8..d1713c0 100644
--- a/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
+++ b/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.auth.security.student;
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.auth.entity.SSUAuth;
 import com.assu.server.domain.auth.repository.SSUAuthRepository;
 import com.assu.server.domain.common.enums.ActivationStatus;
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
index fa2df45..6f92b4d 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -6,11 +6,11 @@
 import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
 import com.assu.server.domain.auth.dto.signup.Tokens;
 import com.assu.server.domain.auth.entity.CommonAuth;
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.auth.entity.SSUAuth;
 import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.repository.CommonAuthRepository;
-import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.auth.repository.SSUAuthRepository;
 import com.assu.server.domain.auth.security.JwtUtil;
 import com.assu.server.domain.auth.security.SecurityUtil;
@@ -18,14 +18,10 @@
 import com.assu.server.domain.auth.security.student.StudentUsernamePasswordAuthenticationToken;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.stereotype.Service;
 
-import java.util.Collections;
-import java.util.Map;
-
 @Service
 @RequiredArgsConstructor
 public class LoginServiceImpl implements LoginService {
diff --git a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
index 533c817..b8c5756 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.auth.service;
 
-import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.auth.security.JwtUtil;
 import com.assu.server.domain.auth.security.SecurityUtil;
 import lombok.RequiredArgsConstructor;
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 bf829b7..9681b1a 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
@@ -14,8 +14,8 @@
 import com.assu.server.domain.auth.security.JwtUtil;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.common.enums.UserRole;
-import com.assu.server.domain.auth.entity.Member;
-import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.domain.user.entity.Student;
diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
index 803a077..a84621f 100644
--- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
+++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.chat.converter;
 
 import com.assu.server.domain.admin.entity.Admin;
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.chat.dto.ChatMessageDTO;
 import com.assu.server.domain.chat.dto.ChatRequestDTO;
 import com.assu.server.domain.chat.dto.ChatResponseDTO;
diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java
index 6998121..e668060 100644
--- a/src/main/java/com/assu/server/domain/chat/entity/Message.java
+++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.chat.entity;
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.chat.entity.enums.MessageType;
 
 import com.assu.server.domain.common.entity.BaseEntity;
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index df68e6b..8b4e1e3 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -11,9 +11,9 @@
 import com.assu.server.domain.chat.entity.Message;
 import com.assu.server.domain.chat.repository.ChatRepository;
 import com.assu.server.domain.chat.repository.MessageRepository;
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.common.enums.ActivationStatus;
-import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
diff --git a/src/main/java/com/assu/server/domain/member/controller/MemberController.java b/src/main/java/com/assu/server/domain/member/controller/MemberController.java
new file mode 100644
index 0000000..0c40203
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/member/controller/MemberController.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.member.controller;
+
+public class MemberController {
+}
diff --git a/src/main/java/com/assu/server/domain/member/converter/MemberConverter.java b/src/main/java/com/assu/server/domain/member/converter/MemberConverter.java
new file mode 100644
index 0000000..5f8e135
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/member/converter/MemberConverter.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.member.converter;
+
+public class MemberConverter {
+}
diff --git a/src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java b/src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java
new file mode 100644
index 0000000..b407129
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.member.dto;
+
+public class MemberRequestDTO {
+}
diff --git a/src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java b/src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java
new file mode 100644
index 0000000..ea43988
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.member.dto;
+
+public class MemberResponseDTO {
+}
diff --git a/src/main/java/com/assu/server/domain/auth/entity/Member.java b/src/main/java/com/assu/server/domain/member/entity/Member.java
similarity index 93%
rename from src/main/java/com/assu/server/domain/auth/entity/Member.java
rename to src/main/java/com/assu/server/domain/member/entity/Member.java
index 375a1a8..5145a5a 100644
--- a/src/main/java/com/assu/server/domain/auth/entity/Member.java
+++ b/src/main/java/com/assu/server/domain/member/entity/Member.java
@@ -1,6 +1,8 @@
-package com.assu.server.domain.auth.entity;
+package com.assu.server.domain.member.entity;
 
 import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.auth.entity.CommonAuth;
+import com.assu.server.domain.auth.entity.SSUAuth;
 import com.assu.server.domain.common.entity.BaseEntity;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.common.enums.UserRole;
diff --git a/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
similarity index 70%
rename from src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
rename to src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
index fa2352a..bf34a66 100644
--- a/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
+++ b/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
@@ -1,6 +1,6 @@
-package com.assu.server.domain.auth.repository;
+package com.assu.server.domain.member.repository;
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
diff --git a/src/main/java/com/assu/server/domain/member/service/MemberService.java b/src/main/java/com/assu/server/domain/member/service/MemberService.java
new file mode 100644
index 0000000..56fa069
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/member/service/MemberService.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.member.service;
+
+public interface MemberService {
+}
diff --git a/src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java b/src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java
new file mode 100644
index 0000000..8b54110
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java
@@ -0,0 +1,4 @@
+package com.assu.server.domain.member.service;
+
+public class MemberServiceImpl {
+}
diff --git a/src/main/java/com/assu/server/domain/partner/entity/Partner.java b/src/main/java/com/assu/server/domain/partner/entity/Partner.java
index 0f5994e..9fc67ca 100644
--- a/src/main/java/com/assu/server/domain/partner/entity/Partner.java
+++ b/src/main/java/com/assu/server/domain/partner/entity/Partner.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.partner.entity;
 
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.Entity;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.MapsId;
diff --git a/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java b/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java
index 32b9e05..11259e9 100644
--- a/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java
+++ b/src/main/java/com/assu/server/domain/term/entity/mapping/TermAgreement.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.term.entity.mapping;
 import com.assu.server.domain.common.entity.BaseEntity;
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.term.entity.Term;
 
 import jakarta.persistence.Entity;
diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java
index 0da6b4b..29771f2 100644
--- a/src/main/java/com/assu/server/domain/user/entity/Student.java
+++ b/src/main/java/com/assu/server/domain/user/entity/Student.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.user.entity;
 
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.user.entity.enums.Department;
 import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
 import com.assu.server.domain.user.entity.enums.Major;

From 59fb43a368711e28be213ba5b63176ef48066533 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 17 Aug 2025 22:25:08 +1000
Subject: [PATCH 069/270] =?UTF-8?q?[MERGE/#20]=20dev=20PULL=20=EB=B0=9B?=
 =?UTF-8?q?=EC=95=84=EC=98=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/auth/repository/MemberRepository.java       |  2 ++
 .../domain/common/repository/MemberRepository.java     | 10 ----------
 .../deviceToken/controller/DeviceTokenController.java  |  1 -
 .../server/domain/deviceToken/entity/DeviceToken.java  |  2 +-
 .../deviceToken/repository/DeviceTokenRepository.java  |  2 +-
 .../domain/deviceToken/service/DeviceTokenService.java |  2 --
 .../deviceToken/service/DeviceTokenServiceImpl.java    |  6 +++---
 .../domain/notification/entity/Notification.java       |  5 +++--
 .../notification/entity/NotificationSetting.java       |  2 +-
 .../service/NotificationCommandServiceImpl.java        |  8 +++-----
 .../notification/service/NotificationDispatcher.java   |  4 ++--
 .../service/NotificationQueryServiceImpl.java          |  4 ++--
 .../global/apiPayload/code/status/ErrorStatus.java     |  2 +-
 src/main/java/com/assu/server/infra/FcmClient.java     |  2 +-
 .../com/assu/server/infra/NotificationFactory.java     |  2 +-
 15 files changed, 21 insertions(+), 33 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/common/repository/MemberRepository.java

diff --git a/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
index fa2352a..799edf8 100644
--- a/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
+++ b/src/main/java/com/assu/server/domain/auth/repository/MemberRepository.java
@@ -7,4 +7,6 @@
 @Repository
 public interface MemberRepository extends JpaRepository {
     boolean existsByPhoneNum(String phoneNum);
+    Member findMemberById(Long id);
+
 }
diff --git a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
deleted file mode 100644
index 2775b46..0000000
--- a/src/main/java/com/assu/server/domain/common/repository/MemberRepository.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.assu.server.domain.common.repository;
-
-import com.assu.server.domain.common.entity.Member;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-
-
-public interface MemberRepository extends JpaRepository {
-    Member findMemberById(Long id);
-}
diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index a715eba..eff147b 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -1,6 +1,5 @@
 package com.assu.server.domain.deviceToken.controller;
 
-import com.assu.server.domain.common.entity.Member;
 import com.assu.server.domain.deviceToken.dto.DeviceTokenRequest;
 import com.assu.server.domain.deviceToken.service.DeviceTokenService;
 import com.assu.server.global.apiPayload.BaseResponse;
diff --git a/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java
index 03994a8..c908eaf 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.deviceToken.entity;
 
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.common.entity.BaseEntity;
-import com.assu.server.domain.common.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java
index 32365b9..6720d39 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java
@@ -9,7 +9,7 @@
 import java.util.Optional;
 
 public interface DeviceTokenRepository extends JpaRepository {
-    @Query("select dt.token from DeviceToken dt where dt.member.id=:memberId and dt.active=true")
+    @Query("select dt.token from DeviceToken dt where dt.member.id =:memberId and dt.active=true")
     List findActiveTokensByMemberId(@Param("memberId") Long memberId);
 
     Optional findByToken(String token);
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
index 0e03b3d..254402c 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
@@ -1,7 +1,5 @@
 package com.assu.server.domain.deviceToken.service;
 
-import com.assu.server.domain.common.entity.Member;
-
 public interface DeviceTokenService {
     void register(String tokenId, Long memberId);
     void unregister(Long tokenId);
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
index 1d5925e..2defd5c 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
@@ -1,11 +1,11 @@
 package com.assu.server.domain.deviceToken.service;
 
-import com.assu.server.domain.common.entity.Member;
-import com.assu.server.domain.common.repository.MemberRepository;
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.auth.repository.MemberRepository;
 import com.assu.server.domain.deviceToken.entity.DeviceToken;
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/assu/server/domain/notification/entity/Notification.java b/src/main/java/com/assu/server/domain/notification/entity/Notification.java
index 5ab52a5..de83def 100644
--- a/src/main/java/com/assu/server/domain/notification/entity/Notification.java
+++ b/src/main/java/com/assu/server/domain/notification/entity/Notification.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.notification.entity;
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.common.entity.BaseEntity;
 
-import com.assu.server.domain.common.entity.Member;
 import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -18,7 +18,8 @@
 @AllArgsConstructor
 public class Notification extends BaseEntity {
 
-	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
 	private Long id;
 
 	@ManyToOne(fetch = FetchType.LAZY)
diff --git a/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java b/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
index 590e21c..00d90b4 100644
--- a/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
+++ b/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.notification.entity;
 
-import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.auth.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index ad7ca9c..bcb023c 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.notification.service;
 
-import com.assu.server.domain.common.entity.Member;
-import com.assu.server.domain.common.repository.MemberRepository;
+import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.auth.repository.MemberRepository;
 import com.assu.server.domain.notification.dto.QueueNotificationRequest;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.entity.NotificationOutbox;
@@ -11,17 +11,15 @@
 import com.assu.server.domain.notification.repository.NotificationRepository;
 import com.assu.server.domain.notification.repository.NotificationSettingRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import com.assu.server.infra.NotificationFactory;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
-import java.nio.file.AccessDeniedException;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Objects;
 
 @Service
 @RequiredArgsConstructor
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java b/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
index 6157c0e..56b9d85 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
@@ -25,8 +25,8 @@ public void dispatch() {
 
         for (NotificationOutbox o : batch) {
             try {
-                Notification n = o.getNotification();
-                fcmClient.sendToMember(n.getReceiver(), n);
+                Notification notification = o.getNotification();
+                fcmClient.sendToMember(notification.getReceiver(), notification);
                 o.markSent();
             } catch (Exception e) {
                 o.incRetry();
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
index 99dc5df..63ee801 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
@@ -1,12 +1,12 @@
 package com.assu.server.domain.notification.service;
 
-import com.assu.server.domain.common.repository.MemberRepository;
+import com.assu.server.domain.auth.repository.MemberRepository;
 import com.assu.server.domain.notification.converter.NotificationConverter;
 import com.assu.server.domain.notification.dto.NotificationResponseDTO;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.repository.NotificationRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 4c98b94..898d625 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -45,7 +45,7 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."),
     NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."),
     NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."),
-    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다.")
+    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
 
     // 알림(Notification) 에러
diff --git a/src/main/java/com/assu/server/infra/FcmClient.java b/src/main/java/com/assu/server/infra/FcmClient.java
index 799e723..9e2a68e 100644
--- a/src/main/java/com/assu/server/infra/FcmClient.java
+++ b/src/main/java/com/assu/server/infra/FcmClient.java
@@ -1,6 +1,6 @@
 package com.assu.server.infra;
 
-import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
 import com.assu.server.domain.notification.entity.Notification;
 import com.google.firebase.messaging.FirebaseMessaging;
diff --git a/src/main/java/com/assu/server/infra/NotificationFactory.java b/src/main/java/com/assu/server/infra/NotificationFactory.java
index 0632e9a..a7fba66 100644
--- a/src/main/java/com/assu/server/infra/NotificationFactory.java
+++ b/src/main/java/com/assu/server/infra/NotificationFactory.java
@@ -1,8 +1,8 @@
 package com.assu.server.infra;
 
+import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.entity.NotificationType;
-import com.assu.server.domain.common.entity.Member;
 import org.springframework.stereotype.Component;
 
 import java.util.Map;

From 2211321c72c7a00f50be9894e984f11d180b5c72 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:05:22 +0900
Subject: [PATCH 070/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 응답 예시 삭제
---
 .../auth/controller/AuthController.java       | 97 +++----------------
 1 file changed, 12 insertions(+), 85 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 21354f2..a5ebc5d 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,6 +1,6 @@
 package com.assu.server.domain.auth.controller;
 
-import com.assu.server.domain.auth.dto.login.LoginRequest;
+import com.assu.server.domain.auth.dto.login.CommonLoginRequest;
 import com.assu.server.domain.auth.dto.login.LoginResponse;
 import com.assu.server.domain.auth.dto.login.RefreshResponse;
 import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
@@ -13,7 +13,6 @@
 import com.assu.server.domain.auth.service.LogoutService;
 import com.assu.server.domain.auth.service.PhoneAuthService;
 import com.assu.server.domain.auth.service.SignUpService;
-import com.assu.server.domain.common.enums.UserRole;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import io.swagger.v3.oas.annotations.Operation;
@@ -22,16 +21,12 @@
 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 io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.multipart.MultipartRequest;
 
 @Tag(name = "Auth", description = "인증/회원가입 API")
 @RestController
@@ -50,12 +45,6 @@ public class AuthController {
                     "- 입력한 휴대폰 번호로 1회용 인증번호(OTP)를 발송합니다.\n" +
                     "- 유효시간/재요청 제한 정책은 서버 설정에 따릅니다."
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "인증번호 발송 성공",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "400", description = "잘못된 요청 (형식/필수값 오류)",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @PostMapping("/phone-numbers/send")
     public BaseResponse sendAuthNumber(
             @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthSendRequest request
@@ -70,12 +59,6 @@ public BaseResponse sendAuthNumber(
                     "- 발송된 인증번호(OTP)를 검증합니다.\n" +
                     "- 성공 시 서버에 휴대폰 인증 상태가 기록됩니다."
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "인증번호 검증 성공",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "400", description = "인증 실패/만료/형식 오류",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @PostMapping("/phone-numbers/verify")
     public BaseResponse checkAuthNumber(
             @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthVerifyRequest request
@@ -94,14 +77,6 @@ public BaseResponse checkAuthNumber(
                     "- 처리: users + ssu_auth 등 가입 레코드 생성, 휴대폰 인증 여부 확인.\n" +
                     "- 성공 시 201(Created)과 생성된 memberId 반환."
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "201", description = "회원가입 성공",
-                    content = @Content(schema = @Schema(implementation = SignUpResponse.class))),
-            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "409", description = "중복(전화번호 등)으로 인한 충돌",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @PostMapping(value = "/signup/student", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse signupStudent(
             @Valid @RequestBody StudentSignUpRequest request) {
@@ -116,14 +91,6 @@ public BaseResponse signupStudent(
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
                     "- 성공 시 201(Created)과 생성된 memberId 반환."
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "201", description = "회원가입 성공",
-                    content = @Content(schema = @Schema(implementation = SignUpResponse.class))),
-            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류(비밀번호 불일치 등)",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "409", description = "중복(전화/이메일)으로 인한 충돌",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @PostMapping(value = "/signup/partner", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse signupPartner(
             @Valid @RequestPart("request")
@@ -155,14 +122,6 @@ public BaseResponse signupPartner(
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
                     "- 성공 시 201(Created)과 생성된 memberId 반환."
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "201", description = "회원가입 성공",
-                    content = @Content(schema = @Schema(implementation = SignUpResponse.class))),
-            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류(비밀번호 불일치 등)",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "409", description = "중복(전화/이메일)으로 인한 충돌",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @PostMapping(value = "/signup/admin", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse signupAdmin(
             @Valid @RequestPart("request")
@@ -187,7 +146,7 @@ public BaseResponse signupAdmin(
 
     // 로그인 (파트너/관리자 공통)
     @Operation(
-            summary = "로그인 API",
+            summary = "공통 로그인 API",
             description = "# v1.0 (2025-08-15)\n" +
                     "- `application/json`로 호출합니다.\n" +
                     "- 바디: `LoginRequest(email, password)`.\n" +
@@ -196,30 +155,22 @@ public BaseResponse signupAdmin(
     )
     @io.swagger.v3.oas.annotations.parameters.RequestBody(
             required = true,
-            content = @Content(schema = @Schema(implementation = LoginRequest.class))
+            content = @Content(schema = @Schema(implementation = CommonLoginRequest.class))
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "로그인 성공",
-                    content = @Content(schema = @Schema(implementation = LoginResponse.class))),
-            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "401", description = "인증 실패(자격 증명 오류)",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
-    @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE)
-    public BaseResponse login(
-            @RequestBody @Valid LoginRequest request
+    @PostMapping(value = "/login/common", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public BaseResponse loginCommon(
+            @RequestBody @Valid CommonLoginRequest request
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, loginService.login(request));
+        return BaseResponse.onSuccess(SuccessStatus._OK, loginService.loginCommon(request));
     }
 
 
     // 학생 로그인
     @Operation(
             summary = "학생 로그인 API",
-            description = "# v1.0 (2025-08-15)\n" +
+            description = "# v1.1 (2025-08-18)\n" +
                     "- `application/json`로 호출합니다.\n" +
-                    "- 바디: `StudentLoginRequest(email, password)`.\n" +
+                    "- 바디: `바디: `StudentLoginRequest(studentNumber, studentPassword, school)`.\n" +
                     "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
                     "- 성공 시 200(OK)과 토큰/만료시각 반환."
     )
@@ -227,16 +178,8 @@ public BaseResponse login(
             required = true,
             content = @Content(schema = @Schema(implementation = StudentLoginRequest.class))
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "로그인 성공",
-                    content = @Content(schema = @Schema(implementation = LoginResponse.class))),
-            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "401", description = "인증 실패(자격 증명 오류)",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @PostMapping(value = "/login/student", consumes = MediaType.APPLICATION_JSON_VALUE)
-    public BaseResponse login(
+    public BaseResponse loginStudent(
             @RequestBody @Valid StudentLoginRequest request
     ) {
         return BaseResponse.onSuccess(SuccessStatus._OK, loginService.loginStudent(request));
@@ -258,16 +201,6 @@ public BaseResponse login(
             @Parameter(name = "RefreshToken", description = "Refresh Token", required = true,
                     in = ParameterIn.HEADER, schema = @Schema(type = "string"))
     })
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "토큰 재발급 성공",
-                    content = @Content(schema = @Schema(implementation = RefreshResponse.class))),
-            @ApiResponse(responseCode = "400", description = "검증 실패/형식 오류",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "401", description = "인증 실패(토큰 오류/만료)",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "403", description = "접근 거부",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @PostMapping("/refresh")
     public BaseResponse refreshToken(
             @RequestHeader("RefreshToken") String refreshToken
@@ -285,20 +218,14 @@ public BaseResponse refreshToken(
                     "- 처리: Refresh 무효화(선택), Access 블랙리스트 등록.\n" +
                     "- 성공 시 200(OK)."
     )
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "로그아웃 성공",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class))),
-            @ApiResponse(responseCode = "401", description = "인증 실패(토큰 오류/만료)",
-                    content = @Content(schema = @Schema(implementation = BaseResponse.class)))
-    })
     @DeleteMapping("/logout")
     public BaseResponse logout(
             @RequestHeader("Authorization")
             @Parameter(name = "Authorization", description = "Access Token. 형식: `Bearer `", required = true,
                             in = ParameterIn.HEADER, schema = @Schema(type = "string"))
-            String accessToken
+            String authorization
     ) {
-        logoutService.logout(accessToken);
+        logoutService.logout(authorization);
         return BaseResponse.onSuccess(SuccessStatus._OK, null);
     }
 

From ab002556ca6a18dd2778d42c5d5f38e855a1848a Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:05:58 +0900
Subject: [PATCH 071/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- SecurityConfig 인증 경로 구분
---
 .../server/global/config/SecurityConfig.java  | 39 +++++++++----------
 1 file changed, 19 insertions(+), 20 deletions(-)

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 c9affe7..bc449a4 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -1,21 +1,14 @@
 package com.assu.server.global.config;
 
-import com.assu.server.domain.auth.security.JwtAuthFilter;
+import com.assu.server.domain.auth.security.jwt.JwtAuthFilter;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.ProviderManager;
-import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 
-import java.util.List;
-
 @Configuration
 public class SecurityConfig {
 
@@ -27,20 +20,26 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                 .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                 .authorizeHttpRequests(auth -> auth
                         .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+
+                        // Swagger 등 공개 리소스
                         .requestMatchers(
-                                "/chat/**",
-                                "/suggestion/**",
-                                "/review/**",
-                                "/ws/**",
-                                "/pub/**",
-                                "/sub/**",
-                                "/v3/api-docs/**",
-                                "/swagger-ui/**",
-                                "/swagger-ui.html",
-                                "/swagger-resources/**",
-                                "/webjars/**",
-                                "/auth/**"
+                                "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
+                                "/swagger-resources/**", "/webjars/**"
                         ).permitAll()
+
+                        // 로그인/회원가입/재발급만 공개
+                        .requestMatchers(
+                                "/auth/login/common",
+                                "/auth/login/student",
+                                "/auth/signup/**",
+                                "/auth/refresh",
+                                "/auth/phone-numbers/**"
+                        ).permitAll()
+
+                        // 로그아웃은 인증 필요
+                        .requestMatchers("/auth/logout").authenticated()
+
+                        // 나머지는 인증 필요
                         .anyRequest().authenticated()
                 )
                 .formLogin(login -> login.disable())

From ce91e8ccfa5caf7f60dbdab871fd8d35397290c8 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:06:26 +0900
Subject: [PATCH 072/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 사용하지 않는 클래스 삭제
---
 .../domain/auth/security/JwtAuthFilter.java   | 118 -----------
 .../server/domain/auth/security/JwtUtil.java  | 183 ------------------
 .../domain/auth/security/SecurityUtil.java    |  23 ---
 .../common/CommonUserDetailsService.java      |  48 -----
 .../student/StudentUserDetailsService.java    |  38 ----
 ...ntUsernamePasswordAuthenticationToken.java |  10 -
 6 files changed, 420 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java

diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
deleted file mode 100644
index f2a54e5..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package com.assu.server.domain.auth.security;
-
-import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import io.jsonwebtoken.Claims;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.security.authentication.InsufficientAuthenticationException;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.stereotype.Component;
-import org.springframework.util.AntPathMatcher;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import java.io.IOException;
-
-@RequiredArgsConstructor
-@Component
-@Slf4j
-public class JwtAuthFilter extends OncePerRequestFilter {
-
-    @Value("${jwt.header}")
-    private String jwtHeader;
-    private final JwtUtil jwtUtil;
-    private final RedisTemplate redisTemplate;
-
-    private static final AntPathMatcher PATH = new AntPathMatcher();
-    private static final String[] WHITELIST = {
-            "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
-            "/swagger-resources/**", "/webjars/**",
-            "/auth/**",           // ← 로그인/회원가입/인증 등은 토큰 없이 접근
-            "/chat/**", "/suggestion/**", "/review/**",
-            "/ws/**", "/pub/**", "/sub/**"
-    };
-
-    @Override
-    protected boolean shouldNotFilter(HttpServletRequest request) {
-        String uri = request.getRequestURI();
-        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; // CORS preflight 우회
-        if (PATH.match("/auth/refresh", uri)) return false;               // 토큰 재발급은 필터 적용
-        for (String p : WHITELIST) if (PATH.match(p, uri)) return true;   // 나머지 공개 경로 우회
-        return false;                                                     // 보호 자원은 필터 적용
-    }
-
-    private static void checkAuthorizationHeader(String header) {
-        log.info("-------------------#@@@@@------------------");
-        if(header == null) {
-            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
-        } else if (!header.startsWith("Bearer ")) {
-            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
-        }
-    }
-
-    @Override
-    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
-            throws ServletException, IOException {
-
-        final String authHeader = request.getHeader(jwtHeader);
-
-        // Refresh 전용 처리
-        if (PATH.match("/auth/refresh", request.getRequestURI())) {
-            final String refreshToken = request.getHeader("refreshToken");
-            try {
-                // 둘 다 필수
-                checkAuthorizationHeader(authHeader);
-                if (refreshToken == null) throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
-
-                String accessToken = JwtUtil.getTokenFromHeader(authHeader);
-                Claims claims = jwtUtil.validateTokenOnlySignature(accessToken); // 서명만 검증(만료 허용)
-                Authentication authentication = jwtUtil.getAuthenticationFromExpiredAccessToken(accessToken);
-                SecurityContextHolder.getContext().setAuthentication(authentication);
-
-                jwtUtil.validateRefreshToken(refreshToken); // RT는 만료 허용 X
-                chain.doFilter(request, response);
-                return;
-            } catch (Exception e) {
-                // EntryPoint로 넘겨 통일 처리
-                if (e instanceof CustomAuthException ce) {
-                    request.setAttribute("exceptionCode", ce.getCode());
-                    request.setAttribute("exceptionMessage", ce.getMessage());
-                    request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
-                }
-                throw new InsufficientAuthenticationException(e.getMessage(), e);
-            }
-        }
-
-        // 그 외(보호 자원): Authorization 헤더가 없으면 그냥 통과
-        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
-            chain.doFilter(request, response);
-            return;
-        }
-
-        try {
-            String accessToken = JwtUtil.getTokenFromHeader(authHeader);
-            jwtUtil.validateToken(accessToken);
-            jwtUtil.isTokenBlacklisted(accessToken); // accessToken 전달
-
-            Authentication authentication = jwtUtil.getAuthentication(accessToken);
-            SecurityContextHolder.getContext().setAuthentication(authentication);
-
-            chain.doFilter(request, response);
-        } catch (Exception e) {
-            if (e instanceof CustomAuthException ce) {
-                request.setAttribute("exceptionCode", ce.getCode());
-                request.setAttribute("exceptionMessage", ce.getMessage());
-                request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
-            }
-            throw new InsufficientAuthenticationException(e.getMessage(), e);
-        }
-    }
-}
-
diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
deleted file mode 100644
index 9d6fdec..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/JwtUtil.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package com.assu.server.domain.auth.security;
-
-import com.assu.server.domain.auth.dto.signup.Tokens;
-import com.assu.server.domain.member.entity.Member;
-import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.domain.member.repository.MemberRepository;
-import com.assu.server.domain.common.enums.UserRole;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.ExpiredJwtException;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.security.Keys;
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.stereotype.Component;
-
-import javax.crypto.SecretKey;
-import java.nio.charset.StandardCharsets;
-import java.time.ZonedDateTime;
-import java.util.*;
-
-@Component
-@RequiredArgsConstructor
-public class JwtUtil {
-
-    @Value("${jwt.secret}")
-    public String secretKey;
-
-    @Value("${jwt.access-valid-seconds:3600}")      // 1시간 기본
-    private int accessValidSeconds;
-
-    @Value("${jwt.refresh-valid-seconds:1209600}")  // 14일 기본
-    private int refreshValidSeconds;
-
-    private final RedisTemplate redisTemplate;
-    private final MemberRepository memberRepository;
-
-    // 헤더에 "Bearer XXX" 형식으로 담겨온 토큰을 추출한다
-    public static String getTokenFromHeader(String header) {
-        return header.split(" ")[1];
-    }
-
-    public String generateToken(Map valueMap, int validTime) { // static 제거
-        SecretKey key = null;
-        try {
-            key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
-        } catch(Exception e){
-            throw new RuntimeException(e.getMessage());
-        }
-        return Jwts.builder()
-                .setHeader(Map.of("typ","JWT"))
-                .setClaims(valueMap)
-                .setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
-                .setExpiration(Date.from(ZonedDateTime.now().plusSeconds(validTime).toInstant()))
-                .signWith(key)
-                .compact();
-    }
-
-    public Tokens issueAndPersistTokens(Member member, UserRole role) {
-        // 공통 Claims
-        Map claims = new HashMap<>();
-        claims.put("userId", member.getId());
-        claims.put("role", role.name());
-
-        // access / refresh 발급
-        String access = generateToken(claims, accessValidSeconds);
-        String refresh = generateToken(claims, refreshValidSeconds);
-
-        // 멤버 엔티티에 저장 (unique=true 컬럼)
-        member.setAccessToken(access);
-        member.setRefreshToken(refresh);
-        memberRepository.save(member);
-
-        return Tokens.builder()
-                .accessToken(access)
-                .refreshToken(refresh)
-                .build();
-    }
-
-    public Authentication getAuthentication(String token) { // context에 넣을 Authentication를 jwt의 userId를 넣어 생성 // static 제거
-        Map claims = validateToken(token);
-        System.out.println("userId type: " + (claims.get("userId") != null ? claims.get("userId").getClass().getName() : "null"));
-
-        Number uid = (Number) claims.get("userId");
-        Long userId = uid.longValue();
-
-        return new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
-    }
-
-    public Map validateToken(String token) { // static 제거
-        Map claim = null;
-        try {
-            SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
-            claim = Jwts.parserBuilder()
-                    .setSigningKey(key)
-                    .build()
-                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
-                    .getBody();
-        } catch(ExpiredJwtException expiredJwtException){
-            throw new CustomAuthException(ErrorStatus.JWT_ACCESS_TOKEN_EXPIRED);
-        } catch(Exception e){
-            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
-        }
-        return claim;
-    }
-
-    public Authentication getAuthenticationFromExpiredAccessToken(String token) { // context에 넣을 Authentication를 jwt의 userId를 넣어 생성 // static 제거
-        Map claims = validateTokenOnlySignature(token);
-        System.out.println("userId type: " + (claims.get("userId") != null ? claims.get("userId").getClass().getName() : "null"));
-
-        Number uid = (Number) claims.get("userId");
-        Long userId = uid.longValue();
-
-        return new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
-    }
-
-    public Claims validateTokenOnlySignature(String token) { // static 제거
-        Claims claims = null;
-        try {
-            SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
-            claims = Jwts.parserBuilder()
-                    .setSigningKey(key)
-                    .build()
-                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
-                    .getBody();
-        } catch(ExpiredJwtException expiredJwtException){
-            return expiredJwtException.getClaims(); // ✅ 만료된 토큰에서도 Claims 추출
-        } catch(Exception e){
-            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
-        }
-        return claims;
-    }
-
-    public void validateRefreshToken(String token) { // static 제거
-        Map claim = null;
-        try {
-            SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
-            claim = Jwts.parserBuilder()
-                    .setSigningKey(key)
-                    .build()
-                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
-                    .getBody();
-        } catch(ExpiredJwtException expiredJwtException){
-            throw new CustomAuthException(ErrorStatus.JWT_REFRESH_TOKEN_EXPIRED);
-        } catch(Exception e){
-            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
-        }
-    }
-
-    // 토큰의 남은 만료시간 계산
-    public long tokenRemainTimeSecond(String header) { // static 제거
-        String accessToken = getTokenFromHeader(header);
-        SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
-        Claims claims = Jwts.parserBuilder()
-                .setSigningKey(key)
-                .build()
-                .parseClaimsJws(accessToken)
-                .getBody();
-
-        Date expDate = claims.getExpiration(); // 만료 시간 반환 (Date 타입)
-        long remainMs = expDate.getTime() - System.currentTimeMillis();
-        return remainMs / 1000;
-    }
-
-    // access token redis의 블랙리스트에서 확인
-    public void isTokenBlacklisted(String accessToken) {
-        Set keys = redisTemplate.keys("blackList:*"); // "blackList:*" 패턴의 모든 Key 검색
-        if (keys == null || keys.isEmpty()) {
-            return; // 블랙리스트가 비어있다면 return
-        }
-
-        // 모든 Key에 대해 해당 Token이 Value로 존재하는지 확인
-        for (String key : keys) {
-            String value = redisTemplate.opsForValue().get(key);
-            if (accessToken.equals(value)) {
-                throw new CustomAuthException(ErrorStatus.LOGOUT_USER);
-            }
-        }
-    }
-}
diff --git a/src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java b/src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java
deleted file mode 100644
index 7dc7d06..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/SecurityUtil.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.assu.server.domain.auth.security;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-
-@Slf4j
-public class SecurityUtil {
-
-    private SecurityUtil() { }
-
-    // SecurityContext 에 유저 정보가 저장되는 시점
-    // Request 가 들어올 때 JwtAuthenticationFilter 의 doFilterInternal 에서 저장
-    public static Long getCurrentUserId() {
-        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-
-        if (authentication == null || authentication.getName() == null) {
-            throw  new RuntimeException("Security Context 에 인증 정보가 없습니다.");
-        }
-
-        return Long.parseLong(authentication.getName());
-    }
-}
diff --git a/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java b/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
deleted file mode 100644
index e8ac262..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/common/CommonUserDetailsService.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.assu.server.domain.auth.security.common;
-
-import com.assu.server.domain.auth.entity.CommonAuth;
-import com.assu.server.domain.member.entity.Member;
-import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.domain.auth.repository.CommonAuthRepository;
-import com.assu.server.domain.common.enums.ActivationStatus;
-import com.assu.server.domain.common.enums.UserRole;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import lombok.RequiredArgsConstructor;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class CommonUserDetailsService implements UserDetailsService {
-
-    private final CommonAuthRepository commonAuthRepository;
-
-    @Override
-    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
-        // CommonAuth: email/password 해시 저장 테이블
-        CommonAuth commonAuth = commonAuthRepository.findByEmail(email)
-                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
-
-        // 연관된 Member에서 역할/상태를 가져옴
-        Member member = commonAuth.getMember();
-        UserRole role = member.getRole();
-        boolean enabled = member.getIsActivated().equals(ActivationStatus.ACTIVE); // ACTIVE면 true
-
-        // 권한명은 스프링 시큐리티 규약에 따라 ROLE_ 접두를 붙임
-        String authority = "ROLE_" + role.name();
-
-        return org.springframework.security.core.userdetails.User
-                .withUsername(commonAuth.getEmail())
-                .password(commonAuth.getPassword()) // 반드시 BCrypt 등 해시
-                .authorities(authority)
-                .accountExpired(false)
-                .accountLocked(false)
-                .credentialsExpired(false)
-                .disabled(!enabled)
-                .build();
-    }
-
-}
-
diff --git a/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java b/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
deleted file mode 100644
index d1713c0..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/student/StudentUserDetailsService.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.assu.server.domain.auth.security.student;
-
-import com.assu.server.domain.member.entity.Member;
-import com.assu.server.domain.auth.entity.SSUAuth;
-import com.assu.server.domain.auth.repository.SSUAuthRepository;
-import com.assu.server.domain.common.enums.ActivationStatus;
-import lombok.RequiredArgsConstructor;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class StudentUserDetailsService implements UserDetailsService {
-    private final SSUAuthRepository ssuAuthRepository;
-
-    @Override
-    public UserDetails loadUserByUsername(String studentNumber) throws UsernameNotFoundException {
-        SSUAuth ssuAuth = ssuAuthRepository.findByStudentNumber(studentNumber)
-                .orElseThrow(() -> new UsernameNotFoundException(studentNumber));
-
-        Member member = ssuAuth.getMember();
-
-        String authority = "ROLE_" + member.getRole().name();
-        boolean enabled = member.getIsActivated().equals(ActivationStatus.ACTIVE);
-
-        // username = 학번, password = "암호문" (Decoder가 복호화/비교)
-        return org.springframework.security.core.userdetails.User
-                .withUsername(ssuAuth.getStudentNumber())
-                .password(ssuAuth.getPasswordCipher()) // 평문 아님! cipher 그대로
-                .authorities(authority)
-                .disabled(!enabled)
-                .accountLocked(false).accountExpired(false).credentialsExpired(false)
-                .build();
-    }
-}
-
diff --git a/src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java b/src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java
deleted file mode 100644
index 698465b..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/student/StudentUsernamePasswordAuthenticationToken.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.assu.server.domain.auth.security.student;
-
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-
-public class StudentUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
-    public StudentUsernamePasswordAuthenticationToken(String studentNumber, String studentPassword) {
-        super(studentNumber, studentPassword);
-    }
-}
-

From f2ca880a43713092c19a1ee5d987a5424c922c1a Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:08:27 +0900
Subject: [PATCH 073/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Auth 설계에 맞도록 구조 리팩토링
---
 .../domain/auth/service/LoginService.java     |   4 +-
 .../domain/auth/service/LoginServiceImpl.java | 121 +++++++++++------
 .../auth/service/LogoutServiceImpl.java       |  44 +++----
 .../auth/service/SignUpServiceImpl.java       | 123 +++++++++---------
 4 files changed, 157 insertions(+), 135 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginService.java b/src/main/java/com/assu/server/domain/auth/service/LoginService.java
index b61edaf..d95714c 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginService.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginService.java
@@ -1,12 +1,12 @@
 package com.assu.server.domain.auth.service;
 
-import com.assu.server.domain.auth.dto.login.LoginRequest;
+import com.assu.server.domain.auth.dto.login.CommonLoginRequest;
 import com.assu.server.domain.auth.dto.login.LoginResponse;
 import com.assu.server.domain.auth.dto.login.RefreshResponse;
 import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
 
 public interface LoginService {
-    LoginResponse login(LoginRequest request);
+    LoginResponse loginCommon(CommonLoginRequest request);
     LoginResponse loginStudent(StudentLoginRequest request);
     RefreshResponse refresh(String refreshToken);
 }
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
index 6f92b4d..76e89de 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -1,50 +1,69 @@
 package com.assu.server.domain.auth.service;
 
-import com.assu.server.domain.auth.dto.login.LoginRequest;
+import com.assu.server.domain.auth.dto.login.CommonLoginRequest;
 import com.assu.server.domain.auth.dto.login.LoginResponse;
 import com.assu.server.domain.auth.dto.login.RefreshResponse;
 import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
 import com.assu.server.domain.auth.dto.signup.Tokens;
-import com.assu.server.domain.auth.entity.CommonAuth;
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
+import com.assu.server.domain.auth.security.token.LoginUsernamePasswordAuthenticationToken;
 import com.assu.server.domain.member.entity.Member;
-import com.assu.server.domain.auth.entity.SSUAuth;
-import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.domain.auth.repository.CommonAuthRepository;
-import com.assu.server.domain.member.repository.MemberRepository;
-import com.assu.server.domain.auth.repository.SSUAuthRepository;
-import com.assu.server.domain.auth.security.JwtUtil;
-import com.assu.server.domain.auth.security.SecurityUtil;
-import com.assu.server.domain.auth.security.common.CommonUsernamePasswordAuthenticationToken;
-import com.assu.server.domain.auth.security.student.StudentUsernamePasswordAuthenticationToken;
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+
 @Service
 @RequiredArgsConstructor
 public class LoginServiceImpl implements LoginService {
 
-    private final CommonAuthRepository commonAuthRepository;
-    private final SSUAuthRepository ssuAuthRepository;
-    private final MemberRepository memberRepository;
-
     private final AuthenticationManager authenticationManager;
     private final JwtUtil jwtUtil;
 
+    // 공통/학생/기타 학교까지 모두 여기로 주입
+    private final List realmAuthAdapters;
+
+    private RealmAuthAdapter pickAdapter(AuthRealm realm) {
+        return realmAuthAdapters.stream()
+                .filter(a -> a.supports(realm))
+                .findFirst()
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION));
+    }
+
+    /**
+     * 공통(파트너/관리자) 로그인: 이메일/비밀번호 기반.
+     * 1) 인증 성공 시 CommonAuth 조회
+     * 2) JWT 발급: username=email, authRealm=COMMON
+     */
     @Override
-    public LoginResponse login(LoginRequest request) {
+    public LoginResponse loginCommon(CommonLoginRequest request) {
         // 공통(파트너/관리자) 로그인: 이메일/비번
-        Authentication auth = authenticationManager.authenticate(
-                new CommonUsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
+        Authentication authentication = authenticationManager.authenticate(
+                new LoginUsernamePasswordAuthenticationToken(
+                        AuthRealm.COMMON,
+                        request.getEmail(),
+                        request.getPassword()
+                )
         );
 
-        CommonAuth commonAuth = commonAuthRepository.findByEmail(auth.getName())
-                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+        RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON);
+
+        // identifier = email
+        Member member = adapter.loadMember(authentication.getName());
 
-        Member member = commonAuth.getMember();
-        Tokens tokens = jwtUtil.issueAndPersistTokens(member, member.getRole());
+        // 토큰 발급 (Access 미저장, Refresh는 Redis 저장)
+        Tokens tokens = jwtUtil.issueTokens(
+                member.getId(),
+                authentication.getName(), // email
+                member.getRole(),
+                adapter.authRealmValue()    // "COMMON"
+        );
 
         return LoginResponse.builder()
                 .memberId(member.getId())
@@ -54,18 +73,36 @@ public LoginResponse login(LoginRequest request) {
                 .build();
     }
 
+    /**
+     * 학생 로그인: 학번/학교 비밀번호 기반.
+     * 1) 인증 성공 시 SSUAuth 조회
+     * 2) JWT 발급: username=studentNumber, authRealm=SSU
+     */
     @Override
     public LoginResponse loginStudent(StudentLoginRequest request) {
-        // 학생 로그인: 학번/학교 비번
-        Authentication auth = authenticationManager.authenticate(
-                new StudentUsernamePasswordAuthenticationToken(request.getStudentNumber(), request.getStudentPassword())
+
+        String realmStr = request.getUniversity().toString();  // University → AuthRealm 매핑
+        AuthRealm authRealm = AuthRealm.valueOf(realmStr);
+        RealmAuthAdapter adapter = pickAdapter(authRealm);
+
+        Authentication authentication = authenticationManager.authenticate(
+                new LoginUsernamePasswordAuthenticationToken(
+                        authRealm,
+                        request.getStudentNumber(),
+                        request.getStudentPassword()
+                )
         );
 
-        SSUAuth ssuAuth = ssuAuthRepository.findByStudentNumber(auth.getName())
-                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+        // identifier = studentNumber
+        Member member = adapter.loadMember(authentication.getName());
 
-        Member member = ssuAuth.getMember();
-        Tokens tokens = jwtUtil.issueAndPersistTokens(member, member.getRole());
+        // 토큰 발급 (Access 미저장, Refresh는 Redis 저장)
+        Tokens tokens = jwtUtil.issueTokens(
+                member.getId(),
+                authentication.getName(), // studentNumber
+                member.getRole(),
+                adapter.authRealmValue()    // 예: "SSU"
+        );
 
         return LoginResponse.builder()
                 .memberId(member.getId())
@@ -75,23 +112,21 @@ public LoginResponse loginStudent(StudentLoginRequest request) {
                 .build();
     }
 
+    /**
+     * Refresh 토큰 재발급(회전).
+     * 전제: JwtAuthFilter가 /auth/refresh 에서 Access(만료 허용) 서명 검증 및 컨텍스트 세팅을 이미 수행.
+     *
+     * 절차:
+     * 1) RT 서명/만료 검증(jwtUtil.validateRefreshToken)
+     * 2) RT의 Claims 추출(만료 X), memberId/jti/username/role/authRealm 획득
+     * 3) Redis 키 "refresh:{memberId}:{jti}" 존재 및 값 일치 확인(도난/중복 재사용 차단)
+     * 4) 기존 RT 키 삭제(회전), 새 토큰 발급(issueTokens)
+     */
     @Override
     public RefreshResponse refresh(String refreshToken) {
-        Long userId = SecurityUtil.getCurrentUserId();
-        Member member = memberRepository.findById(userId)
-                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
-
-        jwtUtil.validateRefreshToken(refreshToken);
-        String savedRt = member.getRefreshToken();
-        if (savedRt == null || !savedRt.equals(refreshToken)) {
-            throw new CustomAuthException(ErrorStatus.REFRESH_TOKEN_NOT_EQUAL);
-        }
-
-        // 회전: JwtUtil에 위임(내부의 valid-seconds 사용)
-        Tokens rotated = jwtUtil.issueAndPersistTokens(member, member.getRole());
-
+        Tokens rotated = jwtUtil.rotateRefreshToken(refreshToken);
         return new RefreshResponse(
-                userId,
+                ((Number) jwtUtil.validateTokenOnlySignature(rotated.getAccessToken()).get("userId")).longValue(),
                 rotated.getAccessToken(),
                 rotated.getRefreshToken()
         );
diff --git a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
index b8c5756..b80f695 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
@@ -1,45 +1,35 @@
 package com.assu.server.domain.auth.service;
 
-import com.assu.server.domain.member.repository.MemberRepository;
-import com.assu.server.domain.auth.security.JwtUtil;
-import com.assu.server.domain.auth.security.SecurityUtil;
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.security.jwt.JwtUtil;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import io.jsonwebtoken.Claims;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 @Service
 @RequiredArgsConstructor
 public class LogoutServiceImpl implements LogoutService {
 
-    private MemberRepository memberRepository;
-
     private final JwtUtil jwtUtil;
     private final RedisTemplate redisTemplate;
 
     @Override
-    public void logout(String rawAccessToken) {
-        Long userId = SecurityUtil.getCurrentUserId();
-
-        // RT 무효화: DB에서 제거
-        memberRepository.findById(userId).ifPresent(m -> {
-            m.setRefreshToken(null);
-            m.setAccessToken(null);
-            memberRepository.save(m);
-        });
-
-        // Access 토큰 블랙리스트 등록
-        long remainSec;
-        try {
-            // JwtUtil에 같은 로직의 오버로드를 추가해도 됨
-            remainSec = jwtUtil.tokenRemainTimeSecond("Bearer " + rawAccessToken);
-        } catch (Exception e) {
-            remainSec = 0L;
-        }
-        if (remainSec > 0) {
-            String key = "blackList:" + userId;
-            redisTemplate.opsForValue().set(key, rawAccessToken, remainSec, TimeUnit.SECONDS);
-        }
+    public void logout(String authorization) {
+        String rawAccessToken = jwtUtil.getTokenFromHeader(authorization);
+
+        // 1) Access 토큰 Claims 추출 (만료 허용, 서명 검증)
+        Claims accessClaims = jwtUtil.validateTokenOnlySignature(rawAccessToken);
+
+        // 2) Access 블랙리스트 등록
+        jwtUtil.blacklistAccess(rawAccessToken);
+
+        // 3) 해당 사용자(memberId)의 모든 Refresh 토큰 키 제거 (전역 로그아웃)
+        Long memberId = ((Number) accessClaims.get("userId")).longValue();
+        jwtUtil.removeAllRefreshTokens(memberId);
     }
 }
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 9681b1a..f1b081d 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
@@ -2,16 +2,13 @@
 
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
-import com.assu.server.domain.auth.crypto.SchoolCredentialEncryptor;
 import com.assu.server.domain.auth.dto.signup.*;
 import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
 import com.assu.server.domain.auth.dto.signup.student.StudentInfoPayload;
-import com.assu.server.domain.auth.entity.CommonAuth;
-import com.assu.server.domain.auth.entity.SSUAuth;
-import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.domain.auth.repository.CommonAuthRepository;
-import com.assu.server.domain.auth.repository.SSUAuthRepository;
-import com.assu.server.domain.auth.security.JwtUtil;
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
+import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.common.enums.UserRole;
 import com.assu.server.domain.member.entity.Member;
@@ -25,40 +22,44 @@
 import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
-import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.List;
+
 
 @Service
 @RequiredArgsConstructor
 public class SignUpServiceImpl implements SignUpService {
 
     private final MemberRepository memberRepository;
-    private final SSUAuthRepository ssuAuthRepository;
-    private final CommonAuthRepository commonAuthRepository;
     private final StudentRepository studentRepository;
     private final PartnerRepository partnerRepository;
     private final AdminRepository adminRepository;
 
-    private final PasswordEncoder passwordEncoder;           // 공통(파트너/관리자)용 BCrypt
-    private final SchoolCredentialEncryptor schoolEncryptor; // 학생용 AES-GCM
+    // Adapter 들을 주입받아서, signup 시에 사용
+    private final List realmAuthAdapters;
+
     private final AmazonS3Manager amazonS3Manager;
     private final JwtUtil jwtUtil;
 
+    private RealmAuthAdapter pickAdapter(AuthRealm realm) {
+        return realmAuthAdapters.stream()
+                .filter(a -> a.supports(realm))
+                .findFirst()
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION));
+    }
+
     /* 학생: JSON */
     @Override
     @Transactional
     public SignUpResponse signupStudent(StudentSignUpRequest req) {
         // 중복 체크
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
-        }
-        if (ssuAuthRepository.existsByStudentNumber(req.getStudentAuth().getStudentNumber())) {
-            throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
+            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
         }
 
-        // member 생성
+        // 1) member 생성
         Member member = memberRepository.save(
                 Member.builder()
                         .phoneNum(req.getPhoneNumber())
@@ -68,18 +69,11 @@ public SignUpResponse signupStudent(StudentSignUpRequest req) {
                         .build()
         );
 
-        // ssu_auth 생성(학생번호/암호화 PW(AES-GCM))
-        String cipher = schoolEncryptor.encrypt(req.getStudentAuth().getStudentPassword());
-        ssuAuthRepository.save(
-                SSUAuth.builder()
-                        .member(member)
-                        .studentNumber(req.getStudentAuth().getStudentNumber())
-                        .passwordCipher(cipher)
-                        .isAuthenticated(true) // 초기값(유세인트 검증 완료 여부에 맞게 조정)
-                        .build()
-        );
+        // 2) RealmAuthAdapter 로 자격 저장 (학교별 암호화 전략 반영됨)
+        RealmAuthAdapter adapter = pickAdapter(AuthRealm.valueOf(req.getStudentInfo().getUniversity().toString()));
+        adapter.registerCredentials(member, req.getStudentAuth().getStudentNumber(), req.getStudentAuth().getStudentPassword());
 
-        // student 프로필 생성
+        // 3) Student 프로필 생성
         StudentInfoPayload info = req.getStudentInfo();
         Major major;
         switch (info.getMajor()) {
@@ -105,8 +99,13 @@ public SignUpResponse signupStudent(StudentSignUpRequest req) {
                         .build()
         );
 
-        // JWT 발급 및 저장
-        Tokens tokens = jwtUtil.issueAndPersistTokens(member, UserRole.STUDENT);
+        // 4) 토큰 발급
+        Tokens tokens = jwtUtil.issueTokens(
+                member.getId(),
+                req.getStudentAuth().getStudentNumber(),
+                UserRole.STUDENT,
+                adapter.authRealmValue()
+        );
 
         return SignUpResponse.builder()
                 .memberId(member.getId())
@@ -121,38 +120,30 @@ public SignUpResponse signupStudent(StudentSignUpRequest req) {
     @Transactional
     public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage) {
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
-        }
-        if (commonAuthRepository.existsByEmail(req.getCommonAuth().getEmail())) {
-            throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL);
+            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
         }
 
+        // 1) member 생성
         Member member = memberRepository.save(
                 Member.builder()
                         .phoneNum(req.getPhoneNumber())
                         .isPhoneVerified(true)
                         .role(UserRole.PARTNER)
-                        .isActivated(ActivationStatus.SUSPEND) // 사업자 등록증 확인 후 활성화
+                        .isActivated(ActivationStatus.ACTIVE) // Todo 초기에 SUSPEND 로직 추가해야함, 허가 후 ACTIVE
                         .build()
         );
 
-        // CommonAuth: BCrypt 해시 저장
-        String pwHash = passwordEncoder.encode(req.getCommonAuth().getPassword());
-        commonAuthRepository.save(
-                CommonAuth.builder()
-                        .member(member)
-                        .email(req.getCommonAuth().getEmail())
-                        .password(pwHash)
-                        .isEmailVerified(false)
-                        .build()
-        );
+        // 2) RealmAuthAdapter 로 Common 자격 저장
+        RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON);
+        adapter.registerCredentials(member, req.getCommonAuth().getEmail(), req.getCommonAuth().getPassword());
 
         // 파일 업로드 + 파트너 정보
         String keyPath = "partners/" + member.getId() + "/" + licenseImage.getOriginalFilename();
         String keyName = amazonS3Manager.generateKeyName(keyPath);
         String licenseUrl = amazonS3Manager.uploadFile(keyName, licenseImage);
-
         CommonInfoPayload info = req.getCommonInfo();
+
+        // 3) Partner 프로필 생성
         partnerRepository.save(
                 Partner.builder()
                         .member(member)
@@ -163,7 +154,13 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
                         .build()
         );
 
-        Tokens tokens = jwtUtil.issueAndPersistTokens(member, UserRole.PARTNER);
+        // 4) 토큰 발급
+        Tokens tokens = jwtUtil.issueTokens(
+                member.getId(),
+                req.getCommonAuth().getEmail(),
+                UserRole.PARTNER,
+                adapter.authRealmValue()
+        );
 
         return SignUpResponse.builder()
                 .memberId(member.getId())
@@ -178,36 +175,30 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
     @Transactional
     public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImage) {
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
-        }
-        if (commonAuthRepository.existsByEmail(req.getCommonAuth().getEmail())) {
-            throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL);
+            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
         }
 
+        // 1) member 생성
         Member member = memberRepository.save(
                 Member.builder()
                         .phoneNum(req.getPhoneNumber())
                         .isPhoneVerified(true)
                         .role(UserRole.ADMIN)
-                        .isActivated(ActivationStatus.SUSPEND) // 인감 확인 후 활성화
+                        .isActivated(ActivationStatus.ACTIVE) // Todo 초기에 SUSPEND 로직 추가해야함, 허가 후 ACTIVE
                         .build()
         );
 
-        String pwHash = passwordEncoder.encode(req.getCommonAuth().getPassword());
-        commonAuthRepository.save(
-                CommonAuth.builder()
-                        .member(member)
-                        .email(req.getCommonAuth().getEmail())
-                        .password(pwHash)
-                        .isEmailVerified(false)
-                        .build()
-        );
+        // 2) RealmAuthAdapter 로 Common 자격 저장
+        RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON);
+        adapter.registerCredentials(member, req.getCommonAuth().getEmail(), req.getCommonAuth().getPassword());
 
+        // 파일 업로드 + 관리자 정보
         String keyPath = "admins/" + member.getId() + "/" + signImage.getOriginalFilename();
         String keyName = amazonS3Manager.generateKeyName(keyPath);
         String signUrl = amazonS3Manager.uploadFile(keyName, signImage);
-
         CommonInfoPayload info = req.getCommonInfo();
+
+        // 3) Partner 프로필 생성
         adminRepository.save(
                 Admin.builder()
                         .member(member)
@@ -218,7 +209,13 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
                         .build()
         );
 
-        Tokens tokens = jwtUtil.issueAndPersistTokens(member, UserRole.ADMIN);
+        // 4) 토큰 발급
+        Tokens tokens = jwtUtil.issueTokens(
+                member.getId(),
+                req.getCommonAuth().getEmail(),
+                UserRole.ADMIN,
+                adapter.authRealmValue()
+        );
 
         return SignUpResponse.builder()
                 .memberId(member.getId())

From d1fb896be3310d75165dbcdacd2fec19265198df Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:10:21 +0900
Subject: [PATCH 074/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- RoutingAuthenticationProvider: Provider 통일
- LoginUsernamePasswordAuthenticationToken: Token 통일
- PrincipalDetails 적용
- SSUAuthAdapter와 CommonAuthAdapter로 Auth 로직 분기
- AuthRealm으로 Auth 로직 분기
---
 .../server/domain/auth/entity/AuthRealm.java  |  5 ++
 .../security/adapter/CommonAuthAdapter.java   | 65 ++++++++++++++++
 .../auth/security/adapter/SSUAuthAdapter.java | 65 ++++++++++++++++
 .../RoutingAuthenticationProvider.java        | 69 +++++++++++++++++
 ...inUsernamePasswordAuthenticationToken.java | 18 +++++
 .../global/config/AuthProviderConfig.java     | 46 ++----------
 .../server/global/util/PrincipalDetails.java  | 75 +++++++++++++++++++
 7 files changed, 302 insertions(+), 41 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/auth/entity/AuthRealm.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java
 create mode 100644 src/main/java/com/assu/server/global/util/PrincipalDetails.java

diff --git a/src/main/java/com/assu/server/domain/auth/entity/AuthRealm.java b/src/main/java/com/assu/server/domain/auth/entity/AuthRealm.java
new file mode 100644
index 0000000..eacd0e1
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/entity/AuthRealm.java
@@ -0,0 +1,5 @@
+package com.assu.server.domain.auth.entity;
+
+public enum AuthRealm {
+    COMMON, SSU
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
new file mode 100644
index 0000000..bb5fc56
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
@@ -0,0 +1,65 @@
+package com.assu.server.domain.auth.security.adapter;
+
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.auth.entity.CommonAuth;
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.repository.CommonAuthRepository;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CommonAuthAdapter implements RealmAuthAdapter {
+    private final CommonAuthRepository commonAuthRepository;
+    private final PasswordEncoder passwordEncoder; // BCrypt
+
+    @Override public boolean supports(AuthRealm realm) { return realm == AuthRealm.COMMON; }
+
+    @Override
+    public UserDetails loadUserDetails(String email) {
+        CommonAuth ca = commonAuthRepository.findByEmail(email)
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+        var m = ca.getMember();
+        boolean enabled = m.getIsActivated() == ActivationStatus.ACTIVE;
+        String authority = "ROLE_" + m.getRole().name();
+
+        return org.springframework.security.core.userdetails.User
+                .withUsername(ca.getEmail())
+                .password(ca.getPassword()) // BCrypt 해시
+                .authorities(authority)
+                .accountExpired(false).accountLocked(false).credentialsExpired(false)
+                .disabled(!enabled)
+                .build();
+    }
+
+    @Override public Member loadMember(String email) {
+        return commonAuthRepository.findByEmail(email)
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER))
+                .getMember();
+    }
+
+    @Override
+    public void registerCredentials(Member member, String email, String rawPassword) {
+        if (commonAuthRepository.existsByEmail(email)) {
+            throw new CustomAuthHandler(ErrorStatus.EXISTED_EMAIL);
+        }
+        String hash = passwordEncoder.encode(rawPassword);
+        commonAuthRepository.save(
+                CommonAuth.builder()
+                        .member(member)
+                        .email(email)
+                        .password(hash)
+                        .isEmailVerified(false)
+                        .build()
+        );
+    }
+
+    @Override public PasswordEncoder passwordEncoder() { return passwordEncoder; }
+    @Override public String authRealmValue() { return "COMMON"; }
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
new file mode 100644
index 0000000..808cd63
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
@@ -0,0 +1,65 @@
+package com.assu.server.domain.auth.security.adapter;
+
+import com.assu.server.domain.auth.crypto.StudentPasswordEncoder;
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.auth.entity.SSUAuth;
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.repository.SSUAuthRepository;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class SSUAuthAdapter implements RealmAuthAdapter {
+    private final SSUAuthRepository ssuAuthRepository;
+    private final StudentPasswordEncoder studentPasswordEncoder; // AES-GCM 비교
+
+    @Override public boolean supports(AuthRealm realm) { return realm == AuthRealm.SSU; }
+
+    @Override
+    public UserDetails loadUserDetails(String studentNumber) {
+        SSUAuth sa = ssuAuthRepository.findByStudentNumber(studentNumber)
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+        var m = sa.getMember();
+        boolean enabled = m.getIsActivated() == ActivationStatus.ACTIVE;
+        String authority = "ROLE_" + m.getRole().name();
+
+        return org.springframework.security.core.userdetails.User
+                .withUsername(sa.getStudentNumber())
+                .password(sa.getPasswordCipher()) // 암호문, 비교는 Encoder가 담당
+                .authorities(authority)
+                .accountExpired(false).accountLocked(false).credentialsExpired(false)
+                .disabled(!enabled)
+                .build();
+    }
+
+    @Override public Member loadMember(String studentNumber) {
+        return ssuAuthRepository.findByStudentNumber(studentNumber)
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER))
+                .getMember();
+    }
+
+    @Override
+    public void registerCredentials(Member member, String studentNumber, String rawPassword) {
+        if (ssuAuthRepository.existsByStudentNumber(studentNumber)) {
+            throw new CustomAuthHandler(ErrorStatus.EXISTED_STUDENT);
+        }
+        String cipher = studentPasswordEncoder.encode(rawPassword);
+        ssuAuthRepository.save(
+                SSUAuth.builder()
+                        .member(member)
+                        .studentNumber(studentNumber)
+                        .passwordCipher(cipher)
+                        .isAuthenticated(true)
+                        .build()
+        );
+    }
+
+    @Override public PasswordEncoder passwordEncoder() { return studentPasswordEncoder; }
+    @Override public String authRealmValue() { return "SSU"; }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java b/src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java
new file mode 100644
index 0000000..01d5dc9
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/provider/RoutingAuthenticationProvider.java
@@ -0,0 +1,69 @@
+package com.assu.server.domain.auth.security.provider;
+
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
+import com.assu.server.domain.auth.security.token.LoginUsernamePasswordAuthenticationToken;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class RoutingAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
+
+    private final List adapters;
+
+    @Override
+    public boolean supports(Class authentication) {
+        return LoginUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
+    }
+
+    private AuthRealm resolveRealm(Authentication auth) {
+        if (auth instanceof LoginUsernamePasswordAuthenticationToken) return ((LoginUsernamePasswordAuthenticationToken) auth).getRealm();
+        throw new IllegalArgumentException("Unsupported authentication token: " + auth.getClass());
+    }
+
+    private RealmAuthAdapter pickAdapter(AuthRealm realm) {
+        return adapters.stream()
+                .filter(a -> a.supports(realm))
+                .findFirst()
+                .orElseThrow(() -> new IllegalStateException("No adapter for realm: " + realm));
+    }
+
+    @Override
+    protected void additionalAuthenticationChecks(
+            UserDetails userDetails,
+            UsernamePasswordAuthenticationToken authentication)
+            throws AuthenticationException {
+
+        AuthRealm realm = resolveRealm(authentication);
+        RealmAuthAdapter adapter = pickAdapter(realm);
+
+        String presented = (authentication.getCredentials() != null)
+                ? authentication.getCredentials().toString()
+                : null;
+
+        if (presented == null || !adapter.passwordEncoder().matches(presented, userDetails.getPassword())) {
+            throw new BadCredentialsException("Bad credentials");
+        }
+    }
+
+    @Override
+    protected UserDetails retrieveUser(
+            String username,
+            UsernamePasswordAuthenticationToken authentication)
+            throws AuthenticationException {
+
+        AuthRealm realm = resolveRealm(authentication);
+        RealmAuthAdapter adapter = pickAdapter(realm);
+        return adapter.loadUserDetails(username);
+    }
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java b/src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java
new file mode 100644
index 0000000..b90043c
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/token/LoginUsernamePasswordAuthenticationToken.java
@@ -0,0 +1,18 @@
+package com.assu.server.domain.auth.security.token;
+
+import com.assu.server.domain.auth.entity.AuthRealm;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+
+// 단일 인증 토큰
+public class LoginUsernamePasswordAuthenticationToken
+        extends UsernamePasswordAuthenticationToken {
+
+    private final AuthRealm realm;
+
+    public LoginUsernamePasswordAuthenticationToken(AuthRealm realm, String principal, String credentials) {
+        super(principal, credentials);
+        this.realm = realm;
+    }
+
+    public AuthRealm getRealm() { return realm; }
+}
diff --git a/src/main/java/com/assu/server/global/config/AuthProviderConfig.java b/src/main/java/com/assu/server/global/config/AuthProviderConfig.java
index 27d320c..0a1b6f8 100644
--- a/src/main/java/com/assu/server/global/config/AuthProviderConfig.java
+++ b/src/main/java/com/assu/server/global/config/AuthProviderConfig.java
@@ -1,59 +1,23 @@
 package com.assu.server.global.config;
 
-import com.assu.server.domain.auth.crypto.StudentPasswordEncoder;
-import com.assu.server.domain.auth.security.*;
-import com.assu.server.domain.auth.security.common.CommonUserDetailsService;
-import com.assu.server.domain.auth.security.common.CommonUsernamePasswordAuthenticationToken;
-import com.assu.server.domain.auth.security.student.StudentUserDetailsService;
-import com.assu.server.domain.auth.security.student.StudentUsernamePasswordAuthenticationToken;
+import com.assu.server.domain.auth.security.provider.RoutingAuthenticationProvider;
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.ProviderManager;
-import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
-import org.springframework.security.crypto.password.PasswordEncoder;
-
 import java.util.List;
 
 @Configuration
 @RequiredArgsConstructor
 public class AuthProviderConfig {
 
-    private final CommonUserDetailsService commonUserDetailsService;
-    private final StudentUserDetailsService studentUserDetailsService;
-    private final PasswordEncoder passwordEncoder;                 // BCrypt (공통)
-    private final StudentPasswordEncoder studentPasswordEncoder;   // 학생 전용(AES-GCM 복호화 비교)
-
-    @Bean
-    public DaoAuthenticationProvider commonAuthProvider() {
-        DaoAuthenticationProvider p = new DaoAuthenticationProvider() {
-            @Override public boolean supports(Class authentication) {
-                return CommonUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
-            }
-        };
-        p.setUserDetailsService(commonUserDetailsService);
-        p.setPasswordEncoder(passwordEncoder);
-        return p;
-    }
-
-    @Bean
-    public DaoAuthenticationProvider studentAuthProvider() {
-        DaoAuthenticationProvider p = new DaoAuthenticationProvider() {
-            @Override public boolean supports(Class authentication) {
-                return StudentUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
-            }
-        };
-        p.setUserDetailsService(studentUserDetailsService);
-        p.setPasswordEncoder(studentPasswordEncoder);
-        return p;
-    }
+    private final RoutingAuthenticationProvider routingAuthenticationProvider;
 
     @Bean
     public AuthenticationManager authenticationManager(
-            DaoAuthenticationProvider studentAuthProvider,
-            DaoAuthenticationProvider commonAuthProvider
     ) {
-        return new ProviderManager(List.of(studentAuthProvider, commonAuthProvider));
+        return new org.springframework.security.authentication.ProviderManager(
+                List.of(routingAuthenticationProvider)
+        );
     }
 }
diff --git a/src/main/java/com/assu/server/global/util/PrincipalDetails.java b/src/main/java/com/assu/server/global/util/PrincipalDetails.java
new file mode 100644
index 0000000..2d56072
--- /dev/null
+++ b/src/main/java/com/assu/server/global/util/PrincipalDetails.java
@@ -0,0 +1,75 @@
+package com.assu.server.global.util;
+
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.domain.member.entity.Member;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Getter
+@Builder
+@RequiredArgsConstructor
+public class PrincipalDetails implements UserDetails {
+
+    private final Long memberId;                     // 항상 존재
+    private final String username;                   // email 또는 studentNumber
+    private final String password;                   // DaoAuthenticationProvider 단계에서만 사용
+    private final UserRole role;                     // STUDENT/PARTNER/ADMIN
+    private final AuthRealm authRealm;               // COMMON or SSU or 추후 확장..
+    private final boolean enabled;
+    private final Collection authorities;
+
+    private final Member member;
+
+    @Override
+    public Collection getAuthorities() {
+        // 단일 롤 → ROLE_ 접두사 필수
+        String roleName = "ROLE_" + member.getRole().name(); // STUDENT → ROLE_STUDENT
+        return List.of(new SimpleGrantedAuthority(roleName));
+    }
+
+    @Override
+    public String getPassword() {
+        // 폼 로그인/DaoAuthenticationProvider를 쓴다면 반드시 반환
+        return member.getCommonAuth().getPassword();
+    }
+
+    @Override
+    public String getUsername() {
+        // 인증 후 Authentication.getName() 으로 쓰일 값
+        return member.getId().toString();
+    }
+
+    public Long getId() {
+        // 인증 후 Authentication.getId() 으로 쓰일 값
+        return member.getId();
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return member.getIsActivated().equals(ActivationStatus.ACTIVE); // 사용자가 Disabled면 DisabledException 방지/반영
+    }
+}
\ No newline at end of file

From bf9437892afba98b0f2159020c3d46fe4203b2de Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:11:12 +0900
Subject: [PATCH 075/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- DTO 일부 수정
- Handler 추가
- Student 엔티티 일부 변경
---
 .../{LoginRequest.java => CommonLoginRequest.java}   |  2 +-
 .../domain/auth/dto/login/StudentLoginRequest.java   | 10 ++++++++--
 .../domain/auth/exception/CustomAuthHandler.java     | 12 ++++++++++++
 .../domain/auth/service/PhoneAuthServiceImpl.java    |  4 ++--
 .../com/assu/server/domain/user/entity/Student.java  |  2 ++
 5 files changed, 25 insertions(+), 5 deletions(-)
 rename src/main/java/com/assu/server/domain/auth/dto/login/{LoginRequest.java => CommonLoginRequest.java} (96%)
 create mode 100644 src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java

diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/LoginRequest.java b/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequest.java
similarity index 96%
rename from src/main/java/com/assu/server/domain/auth/dto/login/LoginRequest.java
rename to src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequest.java
index f5930b1..20ef18b 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/login/LoginRequest.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/login/CommonLoginRequest.java
@@ -13,7 +13,7 @@
 @AllArgsConstructor
 @Builder
 @Schema(description = "파트너/관리자 공통 로그인 요청")
-public class LoginRequest {
+public class CommonLoginRequest {
 
     @Schema(description = "로그인 이메일", example = "user@example.com")
     @NotBlank(message = "이메일은 필수입니다.")
diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java b/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
index c6b334e..7241f27 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
@@ -1,7 +1,9 @@
 package com.assu.server.domain.auth.dto.login;
 
+import com.assu.server.domain.user.entity.enums.University;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
 import lombok.*;
 
@@ -14,14 +16,18 @@
 @Schema(description = "학생 로그인 요청")
 public class StudentLoginRequest {
 
-    @Schema(description = "학번", example = "student@example.com")
+    @Schema(description = "학번", example = "20001234")
     @NotBlank(message = "학번은 필수입니다.")
-    @Size(max = 10, message = "이메일은 10자를 넘을 수 없습니다.")
+    @Size(max = 10, message = "학번은 10자를 넘을 수 없습니다.")
     private String studentNumber;
 
     @Schema(description = "로그인 비밀번호(평문)", example = "P@ssw0rd!")
     @NotBlank(message = "비밀번호는 필수입니다.")
     @Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.")
     private String studentPassword;
+
+    @Schema(description = "대학 이름", example = "SSU")
+    @NotNull(message = "대학 이름은 필수입니다.")
+    private University university;
 }
 
diff --git a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java
new file mode 100644
index 0000000..3ca144a
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java
@@ -0,0 +1,12 @@
+package com.assu.server.domain.auth.exception;
+
+import com.assu.server.global.apiPayload.code.BaseErrorCode;
+import com.assu.server.global.exception.GeneralException;
+import org.springframework.http.HttpStatus;
+
+public class CustomAuthHandler extends GeneralException {
+
+    public CustomAuthHandler(BaseErrorCode errorCode) {
+        super(errorCode);
+    }
+}
\ No newline at end of file
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 bd73185..70104cc 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
@@ -2,7 +2,7 @@
 
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.util.RandomNumberUtil;
-import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.core.ValueOperations;
@@ -38,7 +38,7 @@ public void verifyAuthNumber(String phoneNumber, String authNumber) {
         String stored = valueOps.get(phoneNumber);
 
         if (stored == null || !stored.equals(authNumber)) {
-            throw new CustomAuthException(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
+            throw new CustomAuthHandler(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
         }
 
         // 인증 성공 시 Redis에서 삭제(Optional)
diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java
index 29771f2..b677c78 100644
--- a/src/main/java/com/assu/server/domain/user/entity/Student.java
+++ b/src/main/java/com/assu/server/domain/user/entity/Student.java
@@ -24,6 +24,7 @@ public class Student {
     @MapsId
     private Member member;
 
+    @Enumerated(EnumType.STRING)
     private Department department;
 
     @Enumerated(EnumType.STRING)
@@ -32,6 +33,7 @@ public class Student {
     @Pattern(regexp = "^[0-9]{1}-[1-2]$", message = "yearSemester는 Y-N 형식이어야 합니다. 예: 3-1")
     private String yearSemester;
 
+    @Enumerated(EnumType.STRING)
     private University university;
 
     private int stamp;

From f237bd6944d730edde4efd6e554ee739857df44d Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:11:28 +0900
Subject: [PATCH 076/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 사용하지 않는 클래스 삭제
---
 .../auth/exception/CustomAuthException.java   | 30 -------------------
 ...onUsernamePasswordAuthenticationToken.java |  9 ------
 2 files changed, 39 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java

diff --git a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
deleted file mode 100644
index e98e8a2..0000000
--- a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.assu.server.domain.auth.exception;
-
-import com.assu.server.global.apiPayload.code.BaseErrorCode;
-import org.springframework.http.HttpStatus;
-
-public class CustomAuthException extends RuntimeException {
-
-    private final BaseErrorCode errorCode;
-    private final String code;
-    private final HttpStatus httpStatus;
-
-    public CustomAuthException(BaseErrorCode errorCode) {
-        super(errorCode.getReasonHttpStatus().getMessage());
-        this.errorCode = errorCode;
-        this.code = errorCode.getReasonHttpStatus().getCode();
-        this.httpStatus = errorCode.getReasonHttpStatus().getHttpStatus();
-    }
-
-    public BaseErrorCode getErrorCode() {
-        return errorCode;
-    }
-
-    public String getCode() {
-        return code;
-    }
-
-    public HttpStatus getHttpStatus() {
-        return httpStatus;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java b/src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java
deleted file mode 100644
index 296429b..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/common/CommonUsernamePasswordAuthenticationToken.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.assu.server.domain.auth.security.common;
-
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-
-public class CommonUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
-    public CommonUsernamePasswordAuthenticationToken(String email, String password) {
-        super(email, password);
-    }
-}

From b4bbc9aeec0aee0331580193ac46669d0f846394 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:12:03 +0900
Subject: [PATCH 077/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- JwtUtil과 JwtAuthFilter 로직 수정
---
 .../auth/security/jwt/JwtAuthFilter.java      | 164 +++++++++
 .../domain/auth/security/jwt/JwtUtil.java     | 324 ++++++++++++++++++
 2 files changed, 488 insertions(+)
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java

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
new file mode 100644
index 0000000..4eac706
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtAuthFilter.java
@@ -0,0 +1,164 @@
+package com.assu.server.domain.auth.security.jwt;
+
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * JWT 인증 필터.
+ *
+ * 책임:
+ * - 보호 자원에 대해 Authorization 헤더의 Bearer 토큰을 검증하고 SecurityContext에
+ * Authentication을 설정한다.
+ * - 토큰 재발급 엔드포인트(/auth/token/reissue)에서는
+ * 1) Access 토큰(만료 허용)의 서명을 검증하고 블랙리스트 여부를 확인,
+ * 2) Refresh 토큰의 서명/만료를 검증하고 Redis 저장 여부를 확인한 뒤,
+ * 3) 만료된 Access 토큰에서 Authentication을 복원해 컨텍스트에 주입한다.
+ *
+ * 주의:
+ * - 화이트리스트는 Swagger 등 공개 리소스에 한정한다. /auth/** 전체를 우회시키지 않는다.
+ */
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+    @Value("${jwt.header}")
+    private String jwtHeader;
+    private final JwtUtil jwtUtil;
+    private final RedisTemplate redisTemplate;
+
+    private static final AntPathMatcher PATH = new AntPathMatcher();
+    // 공개 경로(필터 우회)
+    private static final String[] WHITELIST = {
+            "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
+            "/swagger-resources/**", "/webjars/**",
+            "/auth/login/**", "/auth/signup/**", "/auth/phone-numbers/**"
+    };
+
+    /**
+     * 이 요청에 대해 필터를 적용하지 않을지 여부를 판단하는 함수
+     * 화이트리스트 패턴은 우회
+     */
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        if ("OPTIONS".equalsIgnoreCase(request.getMethod()))
+            return true; // CORS preflight 우회
+        if (PATH.match("/auth/refresh", uri))
+            return false; // 토큰 재발급은 필터 적용
+        for (String p : WHITELIST)
+            if (PATH.match(p, uri))
+                return true; // 나머지 공개 경로 우회
+        return false; // 보호 자원은 필터 적용
+    }
+
+    /**
+     * Authorization 헤더가 존재하고 Bearer 포맷인지 확인한다.
+     * 
+     * @throws CustomAuthHandler 헤더 누락/형식 오류
+     */
+    private static void requireBearerAuthorizationHeader(String authorizationHeader) {
+        if (authorizationHeader == null) {
+            throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+        }
+        if (!authorizationHeader.startsWith("Bearer ")) {
+            throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
+        }
+    }
+
+    /**
+     * 실제 필터링 로직.
+     * - 재발급 경로: Access(서명만), Refresh 검증 + 블랙리스트/Redis 확인 후 Authentication 세팅
+     * - 일반 보호 경로: Access 검증 + 블랙리스트 확인 후 Authentication 세팅
+     */
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain chain)
+            throws ServletException, IOException {
+
+        String authorizationHeader = request.getHeader(jwtHeader);
+        String requestUri = request.getRequestURI();
+
+        // ───────── 재발급 경로 처리 (/auth/token/reissue) ─────────
+        if (PATH.match("/auth/refresh", requestUri)) {
+            String refreshToken = request.getHeader("refreshToken");
+            try {
+                // Bearer 헤더 검증
+                requireBearerAuthorizationHeader(authorizationHeader);
+                if (refreshToken == null) {
+                    throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+                }
+
+                // Access 토큰: 서명만 검증(만료 허용) 및 블랙리스트 확인(JTI)
+                String accessToken = jwtUtil.getTokenFromHeader(authorizationHeader);
+                Claims accessClaims = jwtUtil.validateTokenOnlySignature(accessToken);
+                String accessJti = accessClaims.getId();
+                Boolean accessBlacklisted = redisTemplate.hasKey("blacklist:" + accessJti);
+                if (Boolean.TRUE.equals(accessBlacklisted)) {
+                    throw new CustomAuthHandler(ErrorStatus.LOGOUT_USER);
+                }
+
+                // Refresh 토큰: 서명/만료 검증 + Redis 저장 여부 확인
+                jwtUtil.validateRefreshToken(refreshToken);
+                Claims refreshClaims = jwtUtil.validateTokenOnlySignature(refreshToken); // 만료 전이어야 함
+                Long memberIdFromRefresh = ((Number) refreshClaims.get("userId")).longValue();
+                String refreshJti = refreshClaims.getId();
+                String refreshKey = String.format("refresh:%d:%s", memberIdFromRefresh, refreshJti);
+                Boolean refreshExists = redisTemplate.hasKey(refreshKey);
+                if (Boolean.FALSE.equals(refreshExists)) {
+                    // 저장된 RT가 없으면 유효하지 않은 재발급 시도
+                    throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+                }
+
+                // 컨텍스트에 만료된 Access 토큰으로부터 Authentication 복원
+                Authentication authentication = jwtUtil.getAuthenticationFromExpiredAccessToken(accessToken);
+                SecurityContextHolder.getContext().setAuthentication(authentication);
+
+                chain.doFilter(request, response);
+                return;
+            } catch (Exception exception) {
+                log.error("인증 과정 중, 예상치 못한 예외 발생: {}", exception.getMessage(), exception);
+                throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            }
+        }
+
+        // ───────── 일반 보호 자원 처리 ─────────
+        // Authorization 헤더가 없거나 Bearer 형식이 아니면 그대로 통과(익명으로 처리됨)
+        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            String accessToken = jwtUtil.getTokenFromHeader(authorizationHeader);
+
+            // 블랙리스트 확인(만료 허용 X) + Authentication 복원
+            jwtUtil.assertNotBlacklisted(accessToken);
+            Authentication authentication = jwtUtil.getAuthentication(accessToken);
+
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+            chain.doFilter(request, response);
+        } catch (Exception exception) {
+            log.error("인증 과정 중, 예상치 못한 예외 발생: {}", exception.getMessage(), exception);
+            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+        }
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
new file mode 100644
index 0000000..5c86540
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
@@ -0,0 +1,324 @@
+package com.assu.server.domain.auth.security.jwt;
+
+import com.assu.server.domain.auth.dto.signup.Tokens;
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.util.PrincipalDetails;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.time.ZonedDateTime;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * JWT 발급/검증 및 Authentication 복원 유틸리티.
+ */
+@Component
+@RequiredArgsConstructor
+public class JwtUtil {
+
+    @Value("${jwt.secret}")
+    public String secretKey;
+
+    @Value("${jwt.access-valid-seconds:3600}")      // 1시간 기본
+    private int accessValidSeconds;
+
+    @Value("${jwt.refresh-valid-seconds:1209600}")  // 14일 기본
+    private int refreshValidSeconds;
+
+    private final MemberRepository memberRepository;
+    private final RedisTemplate redisTemplate;
+
+    @PostConstruct
+    public void clearRedisOnStartup() {
+        redisTemplate.getConnectionFactory().getConnection().flushAll();
+    }
+
+    // ───────── 토큰 생성 공통 유틸 ─────────
+    /**
+     * 서명용 SecretKey 생성.
+     */
+    private SecretKey key() {
+        return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * "Bearer xxx" 헤더에서 실제 토큰 문자열만 추출.
+     * @param authorizationHeader Authorization 헤더 값
+     * @return 토큰 문자열
+     */
+    public String getTokenFromHeader(String authorizationHeader) {
+        return authorizationHeader.split(" ")[1];
+    }
+
+    /**
+     * JWT 문자열 생성.
+     * @param claims       토큰 클레임
+     * @param validSeconds 유효 기간(초)
+     * @param jti          JWT ID(고유 식별자)
+     * @return 서명된 토큰 문자열
+     */
+    private String generateToken(Map claims, int validSeconds, String jti) {
+        return Jwts.builder()
+                .setHeader(Map.of("typ", "JWT"))
+                .setClaims(claims)
+                .setId(jti)
+                .setIssuedAt(new Date())
+                .setExpiration(Date.from(ZonedDateTime.now().plusSeconds(validSeconds).toInstant()))
+                .signWith(key())
+                .compact();
+    }
+
+    // ───────── 발급 & 저장 ─────────
+    /**
+     * Access/Refresh 토큰 발급.
+     * - Access: 반환만, 저장하지 않음.
+     * - Refresh: Redis에 "refresh:{memberId}:{jti}" 키로 저장(TTL=만료).
+     * @param memberId 사용자 ID
+     * @param username 이메일 혹은 학번
+     * @param role     사용자 역할
+     * @param authRealm COMMON / SSU
+     * @return 발급된 토큰 세트
+     */
+    public Tokens issueTokens(Long memberId, String username, UserRole role, String authRealm) {
+        Map baseClaims = new HashMap<>();
+        baseClaims.put("userId", memberId);
+        baseClaims.put("username", username);
+        baseClaims.put("role", role.name());
+        baseClaims.put("authRealm", authRealm);
+
+        String accessJti = UUID.randomUUID().toString();
+        String refreshJti = UUID.randomUUID().toString();
+
+        String accessToken = generateToken(baseClaims, accessValidSeconds, accessJti);
+        String refreshToken = generateToken(baseClaims, refreshValidSeconds, refreshJti);
+
+        String refreshKey = String.format("refresh:%d:%s", memberId, refreshJti);
+        redisTemplate.opsForValue().set(refreshKey, refreshToken, refreshValidSeconds, TimeUnit.SECONDS);
+
+        return Tokens.builder()
+                .accessToken(accessToken)
+                .refreshToken(refreshToken)
+                .build();
+    }
+
+    // ───────── 검증 ─────────
+    /**
+     * Access 토큰 서명/만료 검증.
+     * @param token Access 토큰
+     * @return 유효한 Claims
+     * @throws CustomAuthHandler 만료/서명 오류
+     */
+    public Claims validateToken(String token) {
+        try {
+            return Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(token).getBody();
+        } catch (ExpiredJwtException exception) {
+            throw new CustomAuthHandler(ErrorStatus.JWT_ACCESS_TOKEN_EXPIRED);
+        } catch (Exception exception) {
+            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+        }
+    }
+
+    /**
+     * Access 토큰의 서명만 검증(만료 허용)하여 Claims 추출.
+     * - 재발급 시 사용.
+     * @param token Access 토큰
+     * @return Claims(만료된 토큰도 반환)
+     * @throws CustomAuthHandler 서명 오류
+     */
+    public Claims validateTokenOnlySignature(String token) {
+        try {
+            return Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(token).getBody();
+        } catch (ExpiredJwtException exception) {
+            return exception.getClaims(); // 만료되어도 Claims는 사용
+        } catch (Exception exception) {
+            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+        }
+    }
+
+    /**
+     * Refresh 토큰 서명/만료 검증.
+     * - Redis 저장값과의 매칭은 호출부 정책에 따라 별도로 수행 가능.
+     * @param refreshToken Refresh 토큰
+     * @throws CustomAuthHandler 만료/서명 오류
+     */
+    public void validateRefreshToken(String refreshToken) {
+        try {
+            Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(refreshToken).getBody();
+        } catch (ExpiredJwtException exception) {
+            throw new CustomAuthHandler(ErrorStatus.JWT_REFRESH_TOKEN_EXPIRED);
+        } catch (Exception exception) {
+            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+        }
+    }
+
+    // ───────── Authentication 복원 ─────────
+    /**
+     * 유효한 Access 토큰을 Authentication(CustomPrincipal + 권한)으로 복원.
+     * @param accessToken Access 토큰
+     * @return 인증 객체
+     */
+    public Authentication getAuthentication(String accessToken) {
+        Claims claims = validateToken(accessToken); // 만료/서명 체크
+        Long memberId = ((Number) claims.get("userId")).longValue();
+        String roleName = (String) claims.get("role");
+        String authRealmName = (String) claims.get("authRealm");
+
+        // DB 조회
+        Member member = memberRepository.findById(memberId)
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+
+        // PrincipalDetails 빌드
+        PrincipalDetails principal = PrincipalDetails.builder()
+                .memberId(member.getId())
+                .username(member.getId().toString())
+                .role(UserRole.valueOf(roleName))
+                .authRealm(AuthRealm.valueOf(authRealmName))
+                .member(member)
+                .enabled(member.getIsActivated().equals(ActivationStatus.ACTIVE))
+                .authorities(List.of(new SimpleGrantedAuthority("ROLE_" + roleName)))
+                .build();
+
+        // UsernamePasswordAuthenticationToken 에 PrincipalDetails 세팅
+        return new UsernamePasswordAuthenticationToken(
+                principal, null, principal.getAuthorities()
+        );
+    }
+
+    /**
+     * 만료된 Access 토큰(서명 유효)을 Authentication으로 복원.
+     * - 재발급 시 SecurityContext 세팅용.
+     * @param expiredAccessToken 만료된 Access 토큰
+     * @return 인증 객체
+     */
+    public Authentication getAuthenticationFromExpiredAccessToken(String expiredAccessToken) {
+        Claims claims = validateTokenOnlySignature(expiredAccessToken);
+
+        Long userId = ((Number) claims.get("userId")).longValue();
+        Member member = memberRepository.findById(userId)
+                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_MEMBER));
+
+        UserRole role = UserRole.valueOf((String) claims.get("role"));
+        String authRealmString = (String) claims.get("authRealm");
+
+        List authorities =
+                List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
+
+        String username="";
+        String password="";
+
+        AuthRealm realm = AuthRealm.valueOf(authRealmString);
+        if (realm == AuthRealm.COMMON) {
+            username = member.getCommonAuth().getEmail();
+            password = member.getCommonAuth().getPassword();
+        } else if (realm == AuthRealm.SSU){
+            // 예: 학생 Realm
+            username = member.getSsuAuth().getStudentNumber();
+            password = member.getSsuAuth().getPasswordCipher();
+        }
+
+        // DB에서 조회한 member를 직접 넣어줌
+        PrincipalDetails principal = PrincipalDetails.builder()
+                .member(member)
+                .memberId(member.getId())
+                .username(username)
+                .password(password)
+                .role(role)
+                .authRealm(realm)
+                .authorities(authorities)
+                .build();
+
+        return new UsernamePasswordAuthenticationToken(principal, null, authorities);
+    }
+
+    // ───────────────────────── 블랙리스트(JTI) ─────────────────────────
+
+    /**
+     * Access 토큰이 블랙리스트에 포함되어 있지 않은지 확인.
+     * @param accessToken Access 토큰
+     * @throws CustomAuthHandler 블랙리스트에 포함된 경우
+     */
+    public void assertNotBlacklisted(String accessToken) {
+        Claims claims = validateTokenOnlySignature(accessToken);
+        String jti = claims.getId();
+        Boolean exists = redisTemplate.hasKey("blacklist:" + jti);
+        if (Boolean.TRUE.equals(exists)) {
+            throw new CustomAuthHandler(ErrorStatus.LOGOUT_USER);
+        }
+    }
+
+    /**
+     * Access 토큰을 블랙리스트에 추가(남은 만료 시간만큼 TTL 부여).
+     * @param accessToken Access 토큰
+     */
+    public void blacklistAccess(String accessToken) {
+        Claims claims = validateTokenOnlySignature(accessToken);
+        String jti = claims.getId();
+        long ttlSeconds = Math.max(1, (claims.getExpiration().getTime() - System.currentTimeMillis()) / 1000);
+        redisTemplate.opsForValue().set("blacklist:" + jti, "1", ttlSeconds, TimeUnit.SECONDS);
+    }
+
+    // ───────────────────────── Refresh Token ─────────────────────────
+
+    /**
+     * 특정 회원의 모든 Refresh 토큰을 Redis에서 제거 (전역 로그아웃용).
+     * @param memberId 사용자 ID
+     */
+    public void removeAllRefreshTokens(Long memberId) {
+        String pattern = String.format("refresh:%d:*", memberId);
+        Set refreshKeys = redisTemplate.keys(pattern);
+        if (refreshKeys != null && !refreshKeys.isEmpty()) {
+            redisTemplate.delete(refreshKeys);
+        }
+    }
+
+    /**
+     * Refresh 토큰 유효성 확인 및 회전.
+     * - 저장된 RT와 일치 여부 확인
+     * - 기존 RT 삭제 후 새 토큰 세트 발급
+     */
+    public Tokens rotateRefreshToken(String refreshToken) {
+        // 1) Refresh 토큰 서명/만료 검증
+        validateRefreshToken(refreshToken);
+
+        // 2) Claims 추출
+        Claims refreshClaims = validateTokenOnlySignature(refreshToken);
+        Long memberId = ((Number) refreshClaims.get("userId")).longValue();
+        String username = (String) refreshClaims.get("username");
+        String roleString = (String) refreshClaims.get("role");
+        String authRealm = (String) refreshClaims.get("authRealm");
+        String refreshJti = refreshClaims.getId();
+
+        UserRole role = UserRole.valueOf(roleString);
+
+        // 3) Redis에 저장된 RT와 일치 확인
+        String refreshKey = String.format("refresh:%d:%s", memberId, refreshJti);
+        String savedRefreshToken = redisTemplate.opsForValue().get(refreshKey);
+        if (savedRefreshToken == null || !savedRefreshToken.equals(refreshToken)) {
+            throw new CustomAuthHandler(ErrorStatus.REFRESH_TOKEN_NOT_EQUAL);
+        }
+
+        // 4) 기존 RT 삭제 후 새 토큰 발급
+        redisTemplate.delete(refreshKey);
+        return issueTokens(memberId, username, role, authRealm);
+    }
+
+}

From 7fbdba71325a2a08391dca4572b177b1ac442be5 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:14:15 +0900
Subject: [PATCH 078/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

    - RoutingAuthenticationProvider: Provider 통일
    - LoginUsernamePasswordAuthenticationToken: Token 통일
    - PrincipalDetails 적용
    - SSUAuthAdapter와 CommonAuthAdapter로 Auth 로직 분기
    - AuthRealm으로 Auth 로직 분기
---
 .../auth/security/adapter/RealmAuthAdapter.java   | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java

diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java
new file mode 100644
index 0000000..1f6748d
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/RealmAuthAdapter.java
@@ -0,0 +1,15 @@
+package com.assu.server.domain.auth.security.adapter;
+
+import com.assu.server.domain.auth.entity.AuthRealm;
+import com.assu.server.domain.member.entity.Member;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public interface RealmAuthAdapter {
+    boolean supports(AuthRealm realm);
+    UserDetails loadUserDetails(String identifier);
+    Member loadMember(String identifier);
+    void registerCredentials(Member member, String username, String rawPassword);
+    PasswordEncoder passwordEncoder();
+    String authRealmValue(); // "COMMON" or "SSU"
+}
\ No newline at end of file

From 0b603be529607086d448fb9c3ec66acbaae52be1 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:14:42 +0900
Subject: [PATCH 079/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Member 엔티티에 토큰 저장하지 않도록 수정
---
 .../java/com/assu/server/domain/member/entity/Member.java   | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/member/entity/Member.java b/src/main/java/com/assu/server/domain/member/entity/Member.java
index 5145a5a..5006c12 100644
--- a/src/main/java/com/assu/server/domain/member/entity/Member.java
+++ b/src/main/java/com/assu/server/domain/member/entity/Member.java
@@ -49,12 +49,6 @@ public class Member extends BaseEntity {
     @JdbcTypeCode(SqlTypes.VARCHAR)
     private ActivationStatus isActivated;  // ACTIVE, INACTIVE, SUSPEND
 
-    @Column(nullable = true, unique = true)
-    private String refreshToken;
-
-    @Column(nullable = true, unique = true)
-    private String accessToken;
-
     // 역할별 프로필 - 선택적으로 연관
     @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
     private Student studentProfile;

From 8bacea0a8108f7176ba3da70d059fd9f84c1814e Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 19 Aug 2025 04:33:25 +0900
Subject: [PATCH 080/270] =?UTF-8?q?[FIX/#33]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?=
 =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=94=84?=
 =?UTF-8?q?=EB=A0=88=EC=8B=9C=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- enable principal에 추가
---
 .../java/com/assu/server/domain/auth/security/jwt/JwtUtil.java   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
index 5c86540..31a19d1 100644
--- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
@@ -241,6 +241,7 @@ public Authentication getAuthenticationFromExpiredAccessToken(String expiredAcce
                 .memberId(member.getId())
                 .username(username)
                 .password(password)
+                .enabled(member.getIsActivated().equals(ActivationStatus.ACTIVE))
                 .role(role)
                 .authRealm(realm)
                 .authorities(authorities)

From aeff07d2a671998171532712e2e3c13d2c68b894 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Wed, 20 Aug 2025 18:36:43 +0900
Subject: [PATCH 081/270] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=98=A4=EB=A5=98?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/repository/AdminRepository.java     |  8 +-
 .../domain/admin/service/AdminService.java    |  2 +-
 .../admin/service/AdminServiceImpl.java       |  2 +-
 .../server/domain/auth/entity/SSUAuth.java    |  2 +-
 .../controller/CertificationController.java   |  7 +-
 .../service/CertificationServiceImpl.java     |  2 +-
 .../server/domain/common/entity/Member.java   | 98 -------------------
 .../server/domain/member/entity/Member.java   |  3 +-
 .../service/PaperQueryServiceImpl.java        |  4 +-
 .../server/domain/user/entity/Student.java    |  2 +
 10 files changed, 18 insertions(+), 112 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/common/entity/Member.java

diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
index 2860697..e91b75f 100644
--- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
+++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
@@ -15,8 +15,12 @@
 public interface AdminRepository extends JpaRepository {
 
 	// 여기 예원이 머지하고 수정
-	List findMatchingAdmins(@Param("university") University university,
-		@Param("department") Department department,
+	@Query("SELECT a FROM Admin a WHERE " +
+		"a.name LIKE %:university% OR " +
+		"a.name LIKE %:department% OR " +
+		"a.major = :major")
+	List findMatchingAdmins(@Param("university") String university,
+		@Param("department") String department,
 		@Param("major") Major major);
 
 	Optional findByName(String name);
diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminService.java b/src/main/java/com/assu/server/domain/admin/service/AdminService.java
index 2a18f75..04a7183 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminService.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminService.java
@@ -9,5 +9,5 @@
 
 // PaperQueryServiceImpl 이 AdminService 참조 중 -> 순환참조 문제 발생하지 않도록 주의
 public interface AdminService {
-	List findMatchingAdmins(University university, Department department, Major major);
+	List findMatchingAdmins(String university, String department, Major major);
 }
diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
index 5c15eeb..60eef1a 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
@@ -22,7 +22,7 @@ public class AdminServiceImpl implements AdminService {
 
 	// 유저의 정보와 맞는 admin 을 찾기
 	@Override
-	public List findMatchingAdmins(University university, Department department, Major major){
+	public List findMatchingAdmins(String university, String department, Major major){
 
 
 		List adminList = adminRepository.findMatchingAdmins(university, department,major);
diff --git a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
index fddee5f..76ad400 100644
--- a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
+++ b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
@@ -11,7 +11,7 @@
 @Table(
         name = "ssu_auth",
         indexes = {
-                @Index(name = "ux_ssu_auth_student_id", columnList = "student_id", unique = true)
+                @Index(name = "ux_ssu_auth_student_id", columnList = "student_number", unique = true)
         }
 )
 @Getter
diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
index e8fc072..a9e3437 100644
--- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
+++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
@@ -36,13 +36,12 @@ public class CertificationController {
 	@PostMapping("/certification/session")
 	@Operation(summary = "세션 정보를 요청하는 api", description = "인원 수 기준이 요구되는 제휴일 때 세션을 만들고, 대표자 QR에 담을 정보를 요청하는 api 입니다.")
 	public ResponseEntity> getSessionId(
-		// @AuthenticationPrincipal PrincipalDetails userDetails,
+		@AuthenticationPrincipal PrincipalDetails userDetails,
 		@RequestBody CertificationRequestDTO.groupRequest dto
 	) {
 
-		// Member member = userDetails.getMember();
-		Member member = memberRepository.findMemberById(1L)
-			.orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER));
+		Member member = userDetails.getMember();
+
 		CertificationResponseDTO.getSessionIdResponse result = certificationService.getSessionId(dto, member);
 
 		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_SESSION_CREATE, result));
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 1b19456..e65e948 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
@@ -90,7 +90,7 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto,
 		// 제휴 대상인지 확인하기
 		Long adminId = dto.getAdminId();
 		Student student = member.getStudentProfile();
-		List admins = adminService.findMatchingAdmins(student.getUniversity(), student.getDepartment(), student.getMajor());
+		List admins = adminService.findMatchingAdmins(student.getUniversity().toString(), student.getDepartment().toString(), student.getMajor());
 
 		boolean matched = admins.stream()
 			.anyMatch(admin -> admin.getId().equals(adminId));
diff --git a/src/main/java/com/assu/server/domain/common/entity/Member.java b/src/main/java/com/assu/server/domain/common/entity/Member.java
deleted file mode 100644
index 2ccfba8..0000000
--- a/src/main/java/com/assu/server/domain/common/entity/Member.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.assu.server.domain.common.entity;
-
-import com.assu.server.domain.common.enums.ActivationStatus;
-import com.assu.server.domain.common.enums.UserRole;
-import com.assu.server.domain.admin.entity.Admin;
-import com.assu.server.domain.partner.entity.Partner;
-import com.assu.server.domain.user.entity.Student;
-import jakarta.persistence.*;
-import lombok.Getter;
-
-import java.time.LocalDateTime;
-
-@Getter
-@Entity
-public class Member extends BaseEntity {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private Long id;
-
-    private String phoneNum;
-
-    private Boolean isPhoneVerified;
-
-    private LocalDateTime phoneVerifiedAt;
-
-    private String profileUrl;
-
-    @Enumerated(EnumType.STRING)
-    private UserRole role;  // User, ADMIN, PARTNER
-
-    @Enumerated(EnumType.STRING)
-    private ActivationStatus isActivated;  // ACTIVE, INACTIVE, SUSPEND
-
-    // 역할별 프로필 - 선택적으로 연관
-    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
-    private Student studentProfile;
-
-    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
-    private Admin adminProfile;
-
-    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
-    private Partner partnerProfile;
-
-    // 편의 메서드 및 Builder 등 생략
-
-    public void setPhoneNum(String phoneNum) {
-        this.phoneNum = phoneNum;
-    }
-
-    public void setIsPhoneVerified(Boolean isPhoneVerified) {
-        this.isPhoneVerified = isPhoneVerified;
-    }
-
-    public void setPhoneVerifiedAt(LocalDateTime phoneVerifiedAt) {
-        this.phoneVerifiedAt = phoneVerifiedAt;
-    }
-
-    public void setRole(UserRole role) {
-        this.role = role;
-    }
-
-    public void setIsActivated(ActivationStatus isActivated) {
-        this.isActivated = isActivated;
-    }
-
-
-    // 하드코딩시에만 사용 -> 원격에 올리기 전 주석 처리
-    public void setId(Long id){
-        this.id = id;
-    }
-
-    // 연관관계 편의 메서드
-
-    // public void setStudentProfile(Student studentProfile) {
-    //     this.studentProfile = studentProfile;
-    //     if (studentProfile.getMember() != this) {
-    //         studentProfile.setMember(this);
-    //     }
-    // }
-    //
-    // public void setAdminProfile(Admin adminProfile) {
-    //     this.adminProfile = adminProfile;
-    //     if (adminProfile.getMember() != this) {
-    //         adminProfile.setMember(this);
-    //     }
-    // }
-    //
-    // public void setPartnerProfile(Partner partnerProfile) {
-    //     this.partnerProfile = partnerProfile;
-    //     if (partnerProfile.getMember() != this) {
-    //         partnerProfile.setMember(this);
-    //     }
-    // }
-
-
-}
-
diff --git a/src/main/java/com/assu/server/domain/member/entity/Member.java b/src/main/java/com/assu/server/domain/member/entity/Member.java
index 5145a5a..3e2acda 100644
--- a/src/main/java/com/assu/server/domain/member/entity/Member.java
+++ b/src/main/java/com/assu/server/domain/member/entity/Member.java
@@ -45,8 +45,7 @@ public class Member extends BaseEntity {
     private UserRole role;  // STUDENT, ADMIN, PARTNER
 
     @Enumerated(EnumType.STRING)
-    @Column(name = "is_activated", nullable = false)
-    @JdbcTypeCode(SqlTypes.VARCHAR)
+    @Column(nullable = false)
     private ActivationStatus isActivated;  // ACTIVE, INACTIVE, SUSPEND
 
     @Column(nullable = true, unique = true)
diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java
index 48d9b91..84b88d0 100644
--- a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java
@@ -47,8 +47,8 @@ public PaperResponseDTO.partnershipContent getStorePaperContent(Long storeId, Me
 
 		// 유저의 학교, 단과대, 학부 정보를 조회하여 일치하는 admin을 찾습니다.
 		List adminList = adminService.findMatchingAdmins(
-			student.getUniversity(),
-			student.getDepartment(),
+			student.getUniversity().toString(),
+			student.getDepartment().toString(),
 			student.getMajor());
 
 		// // 한번 더 거르기 위해서
diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java
index 5383db3..8a44c66 100644
--- a/src/main/java/com/assu/server/domain/user/entity/Student.java
+++ b/src/main/java/com/assu/server/domain/user/entity/Student.java
@@ -24,6 +24,7 @@ public class Student {
     @MapsId
     private Member member;
 
+    @Enumerated(EnumType.STRING)
     private Department department;
 
     @Enumerated(EnumType.STRING)
@@ -32,6 +33,7 @@ public class Student {
     @Pattern(regexp = "^[0-9]{1}-[1-2]$", message = "yearSemester는 Y-N 형식이어야 합니다. 예: 3-1")
     private String yearSemester;
 
+    @Enumerated(EnumType.STRING)
     private University university;
 
     private int stamp;

From 00d71bd36ac6e5e4b317c1e16a9b1c9932dbae5e Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Wed, 20 Aug 2025 18:50:53 +0900
Subject: [PATCH 082/270] =?UTF-8?q?refactor/#38=20-=20chatting=20PD=20-=20?=
 =?UTF-8?q?chatting=20api=EC=97=90=EC=84=9C=20memberId=20=ED=98=B8?=
 =?UTF-8?q?=EC=B6=9C=20=EB=B6=80=EB=B6=84=20PD=EB=A1=9C=20=EB=B3=80?=
 =?UTF-8?q?=EA=B2=BD=20-=20Store=20Entity=20=EC=97=B0=EA=B4=80=EA=B4=80?=
 =?UTF-8?q?=EA=B3=84=20=EC=88=98=EC=A0=95=20-=20StoreRepository=20class=20?=
 =?UTF-8?q?->=20interface=20=EC=88=98=EC=A0=95=20-=20=ED=95=84=EC=9A=94?=
 =?UTF-8?q?=ED=95=9C=20ErrorStatus=20=EC=B6=94=EA=B0=80=20-=20=EC=B1=84?=
 =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20api=20request=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/auth/security/JwtAuthFilter.java   |  2 +
 .../chat/controller/ChatController.java       | 47 +++++++++-----
 .../domain/chat/dto/ChatRequestDTO.java       |  2 +-
 .../domain/chat/service/ChatService.java      | 10 +--
 .../domain/chat/service/ChatServiceImpl.java  | 43 ++++++-------
 .../server/domain/store/entity/Store.java     | 14 +----
 .../apiPayload/code/status/ErrorStatus.java   |  9 ++-
 .../server/global/util/PrincipalDetails.java  | 62 +++++++++++++++++++
 8 files changed, 130 insertions(+), 59 deletions(-)
 create mode 100644 src/main/java/com/assu/server/global/util/PrincipalDetails.java

diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
index f2a54e5..a33dac9 100644
--- a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
@@ -63,6 +63,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
 
         final String authHeader = request.getHeader(jwtHeader);
 
+        log.debug("Auth header={}", request.getHeader("Authorization"));
+
         // Refresh 전용 처리
         if (PATH.match("/auth/refresh", request.getRequestURI())) {
             final String refreshToken = request.getHeader("refreshToken");
diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
index 8d38776..3014b6e 100644
--- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
+++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
@@ -4,10 +4,12 @@
 import com.assu.server.domain.chat.dto.ChatResponseDTO;
 import com.assu.server.domain.chat.service.ChatService;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
 import org.springframework.messaging.handler.annotation.MessageMapping;
 import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 import com.assu.server.global.apiPayload.BaseResponse;
 import org.springframework.messaging.simp.SimpMessagingTemplate;
@@ -22,25 +24,31 @@ public class ChatController {
     private final SimpMessagingTemplate simpMessagingTemplate;
 
     @Operation(
-            summary = "채팅방 목록을 조회하는 API 입니다.",
+            summary = "채팅방 목록을 조회하는 API",
             description = "Request Header에 User id를 입력해 주세요."
     )
     @GetMapping("/rooms")
-    public BaseResponse> getChatRoomList() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList());
+    public BaseResponse> getChatRoomList(
+            @AuthenticationPrincipal PrincipalDetails pd
+            ) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList(memberId));
     }
 
     @Operation(
-            summary = "채팅방을 생성하는 API 입니다.",
+            summary = "채팅방을 생성하는 API",
             description = "상대방의 id를 request body에 입력해 주세요"
     )
     @PostMapping("/create/rooms")
-    public BaseResponse createChatRoom(@RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request));
+    public BaseResponse createChatRoom(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request, memberId));
     }
 
     @Operation(
-            summary = "채팅 API 입니다.",
+            summary = "채팅 API.",
             description = "roomId, senderId, message를 입력해 주세요"
     )
     @MessageMapping("/send")
@@ -51,35 +59,44 @@ public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request)
     }
 
     @Operation(
-            summary = "메시지 읽음 처리 API 입니다.",
+            summary = "메시지 읽음 처리 API",
             description = "roomId를 입력해 주세요."
     )
     @PatchMapping("rooms/{roomId}/read")
     public BaseResponse readMessage(
-            @PathVariable Long roomId) {
-        ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId);
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @PathVariable Long roomId
+    ) {
+        Long memberId = pd.getMember().getId();
+        ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
     @Operation(
-            summary = "채팅방 상세 조회 API 입니다.",
+            summary = "채팅방 상세 조회 API",
             description = "roomId를 입력해 주세요."
     )
     @GetMapping("rooms/{roomId}/messages")
-    public BaseResponse getChatHistory(@PathVariable Long roomId) {
-        ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId);
+    public BaseResponse getChatHistory(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @PathVariable Long roomId
+    ) {
+        Long memberId = pd.getMember().getId();
+        ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
     @Operation(
-            summary = "채팅방을 나가는 API 입니다." +
+            summary = "채팅방을 나가는 API" +
                     "참여자가 2명이면 채팅방이 살아있지만, 이미 한 명이 나갔다면 채팅방이 삭제됩니다.",
             description = "roomId를 입력해 주세요."
     )
     @DeleteMapping("rooms/{roomId}/leave")
     public BaseResponse leaveChattingRoom(
+            @AuthenticationPrincipal PrincipalDetails pd,
             @PathVariable Long roomId
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId));
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId, memberId));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
index 90798fc..2123afc 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
@@ -5,7 +5,7 @@
 public class ChatRequestDTO {
     @Getter
     public static class CreateChatRoomRequestDTO {
-        private Long adminId;
+        private Long storeId;
         private Long partnerId;
     }
 
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java
index bc44c56..e11b3c1 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java
@@ -6,10 +6,10 @@
 import java.util.List;
 
 public interface ChatService {
-    List getChatRoomList();
-    ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request);
+    List getChatRoomList(Long memberId);
+    ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId);
     ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request);
-    ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId);
-    ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId);
-    ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId);
+    ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId);
+    ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId);
+    ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index 8b4e1e3..379fec6 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -16,6 +16,8 @@
 import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.store.entity.Store;
+import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
@@ -31,29 +33,33 @@ public class ChatServiceImpl implements ChatService {
     private final PartnerRepository partnerRepository;
     private final AdminRepository adminRepository;
     private final MessageRepository messageRepository;
+    private final StoreRepository storeRepository;
 
 
     @Override
-    public List getChatRoomList() {
-//        Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 1L;
+    public List getChatRoomList(Long memberId) {
 
         List chatRoomList = chatRepository.findChattingRoomsByMemberId(memberId);
         return ChatConverter.toChatRoomListResultDTO(chatRoomList);
     }
 
     @Override
-    public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request) {
-//        Long memberId = SecurityUtil.getCurrentUserId;
-//        Long opponentId = request.getOpponentId();
+    public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId) {
 
-        Long adminId = request.getAdminId();
+        Long storeId = request.getStoreId();
         Long partnerId = request.getPartnerId();
 
-        Admin admin = adminRepository.findById(adminId)
+        Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
         Partner partner = partnerRepository.findById(partnerId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+        Store store = storeRepository.findById(storeId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+
+
+        if (!store.getPartner().getMember().getId().equals(partner.getMember().getId())) {
+            throw new DatabaseException(ErrorStatus.NO_SUCH_STORE_WITH_THAT_PARTNER);
+        }
 
         ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner);
 
@@ -66,9 +72,6 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C
                 admin.getName()
         );
         ChattingRoom savedRoom = chatRepository.save(room);
-
-
-
         return ChatConverter.toCreateChatRoomIdDTO(savedRoom);
     }
 
@@ -90,9 +93,7 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM
 
     @Transactional
     @Override
-    public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) {
-//        Long memberId = SecurityUtil.getCurrentUserId();
-        Long memberId = 2L;
+    public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId) {
 
         List unreadMessages = messageRepository.findUnreadMessagesByRoomAndReceiver(roomId, memberId);
 
@@ -102,24 +103,18 @@ public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) {
     }
 
     @Override
-    public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId) {
-//        Long memberId = SecurityUtil.getCurrentUserId();
-        Long memberId = 1L;
+    public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId) {
 
         ChattingRoom room = chatRepository.findById(roomId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM));
 
-        List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(roomId, memberId);
+        List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(room.getId(), memberId);
 
-        return ChatConverter.toChatHistoryDTO(roomId, allMessages);
+        return ChatConverter.toChatHistoryDTO(room.getId(), allMessages);
     }
 
     @Override
-    public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId) {
-//        Long memberId = SecurityUtil.getCurrentUserId();
-
-        Long memberId = 2L;
-
+    public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId) {
         // 멤버 조회
         Member member = memberRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java
index f808887..c7e5cd5 100644
--- a/src/main/java/com/assu/server/domain/store/entity/Store.java
+++ b/src/main/java/com/assu/server/domain/store/entity/Store.java
@@ -3,15 +3,7 @@
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partner.entity.Partner;
 
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
+import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
@@ -28,7 +20,7 @@ public class Store extends BaseEntity {
 	@GeneratedValue(strategy = GenerationType.IDENTITY)
 	private Long id;
 
-	@ManyToOne(fetch = FetchType.LAZY)
+	@OneToOne(fetch = FetchType.LAZY)
 	@JoinColumn(name = "partner_id")
 	private Partner partner;
 
@@ -39,7 +31,7 @@ public class Store extends BaseEntity {
 
 	private String name;
 
-	private String adderess;
+	private String address;
 
 	private String detailAddress;
 
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index cc95565..f215560 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -35,9 +35,12 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_SUCH_ADMIN(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 admin ID 입니다."),
     NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4003","존재하지 않는 partner ID 입니다."),
     NO_SUCH_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4004","존재하지 않는 student ID 입니다."),
-    EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4005","이미 존재하는 전화번호입니다."),
-    EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."),
-    EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."),
+    NO_SUCH_STORE(HttpStatus.NOT_FOUND,"MEMBER_4005","존재하지 않는 store ID 입니다."),
+    NO_SUCH_STORE_WITH_THAT_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4006","해당 store ID에 해당하는 partner ID가 존재하지 않습니다."),
+    EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 전화번호입니다."),
+    EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4008","이미 존재하는 이메일입니다."),
+    EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4009","이미 존재하는 학번입니다."),
+
 
 
     // 채팅 에러
diff --git a/src/main/java/com/assu/server/global/util/PrincipalDetails.java b/src/main/java/com/assu/server/global/util/PrincipalDetails.java
new file mode 100644
index 0000000..a3c9cff
--- /dev/null
+++ b/src/main/java/com/assu/server/global/util/PrincipalDetails.java
@@ -0,0 +1,62 @@
+package com.assu.server.global.util;
+
+import com.assu.server.domain.member.entity.Member;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RequiredArgsConstructor
+public class PrincipalDetails implements UserDetails {
+
+    private final Member member;
+
+    @Override
+    public Collection getAuthorities() {
+        List roles = new ArrayList<>();
+        roles.add("ROLE_USER");
+
+        return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
+    }
+
+    @Override
+    public String getPassword() {
+        return null;
+    }
+
+    @Override
+    public String getUsername() {
+        return member.getId().toString();
+    }
+
+    public Member getMember() {
+        return member;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return true;
+    }
+}
+
+

From eaa178894b3b79cf2b199d074a7e8ad164a7e07a Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Wed, 20 Aug 2025 18:50:58 +0900
Subject: [PATCH 083/270] =?UTF-8?q?refactor/#38=20-=20chatting=20PD=20-=20?=
 =?UTF-8?q?chatting=20api=EC=97=90=EC=84=9C=20memberId=20=ED=98=B8?=
 =?UTF-8?q?=EC=B6=9C=20=EB=B6=80=EB=B6=84=20PD=EB=A1=9C=20=EB=B3=80?=
 =?UTF-8?q?=EA=B2=BD=20-=20Store=20Entity=20=EC=97=B0=EA=B4=80=EA=B4=80?=
 =?UTF-8?q?=EA=B3=84=20=EC=88=98=EC=A0=95=20-=20StoreRepository=20class=20?=
 =?UTF-8?q?->=20interface=20=EC=88=98=EC=A0=95=20-=20=ED=95=84=EC=9A=94?=
 =?UTF-8?q?=ED=95=9C=20ErrorStatus=20=EC=B6=94=EA=B0=80=20-=20=EC=B1=84?=
 =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EC=83=9D=EC=84=B1=20api=20request=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assu/server/domain/store/repository/StoreRepository.java | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index 5b7f958..fb3611f 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -1,4 +1,7 @@
 package com.assu.server.domain.store.repository;
 
-public class StoreRepository {
+import com.assu.server.domain.store.entity.Store;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface StoreRepository extends JpaRepository {
 }

From 9756451596ffbbbffaa46ecc3547c78077e671d2 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 20 Aug 2025 21:51:44 +1000
Subject: [PATCH 084/270] =?UTF-8?q?[FEAT/#20]=20PrincipalDetails=20?=
 =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/DeviceTokenController.java     | 26 ++++++++++++-------
 .../deviceToken/entity/DeviceToken.java       |  2 +-
 .../service/DeviceTokenService.java           |  2 +-
 .../service/DeviceTokenServiceImpl.java       | 18 ++++++++-----
 .../server/domain/member/entity/Member.java   |  4 ---
 .../controller/NotificationController.java    | 22 +++++++++-------
 .../notification/entity/Notification.java     |  2 +-
 .../entity/NotificationSetting.java           |  2 +-
 .../NotificationCommandServiceImpl.java       |  1 +
 .../service/NotificationQueryServiceImpl.java |  2 +-
 .../apiPayload/code/status/ErrorStatus.java   |  1 +
 .../java/com/assu/server/infra/FcmClient.java |  2 +-
 .../server/infra/NotificationFactory.java     |  2 +-
 13 files changed, 50 insertions(+), 36 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index eff147b..1a632d5 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -4,22 +4,28 @@
 import com.assu.server.domain.deviceToken.service.DeviceTokenService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
+import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("deviceTokens")
 @RequiredArgsConstructor
 public class DeviceTokenController {
+
     private final DeviceTokenService service;
+
     @Operation(
-            summary = "device Token 등록 API",
-            description = "멤버 아이디와 fcm Token을 보내주세요."
+            summary = "Device Token 등록 API",
+            description = "로그인 사용자 기준으로 FCM Device Token을 등록합니다."
     )
     @PostMapping("/register")
-    public BaseResponse register(@RequestBody DeviceTokenRequest req,
-                                         @RequestParam Long memberId) {
+    public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
+                                         @Valid @RequestBody DeviceTokenRequest req) {
+        Long memberId = pd.getMember().getId();
         service.register(req.getToken(), memberId);
         return BaseResponse.onSuccess(
                 SuccessStatus._OK,
@@ -28,15 +34,17 @@ public BaseResponse register(@RequestBody DeviceTokenRequest req,
     }
 
     @Operation(
-            summary = "device Token 등록 해제 API",
-            description = "로그아웃, 회원 탈퇴시 호출하시면 됩니다. 멤버의 tokenId를 보내주세요!"
+            summary = "Device Token 등록 해제 API",
+            description = "로그아웃/탈퇴 시 호출합니다. 자신의 토큰만 해제됩니다."
     )
     @DeleteMapping("/unregister/{tokenId}")
-    public BaseResponse unregister(@PathVariable Long tokenId) {
-        service.unregister(tokenId);
+    public BaseResponse unregister(@AuthenticationPrincipal PrincipalDetails pd,
+                                           @PathVariable Long tokenId) {
+        Long memberId = pd.getMember().getId();
+        service.unregister(tokenId, memberId); // 소유자 검증을 서비스에서 수행하도록 memberId 전달
         return BaseResponse.onSuccess(
                 SuccessStatus._OK,
                 "Device token unregistered successfully. tokenId=" + tokenId
         );
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java
index c908eaf..4d98454 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.deviceToken.entity;
 
-import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.common.entity.BaseEntity;
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
index 254402c..9808ab6 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
@@ -2,5 +2,5 @@
 
 public interface DeviceTokenService {
     void register(String tokenId, Long memberId);
-    void unregister(Long tokenId);
+    void unregister(Long tokenId, Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
index 2defd5c..6135b5d 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
@@ -1,9 +1,9 @@
 package com.assu.server.domain.deviceToken.service;
 
-import com.assu.server.domain.auth.entity.Member;
-import com.assu.server.domain.auth.repository.MemberRepository;
 import com.assu.server.domain.deviceToken.entity.DeviceToken;
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
@@ -32,11 +32,15 @@ public void register(String tokenId, Long memberId) {
 
     @Transactional
     @Override
-    public void unregister(Long tokenId) {
+    public void unregister(Long tokenId, Long memberId) {
         deviceTokenRepository.findById(tokenId)
-                .ifPresentOrElse(
-                        deviceToken -> deviceToken.setActive(false),
-                        () -> { throw new DatabaseException(ErrorStatus.DEVICE_TOKEN_NOT_FOUND); }
-                );
+                .ifPresentOrElse(deviceToken -> {
+                    if (!deviceToken.getMember().getId().equals(memberId)) {
+                        throw new DatabaseException(ErrorStatus.DEVICE_TOKEN_NOT_OWNED);
+                    }
+                    deviceToken.setActive(false);
+                }, () -> {
+                    throw new DatabaseException(ErrorStatus.DEVICE_TOKEN_NOT_FOUND);
+                });
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/member/entity/Member.java b/src/main/java/com/assu/server/domain/member/entity/Member.java
index 5006c12..74e28db 100644
--- a/src/main/java/com/assu/server/domain/member/entity/Member.java
+++ b/src/main/java/com/assu/server/domain/member/entity/Member.java
@@ -59,10 +59,6 @@ public class Member extends BaseEntity {
     @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
     private Partner partnerProfile;
 
-    // 스키마가 BIGINT라서 Long 사용 (필요 시 VARCHAR로 변경)
-    @Column(name = "fcm_token")
-    private Long fcmToken;
-
     // 연관관계 (1:1) — 양방향 필요 없으면 아래 필드 제거해도 됨
     @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
     private SSUAuth ssuAuth;
diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 7ad1c4f..1f8f7b6 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -6,12 +6,14 @@
 import com.assu.server.domain.notification.service.NotificationQueryService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.web.PageableDefault;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.data.domain.Pageable;
 import java.nio.file.AccessDeniedException;
@@ -31,25 +33,27 @@ public class NotificationController {
     )
     @GetMapping
     public BaseResponse> list(
+            @AuthenticationPrincipal PrincipalDetails pd,
             @RequestParam(defaultValue = "all") String status,   // all | unread
             @RequestParam(defaultValue = "1") Integer page,
-            @RequestParam(defaultValue = "20") Integer size,
-            @RequestParam Long memberId
+            @RequestParam(defaultValue = "20") Integer size
     ) {
+        Long memberId = pd.getMember().getId();
         Map body = query.getNotifications(status, page, size, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, body);
     }
 
-
     @Operation(
             summary = "알림 읽음 처리 API",
             description = "알림 아이디를 보내주세요"
     )
     @PostMapping("/{notificationId}/read")
-    public BaseResponse markRead(@PathVariable Long notificationId,
-                         @RequestParam Long memberId) throws AccessDeniedException {
+    public BaseResponse markRead(@AuthenticationPrincipal PrincipalDetails pd,
+                                         @PathVariable Long notificationId) throws AccessDeniedException {
+        Long memberId = pd.getMember().getId();
         command.markRead(notificationId, memberId);
-        return BaseResponse.onSuccess(SuccessStatus._OK,"The notification has been marked as read successfully." + notificationId);
+        return BaseResponse.onSuccess(SuccessStatus._OK,
+                "The notification has been marked as read successfully. id=" + notificationId);
     }
 
     @Operation(
@@ -64,12 +68,12 @@ public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest r
     }
 
     @Operation(summary = "알림 유형별 ON/OFF 토글 API")
-    @PutMapping("/{memberId}/{type}/toggle")
-    public BaseResponse toggle(@PathVariable Long memberId,
+    @PutMapping("/{type}/toggle")
+    public BaseResponse toggle(@AuthenticationPrincipal PrincipalDetails pd,
                                        @PathVariable NotificationType type) {
+        Long memberId = pd.getMember().getId();
         boolean newValue = command.toggle(memberId, type);
         return BaseResponse.onSuccess(SuccessStatus._OK,
                 "Notification setting toggled: now enabled=" + newValue);
     }
-
 }
diff --git a/src/main/java/com/assu/server/domain/notification/entity/Notification.java b/src/main/java/com/assu/server/domain/notification/entity/Notification.java
index de83def..045ac4d 100644
--- a/src/main/java/com/assu/server/domain/notification/entity/Notification.java
+++ b/src/main/java/com/assu/server/domain/notification/entity/Notification.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.notification.entity;
-import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.common.entity.BaseEntity;
 
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
diff --git a/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java b/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
index 00d90b4..aa75cf9 100644
--- a/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
+++ b/src/main/java/com/assu/server/domain/notification/entity/NotificationSetting.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.notification.entity;
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index 92e613c..5d223a4 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.notification.service;
 
 
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.notification.dto.QueueNotificationRequest;
 import com.assu.server.domain.notification.entity.Notification;
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
index 63ee801..f8d9187 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.notification.service;
 
-import com.assu.server.domain.auth.repository.MemberRepository;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.notification.converter.NotificationConverter;
 import com.assu.server.domain.notification.dto.NotificationResponseDTO;
 import com.assu.server.domain.notification.entity.Notification;
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 898d625..4319445 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -57,6 +57,7 @@ public enum ErrorStatus implements BaseErrorCode {
 
     // 디바이스 토큰(DeviceToken) 에러
     DEVICE_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND,"DEVICE_4001","존재하지 않는 Device Token 입니다."),
+    DEVICE_TOKEN_NOT_OWNED(HttpStatus.FORBIDDEN, "DEVICE_4004","해당 토큰은 본인 소유가 아닙니다."),
     DEVICE_TOKEN_REGISTER_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"DEVICE_5001","Device Token 등록에 실패했습니다.")
 
     ;
diff --git a/src/main/java/com/assu/server/infra/FcmClient.java b/src/main/java/com/assu/server/infra/FcmClient.java
index 9e2a68e..c189db8 100644
--- a/src/main/java/com/assu/server/infra/FcmClient.java
+++ b/src/main/java/com/assu/server/infra/FcmClient.java
@@ -1,7 +1,7 @@
 package com.assu.server.infra;
 
-import com.assu.server.domain.auth.entity.Member;
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.notification.entity.Notification;
 import com.google.firebase.messaging.FirebaseMessaging;
 import com.google.firebase.messaging.FirebaseMessagingException;
diff --git a/src/main/java/com/assu/server/infra/NotificationFactory.java b/src/main/java/com/assu/server/infra/NotificationFactory.java
index a7fba66..bfeefa6 100644
--- a/src/main/java/com/assu/server/infra/NotificationFactory.java
+++ b/src/main/java/com/assu/server/infra/NotificationFactory.java
@@ -1,6 +1,6 @@
 package com.assu.server.infra;
 
-import com.assu.server.domain.auth.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.entity.NotificationType;
 import org.springframework.stereotype.Component;

From 7d254b4b7d46ab4213a9b69c869db9c4bf8f7ade Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 20 Aug 2025 22:11:22 +1000
Subject: [PATCH 085/270] =?UTF-8?q?[FEAT/#22]=20pd=20=EC=93=B0=EB=8F=84?=
 =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../inquiry/controller/InquiryController.java | 30 ++++++++++---------
 .../server/domain/inquiry/entity/Inquiry.java |  2 +-
 .../inquiry/repository/InquiryRepository.java |  1 -
 .../inquiry/service/InquiryServiceImpl.java   |  7 +++--
 .../apiPayload/code/status/ErrorStatus.java   |  3 +-
 5 files changed, 22 insertions(+), 21 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index 6e3fc4a..b2317b7 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -6,13 +6,12 @@
 import com.assu.server.domain.inquiry.service.InquiryService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Sort;
-import org.springframework.data.web.PageableDefault;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.Map;
@@ -26,13 +25,14 @@ public class InquiryController {
 
     @Operation(
             summary = "문의를 생성하는 API",
-            description = "셍성 성공시 생성된 문의의 ID를 반환합니다."
+            description = "생성 성공시 생성된 문의의 ID를 반환합니다."
     )
     @PostMapping
     public BaseResponse create(
-            @RequestBody @Valid InquiryCreateRequestDTO req,
-            @RequestParam Long memberId
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @RequestBody @Valid InquiryCreateRequestDTO req
     ) {
+        Long memberId = pd.getMember().getId();
         Long id = inquiryService.create(req, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, id);
     }
@@ -43,30 +43,32 @@ public BaseResponse create(
     )
     @GetMapping
     public BaseResponse> list(
+            @AuthenticationPrincipal PrincipalDetails pd,
             @RequestParam(defaultValue = "all") String status,
             @RequestParam(defaultValue = "1") Integer page,
-            @RequestParam(defaultValue = "20") Integer size,
-            @RequestParam Long memberId
+            @RequestParam(defaultValue = "20") Integer size
     ) {
+        Long memberId = pd.getMember().getId();
         Map response = inquiryService.getInquiries(status, page, size, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
-    /** 단건 상세 조회*/
+    /** 단건 상세 조회 */
     @Operation(
             summary = "문의 단건 상세 조회 API",
             description = "문의 ID를 보내주세요."
     )
     @GetMapping("/{inquiryId}")
     public BaseResponse get(
-            @PathVariable("inquiryId") Long inquiryId,
-            @RequestParam Long memberId
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @PathVariable("inquiryId") Long inquiryId
     ) {
+        Long memberId = pd.getMember().getId();
         InquiryResponseDTO response = inquiryService.get(inquiryId, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
-    /** 문의 답변*/
+    /** 문의 답변 (운영자) */
     @Operation(
             summary = "운영자 답변 API",
             description = "문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다."
@@ -79,4 +81,4 @@ public BaseResponse answer(
         inquiryService.answer(inquiryId, req.getAnswer());
         return BaseResponse.onSuccess(SuccessStatus._OK, null);
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java b/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java
index e32cce4..3f5da96 100644
--- a/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java
+++ b/src/main/java/com/assu/server/domain/inquiry/entity/Inquiry.java
@@ -1,7 +1,7 @@
 package com.assu.server.domain.inquiry.entity;
 
 import com.assu.server.domain.common.entity.BaseEntity;
-import com.assu.server.domain.common.entity.Member;
+import com.assu.server.domain.member.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java b/src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java
index 92d8f02..32d0e87 100644
--- a/src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java
+++ b/src/main/java/com/assu/server/domain/inquiry/repository/InquiryRepository.java
@@ -1,6 +1,5 @@
 package com.assu.server.domain.inquiry.repository;
 
-import com.assu.server.domain.common.entity.Member;
 import com.assu.server.domain.inquiry.entity.Inquiry;
 import com.assu.server.domain.inquiry.entity.Inquiry.Status;
 import org.springframework.data.domain.*;
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
index 17dac4c..e386a36 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java
@@ -1,15 +1,16 @@
 package com.assu.server.domain.inquiry.service;
 
-import com.assu.server.domain.common.entity.Member;
-import com.assu.server.domain.common.repository.MemberRepository;
+
 import com.assu.server.domain.inquiry.converter.InquiryConverter;
 import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO;
 import com.assu.server.domain.inquiry.dto.InquiryResponseDTO;
 import com.assu.server.domain.inquiry.entity.Inquiry;
 import com.assu.server.domain.inquiry.entity.Inquiry.Status;
 import com.assu.server.domain.inquiry.repository.InquiryRepository;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.*;
 import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 08e07b1..fb1d9e4 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -46,8 +46,7 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."),
     NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."),
     NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."),
-    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다.")
-
+    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
 
     // 문의(Inquiry)

From f6f6826e989c19ee06c75e80b6642d6684b7b33f Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 20 Aug 2025 23:58:23 +0900
Subject: [PATCH 086/270] =?UTF-8?q?[FEAT/#33]=20=EC=88=AD=EC=8B=A4?=
 =?UTF-8?q?=EB=8C=80=20SSO=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=ED=8F=AC?=
 =?UTF-8?q?=ED=84=B8=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Webclient 추가
---
 build.gradle                                  |  3 ++
 .../auth/controller/AuthController.java       | 33 ++++++++++++++++---
 .../server/global/config/WebClientConfig.java | 25 ++++++++++++++
 3 files changed, 57 insertions(+), 4 deletions(-)
 create mode 100644 src/main/java/com/assu/server/global/config/WebClientConfig.java

diff --git a/build.gradle b/build.gradle
index 8bbeed4..4ff15e0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -41,6 +41,9 @@ dependencies {
 	implementation 'org.springframework.boot:spring-boot-starter-amqp'
 	testImplementation 'org.springframework.amqp:spring-rabbit-test'
 
+	// webflux; webclient
+	implementation 'org.springframework.boot:spring-boot-starter-webflux:3.4.5'
+
 	// batch
 	implementation 'org.springframework.boot:spring-boot-starter-batch'
 	testImplementation 'org.springframework.batch:spring-batch-test'
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 21354f2..72126b7 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
@@ -9,10 +9,9 @@
 import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequest;
 import com.assu.server.domain.auth.dto.signup.SignUpResponse;
 import com.assu.server.domain.auth.dto.signup.StudentSignUpRequest;
-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.SignUpService;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse;
+import com.assu.server.domain.auth.service.*;
 import com.assu.server.domain.common.enums.UserRole;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
@@ -43,6 +42,7 @@ public class AuthController {
     private final SignUpService signUpService;
     private final LoginService loginService;
     private final LogoutService logoutService;
+    private final SSUAuthService ssuAuthService;
 
     @Operation(
             summary = "휴대폰 인증번호 발송 API (추후 개발)",
@@ -302,4 +302,29 @@ public BaseResponse logout(
         return BaseResponse.onSuccess(SuccessStatus._OK, null);
     }
 
+    // 숭실대 인증 및 개인정보 조회
+    @Operation(
+            summary = "숭실대 유세인트 인증 API",
+            description = "# v1.0 (2025-08-20)\n" +
+                    "- `application/json`으로 호출합니다.\n" +
+                    "- 요청 바디: `USaintAuthRequest(sToken, sIdno)`.\n" +
+                    "- 처리 순서:\n" +
+                    "  1) 유세인트 SSO 로그인 시도 (sToken, sIdno 검증)\n" +
+                    "  2) 응답 Body 검증 후 세션 쿠키 추출\n" +
+                    "  3) 유세인트 포털 페이지 접근 및 HTML 파싱\n" +
+                    "  4) 이름, 학번, 소속, 학적 상태, 학년/학기 정보 추출\n" +
+                    "  5) 소속 문자열을 전공 Enum(`Major`)으로 매핑\n" +
+                    "  6) 인증 결과를 `USaintAuthResponse` DTO로 반환\n"
+    )
+    @io.swagger.v3.oas.annotations.parameters.RequestBody(
+            required = true,
+            content = @Content(schema = @Schema(implementation = USaintAuthRequest.class))
+    )
+    @PostMapping(value = "/schools/ssu", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public BaseResponse ssuAuth(
+            @RequestBody @Valid USaintAuthRequest request
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, ssuAuthService.uSaintAuth(request));
+    }
+
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/global/config/WebClientConfig.java b/src/main/java/com/assu/server/global/config/WebClientConfig.java
new file mode 100644
index 0000000..2aea830
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/WebClientConfig.java
@@ -0,0 +1,25 @@
+package com.assu.server.global.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import org.springframework.web.reactive.function.client.ExchangeStrategies;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Configuration
+public class WebClientConfig {
+
+    @Bean
+    public WebClient webClient(WebClient.Builder builder) {
+        // Body buffer 사이즈 확장 (대용량 응답 대비)
+        ExchangeStrategies strategies = ExchangeStrategies.builder()
+                .codecs(configurer -> configurer
+                        .defaultCodecs()
+                        .maxInMemorySize(10 * 1024 * 1024)) // 10MB
+                .build();
+
+        return builder
+                .exchangeStrategies(strategies)
+                .build();
+    }
+}
\ No newline at end of file

From 62c6acae46c3e4052743bd69a462d34ede1ab0c1 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 20 Aug 2025 23:59:54 +0900
Subject: [PATCH 087/270] =?UTF-8?q?[FEAT/#33]=20=EC=88=AD=EC=8B=A4?=
 =?UTF-8?q?=EB=8C=80=20SSO=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=ED=8F=AC?=
 =?UTF-8?q?=ED=84=B8=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- ErrorStatus 추가
---
 .../server/global/apiPayload/code/status/ErrorStatus.java   | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index cc95565..764e672 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -24,6 +24,12 @@ public enum ErrorStatus implements BaseErrorCode {
     JWT_TOKEN_OUT_OF_FORM(HttpStatus.UNAUTHORIZED, "AUTH4006", "JWT 토큰의 형식이 올바르지 않습니다."),
     REFRESH_TOKEN_NOT_EQUAL(HttpStatus.UNAUTHORIZED, "AUTH4007", "Refreash 토큰이 일치하지 않습니다."),
 
+    // 숭실대 관련 에러
+    SSU_SAINT_SSO_FAILED(HttpStatus.UNAUTHORIZED, "SSU4000", "숭실대학교 유세인트 SSO 로그인에 실패했습니다."),
+    SSU_SAINT_PORTAL_FAILED(HttpStatus.UNAUTHORIZED, "SSU4001", "숭실대학교 유세인트 포털 접근에 실패했습니다."),
+    SSU_SAINT_PARSE_FAILED(HttpStatus.UNAUTHORIZED, "SSU4002", "숭실대학교 유세인트 포털 크롤링 파싱에 실패했습니다."),
+    SSU_SAINT_UNSUPPORTED_MAJOR(HttpStatus.UNAUTHORIZED, "SSU4003", "지원하는 학과가 아닙니다."),
+
     // 인증 에러
     NOT_VERIFIED_PHONE_NUMBER(HttpStatus.BAD_REQUEST,"AUTH_4007","전화번호 인증에 실패했습니다."),
 

From 58117bc6d53da4893ec0ce4eac4a1050eb9646ed Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 21 Aug 2025 00:00:29 +0900
Subject: [PATCH 088/270] =?UTF-8?q?[FEAT/#33]=20=EC=88=AD=EC=8B=A4?=
 =?UTF-8?q?=EB=8C=80=20SSO=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=ED=8F=AC?=
 =?UTF-8?q?=ED=84=B8=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- SSO 인증 및 포털 크롤링 구현
- 숭실대 인증 및 포털 크롤링 API 구현
---
 .../auth/dto/ssu/USaintAuthRequest.java       |  18 ++
 .../auth/dto/ssu/USaintAuthResponse.java      |  17 ++
 .../domain/auth/service/SSUAuthService.java   |   5 +-
 .../auth/service/SSUAuthServiceImpl.java      | 238 +++++++++---------
 4 files changed, 162 insertions(+), 116 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java

diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java
new file mode 100644
index 0000000..b089dfb
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java
@@ -0,0 +1,18 @@
+package com.assu.server.domain.auth.dto.ssu;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.jetbrains.annotations.NotNull;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class USaintAuthRequest {
+    @NotNull
+    private String sToken;
+    @NotNull
+    private Integer sIdno;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java
new file mode 100644
index 0000000..5682de0
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java
@@ -0,0 +1,17 @@
+package com.assu.server.domain.auth.dto.ssu;
+
+import com.assu.server.domain.user.entity.enums.Major;
+import lombok.*;
+
+@Setter
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class USaintAuthResponse {
+    private Integer id;
+    private String name;
+    private String enrollmentStatus;
+    private String yearSemester;
+    private Major major;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java
index d5278b5..99d1098 100644
--- a/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java
+++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthService.java
@@ -1,6 +1,9 @@
 package com.assu.server.domain.auth.service;
 
 
+import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse;
+
 public interface SSUAuthService {
-    // UsaintAuthReturnDto uSaintAuth(UsaintAuthParamDto usaintAuthParamDto) throws APIRequestFailedException, AuthFailedException, HTMLParseFailedException, UnsupportedMajorException;
+    USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest);
 }
diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
index 2cbb056..ef73dac 100644
--- a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
@@ -1,171 +1,179 @@
 package com.assu.server.domain.auth.service;
 
 
+import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.user.entity.enums.Major;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.jetbrains.annotations.NotNull;
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.select.Elements;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
 
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 @Service
 @RequiredArgsConstructor
 @Slf4j
 public class SSUAuthServiceImpl implements SSUAuthService {
-    /*
-    @NotNull
-    @Override
-    public UsaintAuthReturnDto uSaintAuth(@NotNull UsaintAuthParamDto usaintAuthParamDto) throws APIRequestFailedException, AuthFailedException, HTMLParseFailedException, UnsupportedMajorException {
-        String sToken = usaintAuthParamDto.getSToken();
-        Integer sIdno = usaintAuthParamDto.getSIdno();
 
-        // Phase 1 : uSaint SSO
+    private final WebClient webClient;
 
-        String uSaintSSORequestUrl = globalVariable.uSaintSSOUrl + "?sToken=" + sToken + "&sIdno=" + sIdno;
+    private static final String USaintSSOUrl = "https://saint.ssu.ac.kr/webSSO/sso.jsp";
+    private static final String USaintPortalUrl = "https://saint.ssu.ac.kr/webSSUMain/main_student.jsp";
 
-        HashMap uSaintSSORequestHeaders = new HashMap<>();
-        uSaintSSORequestHeaders.put("Cookie", "sToken=" + sToken + "; sIdno=" + sIdno);
+    @Override
+    public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) {
 
-        APIRequestDto uSaintSSORequestDto = APIRequestDto.builder()
-                .headers(uSaintSSORequestHeaders)
-                .url(uSaintSSORequestUrl)
-                .build();
+        String sToken = uSaintAuthRequest.getSToken();
+        Integer sIdno = uSaintAuthRequest.getSIdno();
 
-        APIResponseDto uSaintSSOResponseDto;
+        // 1) SSO 로그인 요청
+        ResponseEntity uSaintSSOResponseEntity;
         try {
-            uSaintSSOResponseDto = apiProvider.get(uSaintSSORequestDto);
-        }
-        catch (Exception e){
+            uSaintSSOResponseEntity = requestUSaintSSO(sToken, sIdno);
+        } catch (Exception e) {
             log.error("API request to uSaint SSO failed.", e);
-            throw new APIRequestFailedException();
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_SSO_FAILED);
         }
 
-        if(!uSaintSSOResponseDto.getBody().contains("location.href = \"/irj/portal\";")){
-            log.error("Student authentication with sToken {} and sIdno {} failed.", sToken, sIdno);
-            throw new AuthFailedException();
+        if (uSaintSSOResponseEntity == null || uSaintSSOResponseEntity.getBody() == null) {
+            log.error("Empty response from USaint SSO. sToken={}, sIdno={}", sToken, sIdno);
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_SSO_FAILED);
         }
 
-        Map> uSaintSSOResponseHeaders = uSaintSSOResponseDto.getHeaders();
-        List setCookieList = uSaintSSOResponseHeaders.get("set-cookie");
-        StringBuilder uSaintPortalCookie = new StringBuilder();
-
-        for(String setCookie : setCookieList){
-            setCookie = setCookie.split(";")[0];
-            uSaintPortalCookie.append(setCookie).append("; ");
+        String body = uSaintSSOResponseEntity.getBody();
+        if (!body.contains("location.href = \"/irj/portal\";")) {
+            log.error("Invalid SSO response. sToken={}, sIdno={}", sToken, sIdno);
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_SSO_FAILED);
         }
 
-        // Phase 2 : uSaint Portal
-
-        String uSaintPortalRequestUrl = globalVariable.uSaintPortalUrl;
-
-        HashMap uSaintPortalRequestHeaders = new HashMap<>();
-        uSaintPortalRequestHeaders.put("Cookie", uSaintPortalCookie.toString());
+        // 쿠키 추출
+        HttpHeaders headers = uSaintSSOResponseEntity.getHeaders();
+        List setCookieList = headers.get(HttpHeaders.SET_COOKIE);
 
-        APIRequestDto uSaintPortalRequestDto = APIRequestDto.builder()
-                .headers(uSaintPortalRequestHeaders)
-                .url(uSaintPortalRequestUrl)
-                .build();
+        StringBuilder uSaintPortalCookie = new StringBuilder();
+        if (setCookieList != null) {
+            for (String setCookie : setCookieList) {
+                setCookie = setCookie.split(";")[0];
+                uSaintPortalCookie.append(setCookie).append("; ");
+            }
+        }
 
-        APIResponseDto uSaintPortalResponseDto;
+        // 2) 포털 접근
+        ResponseEntity portalResponse;
         try {
-            uSaintPortalResponseDto = apiProvider.get(uSaintPortalRequestDto);
-        }
-        catch (Exception e){
+            portalResponse = requestUSaintPortal(uSaintPortalCookie);
+        } catch (Exception e) {
             log.error("API request to uSaint Portal failed.", e);
-            throw new APIRequestFailedException();
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_PORTAL_FAILED);
         }
 
-        String uSaintPortalResponseBody = uSaintPortalResponseDto.getBody();
-        UsaintAuthReturnDto usaintAuthReturnDto = UsaintAuthReturnDto.builder().build();
+        if (portalResponse == null || portalResponse.getBody() == null) {
+            log.error("Empty response from uSaint Portal. cookie={}", uSaintPortalCookie);
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_PORTAL_FAILED);
+        }
 
-        Document uSaintPortalDocument = Jsoup.parse(uSaintPortalResponseBody);
-        Element uSaintPortalNameBox = uSaintPortalDocument.getElementsByClass("main_box09").first();
-        Element uSaintPortalInfoBox = uSaintPortalDocument.getElementsByClass("main_box09_con").first();
-        if(uSaintPortalNameBox == null){
-            log.error("uSaintPortalNameBox is null.");
-            log.debug(uSaintPortalResponseBody);
-            throw new HTMLParseFailedException();
+        String uSaintPortalResponseBody = portalResponse.getBody();
+        USaintAuthResponse usaintAuthResponse = USaintAuthResponse.builder().build();
+
+        // 3) HTML 파싱
+        Document doc;
+        try {
+            doc = Jsoup.parse(uSaintPortalResponseBody);
+        } catch (Exception e) {
+            log.error("Jsoup parsing failed.", e);
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED);
         }
-        if(uSaintPortalInfoBox == null){
-            log.error("uSaintPortalInfoBox is null.");
+
+        Element nameBox = doc.getElementsByClass("main_box09").first();
+        Element infoBox = doc.getElementsByClass("main_box09_con").first();
+
+        if (nameBox == null || infoBox == null) {
+            log.error("Portal HTML structure parsing failed.");
             log.debug(uSaintPortalResponseBody);
-            throw new HTMLParseFailedException();
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED);
         }
 
-        Element uSaintPortalNameBoxSpan = uSaintPortalNameBox.getElementsByTag("span").first();
-        if(uSaintPortalNameBoxSpan == null || uSaintPortalNameBoxSpan.text().equals("")){
-            log.error("uSaintPortalNameBoxSpan is null or empty.");
-            log.debug(uSaintPortalResponseBody);
-            throw new HTMLParseFailedException();
+        // 이름 추출
+        Element span = nameBox.getElementsByTag("span").first();
+        if (span == null || span.text().isEmpty()) {
+            log.error("Student name span not found or empty.");
+            throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED);
         }
-        String studentName = uSaintPortalNameBoxSpan.text();
-        studentName = studentName.split("님")[0];
-        usaintAuthReturnDto.setName(studentName);
-
-        Elements uSaintPortalInfoBoxLis = uSaintPortalInfoBox.getElementsByTag("li");
-
-        for(Element uSaintPortalInfoBoxLi : uSaintPortalInfoBoxLis){
-            Element dt = uSaintPortalInfoBoxLi.getElementsByTag("dt").first();
-            if(dt == null){
-                log.error("dt in uSaintPortalInfoBoxLi is null.");
-                log.debug(uSaintPortalResponseBody);
-                throw new HTMLParseFailedException();
-            }
+        usaintAuthResponse.setName(span.text().split("님")[0]);
+
+        // 학번, 소속, 학적 상태, 학년학기 추출
+        Elements infoLis = infoBox.getElementsByTag("li");
+        for (Element li : infoLis) {
+            Element dt = li.getElementsByTag("dt").first();
+            Element strong = li.getElementsByTag("strong").first();
 
-            Element strong = uSaintPortalInfoBoxLi.getElementsByTag("strong").first();
-            if(strong == null || strong.text().equals("")){
-                log.error("strong in uSaintPortalInfoBoxLi is null or empty.");
-                log.debug(uSaintPortalResponseBody);
-                throw new HTMLParseFailedException();
+            if (dt == null || strong == null || strong.text().isEmpty()) {
+                log.error("Missing dt/strong in infoBox. li={}", li);
+                throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED);
             }
 
-            if(dt.text().equals("학번")){
-                try{
-                    usaintAuthReturnDto.setId(Integer.valueOf(strong.text()));
+            switch (dt.text()) {
+                case "학번" -> {
+                    try {
+                        usaintAuthResponse.setId(Integer.valueOf(strong.text()));
+                    } catch (NumberFormatException e) {
+                        log.error("Invalid studentId format: {}", strong.text());
+                        throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED);
+                    }
                 }
-                catch(NumberFormatException e){
-                    log.error("studentId in strong is not an integer.");
-                    log.debug(uSaintPortalResponseBody);
-                    throw new HTMLParseFailedException();
+                case "소속" -> {
+                    // 원본 문자열 저장
+                    String majorStr = strong.text();
+
+                    // 매핑된 Enum 값 저장
+                    switch (majorStr) {
+                        case "컴퓨터학부" -> usaintAuthResponse.setMajor(Major.COM);
+                        case "소프트웨어학부" -> usaintAuthResponse.setMajor(Major.SW);
+                        case "글로벌미디어학부" -> usaintAuthResponse.setMajor(Major.GM);
+                        case "미디어경영학과" -> usaintAuthResponse.setMajor(Major.MB);
+                        case "AI융합학부" -> usaintAuthResponse.setMajor(Major.AI);
+                        case "전자정보공학부" -> usaintAuthResponse.setMajor(Major.EE);
+                        case "정보보호학과" -> usaintAuthResponse.setMajor(Major.IP);
+                        default -> {
+                            log.debug("{} is not a supported major.", majorStr);
+                            throw new CustomAuthException(ErrorStatus.SSU_SAINT_UNSUPPORTED_MAJOR);
+                        }
+                    }
                 }
+                case "과정/학적" -> usaintAuthResponse.setEnrollmentStatus(strong.text());
+                case "학년/학기" -> usaintAuthResponse.setYearSemester(strong.text());
             }
-            else if(dt.text().equals("소속")){
-                usaintAuthReturnDto.setMajor(strong.text());
-            }
-            else if(dt.text().equals("과정/학적")){
-                usaintAuthReturnDto.setStatus(strong.text());
-            }
-
         }
 
-        if(usaintAuthReturnDto.getMajor().contains("전자정보공학부")){
-            usaintAuthReturnDto.setMajor("infocom");
-            return usaintAuthReturnDto;
-        }
+        return usaintAuthResponse;
+    }
 
-        switch (usaintAuthReturnDto.getMajor()) {
-            case "컴퓨터학부" -> usaintAuthReturnDto.setMajor(Major.COM);
-            case "소프트웨어학부" -> usaintAuthReturnDto.setMajor(Major.SW);
-            case "글로벌미디어학부" -> usaintAuthReturnDto.setMajor(Major.GM);
-            case "미디어경영학과" -> usaintAuthReturnDto.setMajor(Major.MB);
-            case "AI융합학부" -> usaintAuthReturnDto.setMajor(Major.AI);
-            case "전자정보공학부" -> usaintAuthReturnDto.setMajor(Major.EE);
-            case "정보보호학과" -> usaintAuthReturnDto.setMajor(Major.IP);
-            default -> {
-                log.debug("{} is not a supported major.", usaintAuthReturnDto.getMajor());
-                throw new UnsupportedMajorException();
-            }
-        }
+    private ResponseEntity requestUSaintSSO(String sToken, Integer sIdno) {
+        String url = USaintSSOUrl + "?sToken=" + sToken + "&sIdno=" + sIdno;
 
-        return usaintAuthReturnDto;
+        return webClient.get()
+                .uri(url)
+                .header("Cookie", "sToken=" + sToken + "; sIdno=" + sIdno)
+                .retrieve()
+                .toEntity(String.class)   // ResponseEntity 전체 반환 (body + header 포함)
+                .block();                 // 동기 방식
     }
-    */
 
+    private ResponseEntity requestUSaintPortal(StringBuilder cookie) {
+        return webClient.get()
+                .uri(USaintPortalUrl)
+                .header(HttpHeaders.COOKIE, cookie.toString()) // StringBuilder → String 변환
+                .retrieve()
+                .toEntity(String.class)
+                .block();
+    }
 }

From 98ee9ca44bcdd4f313e7de50cffc1c419d6530f0 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 21 Aug 2025 00:00:43 +0900
Subject: [PATCH 089/270] =?UTF-8?q?[FEAT/#33]=20=EC=88=AD=EC=8B=A4?=
 =?UTF-8?q?=EB=8C=80=20SSO=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=ED=8F=AC?=
 =?UTF-8?q?=ED=84=B8=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Major 주석처리
---
 .../com/assu/server/domain/user/entity/enums/Major.java   | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
index e87380f..611757d 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
@@ -1,5 +1,11 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum Major {
-    SW, GM, COM, EE, IP, AI, MB
+    SW, // 소프트웨어학부
+    GM, // 글로벌미디어학과
+    COM, // 컴퓨터학부
+    EE, // 전자정보공학부
+    IP, // 정보보호학과
+    AI, // AI융합학과
+    MB // 미디어경영학과
 }

From a30fd1f1388945317fa0acc0c5305975914b45cd Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Thu, 21 Aug 2025 17:40:20 +0900
Subject: [PATCH 090/270] =?UTF-8?q?[Feat/#24]=20=20-=20=EC=9C=84=EC=B9=98?=
 =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=20-=20=EC=A3=BC=EB=B3=80=20=EC=9E=A5?=
 =?UTF-8?q?=EC=86=8C=20=EA=B2=80=EC=83=89=20=20-=20=EA=B2=80=EC=83=89?=
 =?UTF-8?q?=EC=96=B4=20=EA=B8=B0=EB=B0=98=20=EC=9E=A5=EC=86=8C=20=EA=B2=80?=
 =?UTF-8?q?=EC=83=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |   5 +
 .../domain/map/controller/MapController.java  |  98 +++++
 .../domain/map/converter/MapConverter.java    |  19 +
 .../server/domain/map/dto/MapRequestDTO.java  |  21 +
 .../server/domain/map/dto/MapResponseDTO.java |  83 ++++
 .../server/domain/map/entity/Location.java    |  31 ++
 .../map/entity/enums/LocationOwnerType.java   |   5 +
 .../domain/map/repository/MapRepository.java  |  33 ++
 .../server/domain/map/service/MapService.java |  21 +
 .../domain/map/service/MapServiceImpl.java    | 370 ++++++++++++++++++
 .../domain/partnership/entity/Paper.java      |  20 +-
 .../partnership/entity/PaperContent.java      |  35 +-
 ...perContentType.java => CriterionType.java} |   6 +-
 .../partnership/entity/enums/OptionType.java  |   5 +
 .../repository/PaperContentRepository.java    |  11 +
 .../repository/PaperRepository.java           |  47 +++
 .../repository/PatnershipRepository.java      |   4 -
 .../server/domain/store/entity/Store.java     |   2 +-
 .../store/repository/StoreRepository.java     |   8 +-
 .../apiPayload/code/status/ErrorStatus.java   |   8 +
 .../global/config/JpaSpatialConfig.java       |  15 +
 .../global/config/KakaoLocalClient.java       |  94 +++++
 .../global/config/KakaoWebClientConfig.java   |  22 ++
 .../server/global/config/SecurityConfig.java  |  34 +-
 src/main/resources/application.yml            |   6 +-
 25 files changed, 940 insertions(+), 63 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/map/controller/MapController.java
 create mode 100644 src/main/java/com/assu/server/domain/map/converter/MapConverter.java
 create mode 100644 src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/map/entity/Location.java
 create mode 100644 src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java
 create mode 100644 src/main/java/com/assu/server/domain/map/repository/MapRepository.java
 create mode 100644 src/main/java/com/assu/server/domain/map/service/MapService.java
 create mode 100644 src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
 rename src/main/java/com/assu/server/domain/partnership/entity/enums/{PaperContentType.java => CriterionType.java} (50%)
 create mode 100644 src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java
 create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
 create mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
 delete mode 100644 src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java
 create mode 100644 src/main/java/com/assu/server/global/config/JpaSpatialConfig.java
 create mode 100644 src/main/java/com/assu/server/global/config/KakaoLocalClient.java
 create mode 100644 src/main/java/com/assu/server/global/config/KakaoWebClientConfig.java

diff --git a/build.gradle b/build.gradle
index b75bd14..9e7ff72 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,6 +29,7 @@ dependencies {
 	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
 	implementation 'org.springframework.boot:spring-boot-starter-validation'
 	implementation 'org.springframework.boot:spring-boot-starter-web'
+	implementation 'org.springframework.boot:spring-boot-starter-webflux'
 
 	// spring security
 	implementation 'org.springframework.boot:spring-boot-starter-security'
@@ -66,6 +67,10 @@ dependencies {
 
     implementation group: 'org.javassist', name: 'javassist', version: '3.15.0-GA'
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+
+	// 공간 데이터
+	implementation("org.hibernate.orm:hibernate-spatial")
+	implementation("org.locationtech.jts:jts-core:1.19.0")
 }
 
 tasks.named('test') {
diff --git a/src/main/java/com/assu/server/domain/map/controller/MapController.java b/src/main/java/com/assu/server/domain/map/controller/MapController.java
new file mode 100644
index 0000000..4ea1ef1
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/controller/MapController.java
@@ -0,0 +1,98 @@
+package com.assu.server.domain.map.controller;
+
+import com.assu.server.domain.map.dto.MapRequestDTO;
+import com.assu.server.domain.map.dto.MapResponseDTO;
+import com.assu.server.domain.map.service.MapService;
+import com.assu.server.global.apiPayload.BaseResponse;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/map")
+public class MapController {
+
+    private final MapService mapService;
+
+    @Operation(
+            summary = "관리자 위치 및 정보를 저장하는 API 입니다.",
+            description = "로그인된 관리자 프로필의 주소를 사용해 위치를 저장/갱신합니다."
+    )
+    @PostMapping("/locations/admin")
+    public BaseResponse saveAdminPin() {
+        return BaseResponse.onSuccess(SuccessStatus._OK, mapService.saveAdminPin());
+    }
+
+    @Operation(
+            summary = "파트너 위치 및 정보를 저장하는 API 입니다.",
+            description = "로그인된 파트너 프로필의 주소를 사용해 위치를 저장/갱신합니다."
+    )
+    @PostMapping("/locations/partner")
+    public BaseResponse savePartnerPin() {
+        return BaseResponse.onSuccess(SuccessStatus._OK, mapService.savePartnerPin());
+    }
+
+    @Operation(
+            summary = "가게 위치 및 정보를 저장하는 API 입니다.",
+            description = "storeId로 스토어를 조회하고 그 주소로 위치를 저장/갱신합니다."
+    )
+    @PostMapping("/locations/store/{storeId}")
+    public BaseResponse saveStorePin(
+            @PathVariable Long storeId
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, mapService.saveStorePin(storeId));
+    }
+
+    @Operation(
+            summary = "주변 장소를 조회하는 API 입니다.",
+            description = "유저의 타입과 공간 인덱싱에 들어갈 좌표 4개를 경도, 위도 순서로 입력해주세요 (type=user -> store 조회 / type=admin -> partner 조회 / type=partner -> admin 조회)"
+    )
+    @GetMapping("/nearby")
+    public BaseResponse getLocations(
+            @RequestParam("type") String type,
+            @ModelAttribute MapRequestDTO.ViewOnMapDTO viewport
+    ) {
+        String t = type.trim().toLowerCase();
+
+        return switch (t) {
+            case "user" -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getStores(viewport));
+            case "admin" -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getPartners(viewport));
+            case "partner" -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getAdmins(viewport));
+            default -> BaseResponse.onFailure(ErrorStatus._BAD_REQUEST, null);
+        };
+    }
+
+    @Operation(
+            summary = "검색어 기반 장소 조회 API 입니다.",
+            description = "유저의 타입과 검색어를 입력해주세요 (type=user → STORE 전체조회 / type=admin → 제휴중인 PARTNER 조회 / type=partner → 제휴중인 ADMIN 조회)"
+    )
+    @GetMapping("/search")
+    public BaseResponse search(
+            @RequestParam("type") String type,
+            @RequestParam("q") @NotNull String keyword
+    ) {
+        String t = type.trim().toLowerCase();
+        return switch (t) {
+            case "user" -> {
+                List list = mapService.searchStores(keyword);
+                yield BaseResponse.onSuccess(SuccessStatus._OK, list);
+            }
+            case "admin" -> {
+                List list = mapService.searchPartner(keyword);
+                yield BaseResponse.onSuccess(SuccessStatus._OK, list);
+            }
+            case "partner" -> {
+                List list = mapService.searchAdmin(keyword);
+                yield BaseResponse.onSuccess(SuccessStatus._OK, list);
+            }
+            default -> BaseResponse.onFailure(ErrorStatus._BAD_REQUEST, null);
+        };
+    }
+
+}
diff --git a/src/main/java/com/assu/server/domain/map/converter/MapConverter.java b/src/main/java/com/assu/server/domain/map/converter/MapConverter.java
new file mode 100644
index 0000000..971b053
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/converter/MapConverter.java
@@ -0,0 +1,19 @@
+package com.assu.server.domain.map.converter;
+
+import com.assu.server.domain.map.dto.MapResponseDTO;
+import com.assu.server.domain.map.entity.Location;
+
+public class MapConverter {
+
+    public static MapResponseDTO.SavePinResponseDTO toSavePinResponseDTO(Location location) {
+        return MapResponseDTO.SavePinResponseDTO.builder()
+                .pinId(location.getId())
+                .ownerType(location.getOwnerType().name())
+                .ownerId(location.getOwnerId())
+                .name(location.getName())
+                .address(location.getRoadAddress() != null ? location.getRoadAddress() : location.getAddress())
+                .latitude(location.getLatitude())
+                .longitude(location.getLongitude())
+                .build();
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java
new file mode 100644
index 0000000..4121ea8
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java
@@ -0,0 +1,21 @@
+package com.assu.server.domain.map.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+
+public class MapRequestDTO {
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    public static class ViewOnMapDTO {
+        private double lng1;
+        private double lat1;
+        private double lng2;
+        private double lat2;
+        private double lng3;
+        private double lat3;
+        private double lng4;
+        private double lat4;
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
new file mode 100644
index 0000000..03d656f
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
@@ -0,0 +1,83 @@
+package com.assu.server.domain.map.dto;
+
+import com.assu.server.domain.partnership.entity.enums.CriterionType;
+import com.assu.server.domain.partnership.entity.enums.OptionType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+
+public class MapResponseDTO {
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class PartnerMapResponseDTO {
+        private Long pinId;
+        private Long partnerId;
+        private String name;
+        private String address;
+        private boolean isPartnered;
+        private Long partnershipId;
+        private LocalDate partnershipStartDate;
+        private LocalDate partnershipEndDate;
+        private Double latitude;
+        private Double longitude;
+    }
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class AdminMapResponseDTO {
+        private Long pinId;
+        private Long adminId;
+        private String name;
+        private String address;
+        private boolean isPartnered;
+        private Long partnershipId;
+        private LocalDate partnershipStartDate;
+        private LocalDate partnershipEndDate;
+        private Double latitude;
+        private Double longitude;
+    }
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class StoreMapResponseDTO {
+        private Long pinId;
+        private Long storeId;
+        private Long adminId;
+        private String name;
+        private String address;
+        private Integer rate;
+        private CriterionType criterionType;
+        private OptionType optionType;
+        private Integer people;
+        private Long cost;
+        private String category;
+        private Long discountRate;
+        private boolean hasPartner;
+        private Double latitude;
+        private Double longitude;
+    }
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class SavePinResponseDTO {
+        private Long pinId;
+        private String ownerType;   // ADMIN / PARTNER / STORE
+        private Long ownerId;
+        private String name;
+        private String address;
+        private Double latitude;
+        private Double longitude;
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/map/entity/Location.java b/src/main/java/com/assu/server/domain/map/entity/Location.java
new file mode 100644
index 0000000..4712cc3
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/entity/Location.java
@@ -0,0 +1,31 @@
+package com.assu.server.domain.map.entity;
+
+import com.assu.server.domain.common.entity.BaseEntity;
+import com.assu.server.domain.map.entity.enums.LocationOwnerType;
+import jakarta.persistence.*;
+import lombok.*;
+import org.locationtech.jts.geom.Point;
+
+@Entity
+@Getter @Setter @Builder
+@NoArgsConstructor @AllArgsConstructor
+public class Location extends BaseEntity {
+
+    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long Id;
+
+    @Enumerated(EnumType.STRING)
+    private LocationOwnerType ownerType;
+
+    private Long ownerId;
+
+    private String name;
+    private String address;
+    private String roadAddress;
+
+    private Double latitude;
+    private Double longitude;
+
+    @Column(columnDefinition = "POINT SRID 4326")
+    private Point point;
+}
diff --git a/src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java b/src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java
new file mode 100644
index 0000000..9a15314
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java
@@ -0,0 +1,5 @@
+package com.assu.server.domain.map.entity.enums;
+
+public enum LocationOwnerType {
+    ADMIN, PARTNER, STORE
+}
diff --git a/src/main/java/com/assu/server/domain/map/repository/MapRepository.java b/src/main/java/com/assu/server/domain/map/repository/MapRepository.java
new file mode 100644
index 0000000..e5527e6
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/repository/MapRepository.java
@@ -0,0 +1,33 @@
+package com.assu.server.domain.map.repository;
+
+import com.assu.server.domain.map.entity.Location;
+import com.assu.server.domain.map.entity.enums.LocationOwnerType;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+public interface MapRepository extends JpaRepository {
+
+    Optional findByOwnerTypeAndOwnerId(LocationOwnerType ownerType, Long ownerId);
+
+    List findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType ownerType, Collection ownerIds);
+
+    @Query(value = """
+        SELECT l.*
+        FROM location l
+        WHERE l.owner_type = :ownerType
+            AND l.point IS NOT NULL
+            AND ST_Contains(
+                ST_GeomFromText(:wkt, 4326),
+                l.point
+            )
+    """, nativeQuery = true)
+    List findAllByCoordinates(
+            @Param("ownerType") String ownerType,
+            @Param("wkt") String wkt
+    );
+}
diff --git a/src/main/java/com/assu/server/domain/map/service/MapService.java b/src/main/java/com/assu/server/domain/map/service/MapService.java
new file mode 100644
index 0000000..61f10c3
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/service/MapService.java
@@ -0,0 +1,21 @@
+package com.assu.server.domain.map.service;
+
+import com.assu.server.domain.map.dto.MapRequestDTO;
+import com.assu.server.domain.map.dto.MapResponseDTO;
+import com.assu.server.domain.map.entity.Location;
+
+import java.util.List;
+
+public interface MapService {
+    MapResponseDTO.SavePinResponseDTO saveAdminPin();
+    MapResponseDTO.SavePinResponseDTO savePartnerPin();
+    MapResponseDTO.SavePinResponseDTO saveStorePin(Long storeId);
+
+    List getAdmins(MapRequestDTO.ViewOnMapDTO viewport);
+    List getPartners(MapRequestDTO.ViewOnMapDTO viewport);
+    List getStores(MapRequestDTO.ViewOnMapDTO viewport);
+
+    List   searchStores(String keyword);
+    List searchPartner(String keyword);
+    List   searchAdmin(String keyword);
+}
diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
new file mode 100644
index 0000000..52740a4
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -0,0 +1,370 @@
+package com.assu.server.domain.map.service;
+
+import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.admin.repository.AdminRepository;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.map.converter.MapConverter;
+import com.assu.server.domain.map.dto.MapRequestDTO;
+import com.assu.server.domain.map.dto.MapResponseDTO;
+import com.assu.server.domain.map.entity.Location;
+import com.assu.server.domain.map.entity.enums.LocationOwnerType;
+import com.assu.server.domain.map.repository.MapRepository;
+import com.assu.server.domain.partner.entity.Partner;
+import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.partnership.entity.Paper;
+import com.assu.server.domain.partnership.entity.PaperContent;
+import com.assu.server.domain.partnership.repository.PaperContentRepository;
+import com.assu.server.domain.partnership.repository.PaperRepository;
+import com.assu.server.domain.store.entity.Store;
+import com.assu.server.domain.store.repository.StoreRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.config.KakaoLocalClient;
+import com.assu.server.global.exception.exception.DatabaseException;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.springframework.stereotype.Service;
+
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class MapServiceImpl implements MapService {
+
+    private final MapRepository mapRepository;
+    private final AdminRepository adminRepository;
+    private final PartnerRepository partnerRepository;
+    private final StoreRepository storeRepository;
+    private final KakaoLocalClient kakaoLocalClient;
+    private final PaperContentRepository paperContentRepository;
+    private final PaperRepository paperRepository;
+    private final GeometryFactory geometryFactory;
+
+    @Override
+    @Transactional
+    public MapResponseDTO.SavePinResponseDTO saveAdminPin() {
+//        Long adminId = SecurityUtil.getCurrentId();
+        Long adminId = 1L;
+
+        Admin admin = adminRepository.findById(adminId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+
+        String query = joinAddress(admin.getOfficeAddress(), admin.getDetailAddress());
+        if (query.isBlank()) throw new IllegalArgumentException("관리자 주소가 비어 있습니다.");
+
+        var geo = kakaoLocalClient.geocodeByAddress(query);
+        Location loc = upsert(LocationOwnerType.ADMIN, admin.getId(), admin.getName(), query, geo.getRoadAddress(), geo.getLat(), geo.getLng());
+
+        return MapConverter.toSavePinResponseDTO(loc);
+    }
+
+    @Override
+    @Transactional
+    public MapResponseDTO.SavePinResponseDTO savePartnerPin() {
+        //        Long partnerId = SecurityUtil.getCurrentId();
+        Long partnerId = 2L;
+
+        Partner partner = partnerRepository.findById(partnerId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+
+        String query = joinAddress(partner.getAddress(), partner.getDetailAddress());
+        if (query.isBlank()) throw new IllegalArgumentException("파트너 주소가 비어 있습니다.");
+
+        var geo = kakaoLocalClient.geocodeByAddress(query);
+        Location loc = upsert(LocationOwnerType.PARTNER, partner.getId(), partner.getName(), query, geo.getRoadAddress(), geo.getLat(), geo.getLng());
+
+        return MapConverter.toSavePinResponseDTO(loc);
+    }
+
+    @Override
+    @Transactional
+    public MapResponseDTO.SavePinResponseDTO saveStorePin(Long storeId) {
+        Store store = storeRepository.findById(storeId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+
+        String query = joinAddress(store.getAddress(), store.getDetailAddress());
+        if (query.isBlank()) throw new IllegalArgumentException("스토어 주소가 비어 있습니다.");
+
+        var geo = kakaoLocalClient.geocodeByAddress(query);
+        Location loc = upsert(LocationOwnerType.STORE, store.getId(), store.getName(), query, geo.getRoadAddress(), geo.getLat(), geo.getLng());
+
+        return MapConverter.toSavePinResponseDTO(loc);
+    }
+
+    @Override
+    public List getPartners(MapRequestDTO.ViewOnMapDTO viewport) {
+//        Long adminId = SecurityUtil.getCurrentId();
+        Long adminId = 1L;
+        Admin admin = adminRepository.findById(adminId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+        String wkt = toWKT(viewport);
+
+        List pins = mapRepository.findAllByCoordinates(LocationOwnerType.PARTNER.name(), wkt);
+
+        return pins.stream().map(pin -> {
+            Long partnerId = pin.getOwnerId();
+            Partner partner = partnerRepository.findById(partnerId)
+                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+
+            Paper activePaper = paperRepository
+                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(admin.getId(), partnerId, ActivationStatus.ACTIVE)
+                    .orElse(null);
+
+            boolean isPartnered = (activePaper != null);
+            Long partnershipId = (activePaper != null ? activePaper.getId() : null);
+            var start = (activePaper != null ? activePaper.getPartnershipPeriodStart() : null);
+            var end = (activePaper != null ? activePaper.getPartnershipPeriodEnd() : null);
+
+            return MapResponseDTO.PartnerMapResponseDTO.builder()
+                    .pinId(pin.getId())
+                    .partnerId(partnerId)
+                    .name(partner != null ? partner.getName() : pin.getName())
+                    .address(pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress())
+                    .isPartnered(isPartnered)
+                    .partnershipId(partnershipId)
+                    .partnershipStartDate(start)
+                    .partnershipEndDate(end)
+                    .latitude(pin.getLatitude())
+                    .longitude(pin.getLongitude())
+                    .build();
+        }).toList();
+    }
+
+    @Override
+    public List getAdmins(MapRequestDTO.ViewOnMapDTO viewport) {
+//        Long partnerId = SecurityUtil.getCurrentId();
+        Long partnerId = 2L;
+
+        Partner partner = partnerRepository.findById(partnerId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+        String wkt = toWKT(viewport);
+
+        List pins = mapRepository.findAllByCoordinates(LocationOwnerType.ADMIN.name(), wkt);
+
+        return pins.stream().map(pin -> {
+            Long adminId = pin.getOwnerId();
+            Admin admin = adminRepository.findById(adminId)
+                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+
+            Paper activePaper = paperRepository
+                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(adminId, partner.getId(), ActivationStatus.ACTIVE)
+                    .orElse(null);
+
+            boolean isPartnered = (activePaper != null);
+            Long partnershipId = (activePaper != null ? activePaper.getId() : null);
+            var start = (activePaper != null ? activePaper.getPartnershipPeriodStart() : null);
+            var end = (activePaper != null ? activePaper.getPartnershipPeriodEnd() : null);
+
+            return MapResponseDTO.AdminMapResponseDTO.builder()
+                    .pinId(pin.getId())
+                    .adminId(adminId)
+                    .name(admin != null ? admin.getName() : pin.getName())
+                    .address(pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress())
+                    .isPartnered(isPartnered)
+                    .partnershipId(partnershipId)
+                    .partnershipStartDate(start)
+                    .partnershipEndDate(end)
+                    .latitude(pin.getLatitude())
+                    .longitude(pin.getLongitude())
+                    .build();
+        }).toList();
+    }
+
+    @Override
+    public List getStores(MapRequestDTO.ViewOnMapDTO viewport) {
+        String wkt = toWKT(viewport);
+
+        List pins = mapRepository.findAllByCoordinates(LocationOwnerType.STORE.name(), wkt);
+
+        return pins.stream().map(pin -> {
+            Long storeId = pin.getOwnerId();
+            Store store = storeRepository.findById(storeId)
+                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+
+            boolean hasPartner = store.getPartner() != null;
+
+            PaperContent content = paperContentRepository
+                    .findTopByPaperStoreIdOrderByIdDesc(storeId)
+                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_CONTENT));
+
+            Long adminId = paperRepository.findTopPaperByStoreId(storeId)
+                    .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
+                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+
+            return MapResponseDTO.StoreMapResponseDTO.builder()
+                    .pinId(pin.getId())
+                    .storeId(storeId)
+                    .adminId(adminId)
+                    .name(store.getName())
+                    .address(pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress())
+                    .rate(store.getRate())
+                    .criterionType(content != null ? content.getCriterionType() : null)
+                    .optionType(content != null ? content.getOptionType() : null)
+                    .people(content != null ? content.getPeople() : null)
+                    .cost(content != null ? content.getCost() : null)
+                    .category(content != null ? content.getCategory() : null)
+                    .discountRate(content != null ? content.getDiscount() : null)
+                    .hasPartner(hasPartner)
+                    .latitude(pin.getLatitude())
+                    .longitude(pin.getLongitude())
+                    .build();
+        }).toList();
+    }
+
+    @Override
+    public List searchStores(String keyword) {
+        List stores = storeRepository.findByNameContainingIgnoreCaseOrderByIdDesc(keyword);
+
+        Map locationMap = mapRepository
+                .findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType.STORE, stores.stream().map(Store::getId).toList())
+                .stream().collect(Collectors.toMap(Location::getOwnerId, Function.identity()));
+
+        List result =  new ArrayList<>();
+        for(Store s : stores) {
+            Location pin = locationMap.get(s.getId());
+
+            Paper latest = paperRepository.findTopPaperByStoreId(s.getId()).orElse(null);
+            Long adminId = (latest != null && latest.getAdmin() != null) ? latest.getAdmin().getId() : null;
+
+            PaperContent content = paperContentRepository
+                    .findTopByPaperStoreIdOrderByIdDesc(s.getId())
+                    .orElse(null);
+
+            result.add(MapResponseDTO.StoreMapResponseDTO.builder()
+                    .pinId(pin != null ? pin.getId() : null)
+                    .storeId(s.getId())
+                    .adminId(adminId)
+                    .name(s.getName())
+                    .address(pin != null
+                            ? (pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress()) : null)
+                    .rate(s.getRate())
+                    .criterionType(content != null ? content.getCriterionType() : null)
+                    .optionType(content != null ? content.getOptionType() : null)
+                    .people(content != null ? content.getPeople() : null)
+                    .cost(content != null ? content.getCost() : null)
+                    .category(content != null ? content.getCategory() : null)
+                    .discountRate(content != null ? content.getDiscount() : null)
+                    .hasPartner(s.getPartner() != null)
+                    .latitude(pin != null ? pin.getLatitude() : null)
+                    .longitude(pin != null ? pin.getLongitude() : null)
+                    .build());
+        }
+        return result;
+    }
+
+    @Override
+    public List searchPartner(String keyword) {
+//        Long adminId = SecurityUtil.getCurrentId();
+        Long adminId = 1L;
+
+        List partners = paperRepository
+                .findActivePartnersForAdminByKeyword(adminId, ActivationStatus.ACTIVE, keyword);
+
+        Map locationMap = mapRepository
+                .findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType.PARTNER,
+                        partners.stream().map(Partner::getId).toList())
+                .stream().collect(Collectors.toMap(Location::getOwnerId, Function.identity()));
+
+        List result =  new ArrayList<>();
+        for(Partner p : partners) {
+            Location pin = locationMap.get(p.getId());
+            Paper active = paperRepository
+                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(adminId, p.getId(), ActivationStatus.ACTIVE)
+                    .orElse(null);
+
+            result.add(MapResponseDTO.PartnerMapResponseDTO.builder()
+                            .pinId(pin != null ? pin.getId() : null)
+                            .partnerId(p.getId())
+                            .name(p.getName())
+                            .address(pin != null && pin.getRoadAddress() != null ? pin.getRoadAddress() : (pin != null ? pin.getAddress() : null))
+                            .isPartnered(active != null)
+                            .partnershipId(active != null ? active.getId() : null)
+                            .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
+                            .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
+                            .latitude(pin != null ? pin.getLatitude() : null)
+                            .longitude(pin != null ? pin.getLongitude() : null)
+                    .build());
+        }
+        return result;
+    }
+
+    @Override
+    public List searchAdmin(String keyword) {
+//        Long partnerId = SecurityUtil.getCurrentId();
+        Long partnerId = 2L;
+
+        List admins = paperRepository
+                .findActiveAdminsForPartnerByKeyword(partnerId, ActivationStatus.ACTIVE, keyword);
+
+        Map locMap = mapRepository
+                .findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType.ADMIN,
+                        admins.stream().map(Admin::getId).toList())
+                .stream().collect(Collectors.toMap(Location::getOwnerId, Function.identity()));
+
+        List result = new ArrayList<>();
+        for (Admin a : admins) {
+            Location pin = locMap.get(a.getId());
+            Paper active = paperRepository
+                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(
+                            a.getId(), partnerId, ActivationStatus.ACTIVE)
+                    .orElse(null);
+
+            result.add(MapResponseDTO.AdminMapResponseDTO.builder()
+                    .pinId(pin != null ? pin.getId() : null)
+                    .adminId(a.getId())
+                    .name(a.getName())
+                    .address(pin != null && pin.getRoadAddress() != null ? pin.getRoadAddress()
+                            : (pin != null ? pin.getAddress() : null))
+                    .isPartnered(active != null)
+                    .partnershipId(active != null ? active.getId() : null)
+                    .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
+                    .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
+                    .latitude(pin != null ? pin.getLatitude() : null)
+                    .longitude(pin != null ? pin.getLongitude() : null)
+                    .build());
+        }
+        return result;
+    }
+
+    private String toWKT(MapRequestDTO.ViewOnMapDTO v) {
+        return String.format(
+                "POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))",
+                v.getLng1(), v.getLat1(),
+                v.getLng2(), v.getLat2(),
+                v.getLng3(), v.getLat3(),
+                v.getLng4(), v.getLat4(),
+                v.getLng1(), v.getLat1()
+        );
+    }
+
+    private String joinAddress(String addr, String detail) {
+        String a = (addr == null) ? "" : addr.trim();
+        String d = (detail == null) ? "" : detail.trim();
+        return (a + " " + d).trim();
+    }
+
+    public Location upsert(LocationOwnerType ownerType, Long ownerId, String name, String plainAddress, String roadAddress, Double lat, Double lng) {
+
+        Location loc = mapRepository.findByOwnerTypeAndOwnerId(ownerType, ownerId)
+                .orElseGet(() -> Location.builder().ownerType(ownerType).ownerId(ownerId).build());
+
+        loc.setName(name);
+        loc.setAddress(plainAddress);
+        loc.setRoadAddress(roadAddress);
+        loc.setLatitude(lat);
+        loc.setLongitude(lng);
+
+        Point p = geometryFactory.createPoint(new Coordinate(lng, lat));
+        loc.setPoint(p);
+
+        return mapRepository.save(loc);
+    }
+
+}
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
index faeb633..ce7a699 100644
--- a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
+++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
@@ -5,20 +5,16 @@
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.store.entity.Store;
 
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
+import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
 @Entity
 @Getter
 @NoArgsConstructor
@@ -29,12 +25,12 @@ public class Paper extends BaseEntity {
 	@GeneratedValue(strategy = GenerationType.IDENTITY)
 	private Long id;
 
-	private String partnershipPeriod; //  이게 뭘로 들어오는거지. 그냥 LocalDate  로 하는게 낫지 않나?
+	private LocalDate partnershipPeriodStart; //  LocalDate vs String
+	private LocalDate partnershipPeriodEnd;
 
 	@Enumerated(EnumType.STRING)
 	private ActivationStatus isActivated;
 
-
 	@ManyToOne(fetch = FetchType.LAZY)
 	@JoinColumn(name = "admin_id")
 	private Admin admin;
@@ -47,4 +43,4 @@ public class Paper extends BaseEntity {
 	@JoinColumn(name = "store_id")
 	private Store store;
 
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java
index 29e3195..acc0dc4 100644
--- a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java
+++ b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java
@@ -1,22 +1,17 @@
 package com.assu.server.domain.partnership.entity;
 import com.assu.server.domain.common.entity.BaseEntity;
-import com.assu.server.domain.partnership.entity.enums.PaperContentType;
-import com.assu.server.domain.user.entity.enums.Major;
-
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
+import com.assu.server.domain.partnership.entity.enums.CriterionType;
+import com.assu.server.domain.partnership.entity.enums.OptionType;
+
+import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
+import java.util.ArrayList;
+import java.util.List;
+
 
 @Entity
 @Getter
@@ -33,19 +28,17 @@ public class PaperContent extends BaseEntity {
 	private Paper paper;
 
 	@Enumerated(EnumType.STRING)
-	private PaperContentType type;
+	private CriterionType criterionType;
 
-	private Integer people;
+	@Enumerated(EnumType.STRING)
+	private OptionType optionType;
 
-	private String belonging;
+	private Integer people;
 
 	private Long cost;
 
-	private Long discount;
-
-	private String goods;
+	private String category;
 
-	@Enumerated(EnumType.STRING)
-	private Major major;
+	private Long discount;
 
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java b/src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java
similarity index 50%
rename from src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java
rename to src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java
index 80f7023..bf77b82 100644
--- a/src/main/java/com/assu/server/domain/partnership/entity/enums/PaperContentType.java
+++ b/src/main/java/com/assu/server/domain/partnership/entity/enums/CriterionType.java
@@ -1,5 +1,5 @@
 package com.assu.server.domain.partnership.entity.enums;
 
-public enum PaperContentType{
-	PEOPLE, BELONGING, COST
-}
\ No newline at end of file
+public enum CriterionType {
+    PRICE, HEADCOUNT
+}
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java b/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java
new file mode 100644
index 0000000..3c1470b
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/partnership/entity/enums/OptionType.java
@@ -0,0 +1,5 @@
+package com.assu.server.domain.partnership.entity.enums;
+
+public enum OptionType {
+    SERVICE, COUNT
+}
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
new file mode 100644
index 0000000..65e69d3
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
@@ -0,0 +1,11 @@
+package com.assu.server.domain.partnership.repository;
+
+import com.assu.server.domain.partnership.entity.PaperContent;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface PaperContentRepository extends JpaRepository {
+
+    Optional findTopByPaperStoreIdOrderByIdDesc(Long storeId);
+}
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
new file mode 100644
index 0000000..bd60c56
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
@@ -0,0 +1,47 @@
+package com.assu.server.domain.partnership.repository;
+
+import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.partner.entity.Partner;
+import com.assu.server.domain.partnership.entity.Paper;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface PaperRepository extends JpaRepository {
+
+    Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(
+            Long adminId, Long partnerId, ActivationStatus isActivated
+    );
+
+    Optional findTopPaperByStoreId(Long storeId);
+
+    // 로그인 admin과 활성 제휴 중이며 파트너 이름이 키워드와 매칭
+    @Query("""
+        select distinct p.partner
+        from Paper p
+        where p.admin.id = :adminId
+          and p.isActivated = :status
+          and lower(p.partner.name) like lower(concat('%', :keyword, '%'))
+        order by p.id desc
+    """)
+    List findActivePartnersForAdminByKeyword(@Param("adminId") Long adminId,
+                                                      @Param("status") ActivationStatus status,
+                                                      @Param("keyword") String keyword);
+
+    // 로그인 partner와 활성 제휴 중이며 관리자 이름이 키워드와 매칭
+    @Query("""
+        select distinct p.admin
+        from Paper p
+        where p.partner.id = :partnerId
+          and p.isActivated = :status
+          and lower(p.admin.name) like lower(concat('%', :keyword, '%'))
+        order by p.id desc
+    """)
+    List findActiveAdminsForPartnerByKeyword(@Param("partnerId") Long partnerId,
+                                                    @Param("status") ActivationStatus status,
+                                                    @Param("keyword") String keyword);
+}
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java
deleted file mode 100644
index 7971fb1..0000000
--- a/src/main/java/com/assu/server/domain/partnership/repository/PatnershipRepository.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.partnership.repository;
-
-public class PatnershipRepository {
-}
diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java
index f808887..733f5fe 100644
--- a/src/main/java/com/assu/server/domain/store/entity/Store.java
+++ b/src/main/java/com/assu/server/domain/store/entity/Store.java
@@ -39,7 +39,7 @@ public class Store extends BaseEntity {
 
 	private String name;
 
-	private String adderess;
+	private String address;
 
 	private String detailAddress;
 
diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index 5b7f958..86d2aaf 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -1,4 +1,10 @@
 package com.assu.server.domain.store.repository;
 
-public class StoreRepository {
+import com.assu.server.domain.store.entity.Store;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface StoreRepository extends JpaRepository {
+    List findByNameContainingIgnoreCaseOrderByIdDesc(String name);
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 8bf2f46..54390f0 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -30,6 +30,14 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."),
     NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
+    // 스토어 에러
+    NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_6001", "존재하지 않는 가게입니다."),
+
+    // 주소 에러
+    NO_SUCH_ADDRESS(HttpStatus.NOT_FOUND, "ADDRESS_7001", "주소를 찾을 수 없습니다."),
+
+    // PaperContent 에러
+    NO_SUCH_CONTENT(HttpStatus.NOT_FOUND, "CONTENT_8001", "제휴 내용을 찾을 수 없습니다."),
 
     ;
 
diff --git a/src/main/java/com/assu/server/global/config/JpaSpatialConfig.java b/src/main/java/com/assu/server/global/config/JpaSpatialConfig.java
new file mode 100644
index 0000000..aab5686
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/JpaSpatialConfig.java
@@ -0,0 +1,15 @@
+package com.assu.server.global.config;
+
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.PrecisionModel;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class JpaSpatialConfig {
+    @Bean
+    public GeometryFactory geometryFactory() {
+        // PrecisionModel 기본, SRID=4326 (WGS84)
+        return new GeometryFactory(new PrecisionModel(), 4326);
+    }
+}
diff --git a/src/main/java/com/assu/server/global/config/KakaoLocalClient.java b/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
new file mode 100644
index 0000000..cc17092
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
@@ -0,0 +1,94 @@
+package com.assu.server.global.config;
+
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.exception.GeneralException;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriBuilder;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class KakaoLocalClient {
+    private final WebClient kakaoWebClient;
+
+    @Data
+    public static class KakaoKeywordResp {
+        private List documents;
+        private Meta meta;
+        @Data public static class Document {
+            private String id;
+            private String place_name;
+            private String category_name;
+            private String phone;
+            private String address_name;
+            private String road_address_name;
+            private String x; // 경도 문자열
+            private String y; // 위도 문자열
+            private String place_url;
+            private String distance; // 기준좌표 주면 미터 문자열
+        }
+        @Data public static class Meta { private Integer total_count; private Boolean is_end; }
+    }
+
+    public KakaoKeywordResp searchByKeyword(String query, Double x, Double y,
+                                            Integer radius, Integer page, Integer size) {
+        return kakaoWebClient.get()
+                .uri(uri -> {
+                    UriBuilder b = uri.path("/v2/local/search/keyword.json")
+                            .queryParam("query", query)
+                            .queryParam("page", page == null ? 1 : page)
+                            .queryParam("size", size == null ? 15 : size); // 카카오 최대 15
+                    if (x != null && y != null) {
+                        b.queryParam("x", x).queryParam("y", y);
+                        if (radius != null) b.queryParam("radius", radius);
+                    }
+                    return b.build();
+                })
+                .retrieve()
+                .bodyToMono(KakaoKeywordResp.class)
+                .block();
+    }
+
+    @Data
+    public static class KakaoAddressResp {
+        private List documents;
+        @Data
+        public static class Document {
+            private String x;                 // 경도
+            private String y;                 // 위도
+            private RoadAddress road_address; // 있을 수도/없을 수도
+        }
+        @Data
+        public static class RoadAddress {
+            private String address_name;      // 도로명 전체
+        }
+    }
+
+    @Data
+    public static class Geo {
+        private final Double lat;         // y (latitude)
+        private final Double lng;         // x (longitude)
+        private final String roadAddress; // nullable
+    }
+
+    public Geo geocodeByAddress(String query) {
+        KakaoAddressResp resp = kakaoWebClient.get()
+                .uri(u -> u.path("/v2/local/search/address.json").queryParam("query", query).build())
+                .retrieve()
+                .bodyToMono(KakaoAddressResp.class)
+                .block();
+        if (resp == null || resp.getDocuments() == null || resp.getDocuments().isEmpty())
+            throw new GeneralException(ErrorStatus.NO_SUCH_ADDRESS);
+        var d = resp.getDocuments().get(0);
+        Double lng = Double.valueOf(d.getX());
+        Double lat = Double.valueOf(d.getY());
+        String road = d.getRoad_address() == null ? null : d.getRoad_address().getAddress_name();
+        return new Geo(lat, lng, road);
+    }
+}
+
diff --git a/src/main/java/com/assu/server/global/config/KakaoWebClientConfig.java b/src/main/java/com/assu/server/global/config/KakaoWebClientConfig.java
new file mode 100644
index 0000000..cdb1827
--- /dev/null
+++ b/src/main/java/com/assu/server/global/config/KakaoWebClientConfig.java
@@ -0,0 +1,22 @@
+package com.assu.server.global.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Configuration
+public class KakaoWebClientConfig {
+
+    @Bean
+    public WebClient kakaoWebClient(
+            @Value("${kakao.base-url}") String baseUrl,
+            @Value("${kakao.rest-api-key}") String apiKey
+    ) {
+        return WebClient.builder()
+                .baseUrl(baseUrl)
+                .defaultHeader(HttpHeaders.AUTHORIZATION, "KakaoAK " + apiKey)
+                .build();
+    }
+}
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 c50ebd9..5f0e5f5 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -2,34 +2,28 @@
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.web.SecurityFilterChain;
 
 @Configuration
-public class SecurityConfig {
+public class    SecurityConfig {
 
     @Bean
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
-                .authorizeHttpRequests(auth -> auth
-                        .requestMatchers(
-                                "/chat/**",
-                                "/suggestion/**",
-                                "/review/**",
-                                "/ws/**",
-                                "/pub/**",     // STOMP 메시지 전송
-                                "/sub/**",     // STOMP 메시지 구독
-                                "/v3/api-docs/**",
-                                "/swagger-ui/**",
-                                "/swagger-ui.html",
-                                "/swagger-resources/**",
-                                "/webjars/**"
-                        ).permitAll()
-                        .anyRequest().authenticated()
-                )
-                .csrf(csrf -> csrf.disable()) // websocket은 csrf 필요 없음
-                .formLogin(login -> login.disable())
-                .httpBasic(basic  -> basic.disable());
+                // CSRF 비활성화
+                .csrf(csrf -> csrf.disable())
+                // CORS 기본값(필요 없으면 이 줄 삭제해도 됨)
+                .cors(Customizer.withDefaults())
+                // 모든 요청 허용
+                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+                // 세션 미사용 (원하면 STATELESS 유지, 필요 없으면 이 줄 삭제 가능)
+                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                // 폼 로그인/HTTP Basic 비활성화
+                .formLogin(form -> form.disable())
+                .httpBasic(basic -> basic.disable());
 
         return http.build();
     }
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index c17a186..9368235 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -30,4 +30,8 @@ spring:
 logging:
   level:
     org.springframework.web: DEBUG
-    org.springframework.web.client.DefaultRestClient: OFF
\ No newline at end of file
+    org.springframework.web.client.DefaultRestClient: OFF
+
+kakao:
+  base-url: https://dapi.kakao.com
+  rest-api-key: ${KAKAO_REST_API_KEY}
\ No newline at end of file

From 31ae654796aac3ed39b3e1e56f5db939a1c7a18c Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Thu, 21 Aug 2025 17:52:38 +0900
Subject: [PATCH 091/270] =?UTF-8?q?[Feat/#14]=20=20-=20requestDTO=20?=
 =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../partnership/controller/PartnershipController.java     | 2 +-
 .../domain/partnership/dto/PartnershipRequestDTO.java     | 8 ++++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

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 6ec2fb9..1ee5350 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
@@ -53,7 +53,7 @@ public BaseResponse getPartn
 
     @Operation(
             summary = "제휴 상태를 업데이트하는 API 입니다.",
-            description = "바꾸고 싶은 상태를 입력하세요(PENDING/ACTIVE/INACTIVE)"
+            description = "바꾸고 싶은 상태를 입력하세요(SUSPEND/ACTIVE/INACTIVE)"
     )
     @PatchMapping("/{partnershipId}/status")
     public BaseResponse updatePartnershipStatus(
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
index a0e1db2..b902dfd 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.partnership.dto;
 
+import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partnership.entity.enums.CriterionType;
 import com.assu.server.domain.partnership.entity.enums.OptionType;
 import lombok.*;
@@ -36,4 +37,11 @@ public static class PartnershipOptionRequestDTO {
     public static class PartnershipGoodsRequestDTO {
         private String goodsName;
     }
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    public static class UpdateRequestDTO {
+        private String status;
+    }
 }

From d781353e84866b78960dd0a169c5ac6c1e79995f Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Thu, 21 Aug 2025 18:51:07 +0900
Subject: [PATCH 092/270] =?UTF-8?q?[Feat/#14]=20=20-=20pull=20=EB=B0=9B?=
 =?UTF-8?q?=EC=95=84=EC=99=80=EC=84=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assu/server/domain/admin/service/AdminServiceImpl.java  | 2 +-
 .../server/domain/partner/service/PartnerServiceImpl.java   | 3 ++-
 .../domain/partnership/service/PartnershipServiceImpl.java  | 3 +--
 .../domain/suggestion/service/SuggestionServiceImpl.java    | 2 +-
 .../server/global/apiPayload/code/status/ErrorStatus.java   | 6 +-----
 5 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
index 841a438..160fd84 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
@@ -6,7 +6,7 @@
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
diff --git a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
index d29f408..18f9538 100644
--- a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
@@ -6,7 +6,8 @@
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+
+import com.assu.server.global.exception.DatabaseException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
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 37d0944..8e0bfec 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
@@ -17,10 +17,9 @@
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
index a4b72f6..f02b0fc 100644
--- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
@@ -18,7 +18,7 @@
 import com.assu.server.domain.user.entity.Student;
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 9ffabb2..72473f8 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -45,11 +45,7 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."),
     NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."),
     NO_MEMBER(HttpStatus.NOT_FOUND, "CHATTING_5003", "해당 방에는 사용자가 아무도 없습니다."),
-    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다.")
-
-
-    // 학생 에러
-    NO_SUCH_STUDENT(HttpStatus.NOT_FOUND, "STUDENT_5004", "존재하지 않는 학생입니다."),
+    NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
     // 스토어 에러
     NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_6001", "존재하지 않는 가게입니다."),

From 1775c177ef81c8192ce0b4ecd215db7f030eb3fc Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Sun, 24 Aug 2025 19:42:22 +0900
Subject: [PATCH 093/270] =?UTF-8?q?[Feat/#14]=20=20-=20=EC=A0=9C=ED=9C=B4?=
 =?UTF-8?q?=20=EC=88=98=EB=8F=99=20=EB=93=B1=EB=A1=9D=20=EA=B5=AC=ED=98=84?=
 =?UTF-8?q?=20=20-=20=EC=A0=9C=ED=9C=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B4=80?=
 =?UTF-8?q?=EB=A6=AC=EC=9E=90,=EC=97=85=EC=B2=B4=20=EA=B8=B0=EC=A4=80?=
 =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/PartnershipController.java     |  32 ++-
 .../dto/PartnershipRequestDTO.java            |  13 ++
 .../dto/PartnershipResponseDTO.java           |  15 ++
 .../domain/partnership/entity/Paper.java      |   1 +
 .../repository/PaperRepository.java           |  14 ++
 .../service/PartnershipService.java           |   9 +-
 .../service/PartnershipServiceImpl.java       | 214 ++++++++++++++++--
 .../server/domain/store/entity/Store.java     |   8 +-
 .../store/repository/StoreRepository.java     |   6 +
 .../controller/SuggestionController.java      |   2 +-
 src/main/resources/application.yml            |  20 +-
 11 files changed, 281 insertions(+), 53 deletions(-)

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 1ee5350..5813427 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
@@ -30,14 +30,38 @@ public BaseResponse writePar
     }
 
     @Operation(
-            summary = "제휴를 조회하는 API 입니다.",
+            summary = "제휴 제안서를 수동으로 등록하는 API 입니다.",
+            description = "제공 서비스 종류(서비스 제공, 할인), 서비스 제공 기준(금액, 인원수), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성해주시고 파일명과 타입(png/jpeg)를 설정해주세요."
+    )
+    @PostMapping("/passivity")
+    public BaseResponse createManualPartnership(
+            @RequestBody PartnershipRequestDTO.ManualPartnershipRequestDTO request,
+            @RequestParam(required = false) String filename,
+            @RequestParam(required = false) String contentType
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createManualPartnership(request, filename, contentType));
+    }
+
+    @Operation(
+            summary = "제휴 중인 가게를 조회하는 API 입니다.",
+            description = "전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정해주세요."
+    )
+    @GetMapping("/admin")
+    public BaseResponse> listForAdmin(
+            @RequestParam(name = "all", defaultValue = "false") boolean all
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForAdmin(all));
+    }
+
+    @Operation(
+            summary = "제휴 중인 관리자를 조회하는 API 입니다.",
             description = "전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정해주세요."
     )
-    @GetMapping
-    public BaseResponse> list(
+    @GetMapping("/partner")
+    public BaseResponse> listForPartner(
             @RequestParam(name = "all", defaultValue = "false") boolean all
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnerships(all));
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForPartner(all));
     }
 
     @Operation(
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
index b902dfd..fbd5f2b 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
@@ -44,4 +44,17 @@ public static class PartnershipGoodsRequestDTO {
     public static class UpdateRequestDTO {
         private String status;
     }
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    public static class ManualPartnershipRequestDTO {
+        private Long adminId;
+        private String storeName;
+        private String storeAddress;
+        private String storeDetailAddress;
+        private LocalDate partnershipPeriodStart;
+        private LocalDate partnershipPeriodEnd;
+        private List options;
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
index 8cf4a36..c37d872 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
@@ -61,4 +61,19 @@ public static class UpdateResponseDTO {
         private String newStatus;
         private LocalDateTime changedAt;
     }
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class ManualPartnershipResponseDTO {
+        private Long storeId;
+        private boolean storeCreated;
+        private boolean storeActivated;
+        private String status;
+        private String contractImageUrl;
+        private String objectKey;
+        private WritePartnershipResponseDTO partnership;
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
index 934773f..5510780 100644
--- a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
+++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
@@ -41,4 +41,5 @@ public class Paper extends BaseEntity {
 	@JoinColumn(name = "store_id")
 	private Store store;
 
+	private String contractImageUrl;
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
index 120c192..3c6b4f5 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
@@ -1,7 +1,21 @@
 package com.assu.server.domain.partnership.repository;
 
+import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partnership.entity.Paper;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.List;
+
 public interface PaperRepository extends JpaRepository {
+
+    // Admin 기준 (ACTIVE)
+    List findByAdmin_IdAndIsActivated(Long adminId, ActivationStatus status, Sort sort);
+    Page findByAdmin_IdAndIsActivated(Long adminId, ActivationStatus status, Pageable pageable);
+
+    // Partner 기준 (ACTIVE)
+    List findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Sort sort);
+    Page  findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Pageable pageable);
 }
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 cf236f9..4a21ca7 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
@@ -12,9 +12,16 @@ PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(
             @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request
     );
 
-    List listPartnerships(boolean all);
+    List listPartnershipsForAdmin(boolean all);
+    List listPartnershipsForPartner(boolean all);
+
 
     PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId);
 
     PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request);
+
+    PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership(
+            PartnershipRequestDTO.ManualPartnershipRequestDTO request,
+            String filename, String contentType
+    );
 }
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 8e0bfec..9793ff7 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
@@ -17,18 +17,24 @@
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.config.AmazonConfig;
 import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 import lombok.RequiredArgsConstructor;
+import org.apache.catalina.security.SecurityUtil;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
 
+import java.time.Duration;
 import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Service
@@ -43,6 +49,9 @@ public class PartnershipServiceImpl implements PartnershipService {
     private final PartnerRepository partnerRepository;
     private final StoreRepository storeRepository;
 
+    private final S3Presigner presigner;
+    private final AmazonConfig amazonConfig;
+
     @Override
     public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(PartnershipRequestDTO.WritePartnershipRequestDTO request) {
 
@@ -88,32 +97,37 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(Partn
     }
 
     @Override
-    public List listPartnerships(boolean all) {
+    public List listPartnershipsForAdmin(boolean all) {
+//        Long adminId = SecurityUtil.getCurrentUserId();
+        Long adminId = 1L;
+
         Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
-        List papers;
+        List papers = all
+                ? paperRepository.findByAdmin_IdAndIsActivated(adminId, ActivationStatus.ACTIVE, sort)
+                : paperRepository.findByAdmin_IdAndIsActivated(adminId, ActivationStatus.ACTIVE, PageRequest.of(0, 2, sort)).getContent();
 
-        if (all) {
-            papers = paperRepository.findAll(sort);
-        } else {
-            papers = paperRepository.findAll(PageRequest.of(0, 2, sort)).getContent();
-        }
-        if (papers.isEmpty()) return List.of();
+        papers = papers.stream()
+                .filter(p -> p.getStore() != null)
+                .toList();
 
-        List paperIds = papers.stream().map(Paper::getId).toList();
-        List allContents = paperContentRepository.findAllByPaperIdInFetchGoods(paperIds);
+        return buildPartnershipDTOs(papers);
+    }
 
-        Map> byPaperId = allContents.stream()
-                .collect(Collectors.groupingBy(pc -> pc.getPaper().getId()));
+    @Override
+    public List listPartnershipsForPartner(boolean all) {
+        //        Long partnerId = SecurityUtil.getCurrentUserId();
+        Long partnerId = 3L;
 
-        List result = new ArrayList<>(papers.size());
-        for (Paper p : papers) {
-            List contents = byPaperId.getOrDefault(p.getId(), List.of());
-            List> goodsBatches = contents.stream()
-                    .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods())
-                    .toList();
-            result.add(PartnershipConverter.writePartnershipResultDTO(p, contents, goodsBatches));
-        }
-        return result;
+        Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
+        List papers = all
+                ? paperRepository.findByPartner_IdAndIsActivated(partnerId, ActivationStatus.ACTIVE, sort)
+                : paperRepository.findByPartner_IdAndIsActivated(partnerId, ActivationStatus.ACTIVE, PageRequest.of(0, 2, sort)).getContent();
+
+        papers = papers.stream()
+                .filter(p -> p.getAdmin() != null)
+                .toList();
+
+        return buildPartnershipDTOs(papers);
     }
 
     @Override
@@ -153,6 +167,124 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par
                 .build();
     }
 
+    @Override
+    @Transactional
+    public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership(PartnershipRequestDTO.ManualPartnershipRequestDTO request, String filename, String contentType) {
+        if(request == null || request.getAdminId() == null || request.getStoreAddress() == null) throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
+
+        Admin admin = adminRepository.findById(request.getAdminId())
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+
+        Store store = storeRepository
+                .findByNameAndAddressAndDetailAddress(request.getStoreName(), request.getStoreAddress(), request.getStoreDetailAddress())
+                .orElse(null);
+
+        boolean created = false;
+        boolean reactivated = false;
+
+        if (store == null) {
+            store = Store.builder()
+                    .name(request.getStoreName())
+                    .address(request.getStoreAddress())
+                    .detailAddress(request.getStoreDetailAddress())
+                    .rate(0)
+                    .isActivate(ActivationStatus.SUSPEND)
+                    .build();
+            store = storeRepository.save(store);
+            created = true;
+        } else {
+            if(store.getIsActivate() == ActivationStatus.INACTIVE) {
+                store.setIsActivate(ActivationStatus.SUSPEND);
+                reactivated = true;
+            }
+        }
+
+        Presigned presigned = null;
+        if (filename != null && !filename.isBlank()) {
+            presigned = putUrlForStore(
+                    store.getId(), filename,
+                    (contentType == null || contentType.isBlank()) ? "image/jpeg" : contentType,
+                    Duration.ofMinutes(10)
+            );
+        }
+
+        Paper paper = Paper.builder()
+                .admin(admin)
+                .store(store)
+                .partner(null)
+                .isActivated(ActivationStatus.SUSPEND)
+                .partnershipPeriodStart(request.getPartnershipPeriodStart())
+                .partnershipPeriodEnd(request.getPartnershipPeriodEnd())
+                .build();
+        paper = paperRepository.save(paper);
+
+        List contents = new ArrayList<>();
+        if (request.getOptions() != null) {
+            for (PartnershipRequestDTO.PartnershipOptionRequestDTO o : request.getOptions()) {
+                PaperContent content = PaperContent.builder()
+                        .paper(paper)
+                        .optionType(o.getOptionType())
+                        .criterionType(o.getCriterionType())
+                        .people(o.getPeople())
+                        .cost(o.getCost())
+                        .category(o.getCategory())
+                        .discount(o.getDiscountRate())
+                        .build();
+                content = paperContentRepository.save(content);
+
+                if(o.getGoods() != null && !o.getGoods().isEmpty()) {
+                    List batch = new ArrayList<>(o.getGoods().size());
+                    for (var g : o.getGoods()) {
+                        Goods entity = Goods.builder()
+                                .content(content)
+                                .belonging(g.getGoodsName())
+                                .build();
+                        batch.add(entity);
+                    }
+                    goodsRepository.saveAll(batch);
+                }
+                contents.add(content);
+            }
+        }
+
+        List> goodsBatches = contents.stream()
+                .map(pc -> pc.getGoods() == null ? Collections.emptyList() : pc.getGoods())
+                .toList();
+
+        PartnershipResponseDTO.WritePartnershipResponseDTO partnershipResponseDTO =
+                PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches);
+
+        return PartnershipResponseDTO.ManualPartnershipResponseDTO.builder()
+                .storeId(store.getId())
+                .storeCreated(created)
+                .storeActivated(reactivated)
+                .status(store.getIsActivate() == null ? null : store.getIsActivate().name())
+                .contractImageUrl(presigned == null ? null : presigned.getUrl())
+                .objectKey(presigned == null ? null : presigned.getKey())
+                .partnership(partnershipResponseDTO)
+                .build();
+    }
+
+    private List buildPartnershipDTOs(List papers) {
+        if (papers == null || papers.isEmpty()) return List.of();
+
+        List paperIds = papers.stream().map(Paper::getId).toList();
+        List allContents = paperContentRepository.findAllByPaperIdInFetchGoods(paperIds);
+
+        Map> byPaperId = allContents.stream()
+                .collect(Collectors.groupingBy(pc -> pc.getPaper().getId()));
+
+        List result = new ArrayList<>(papers.size());
+        for (Paper p : papers) {
+            List contents = byPaperId.getOrDefault(p.getId(), List.of());
+            List> goodsBatches = contents.stream()
+                    .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods())
+                    .toList();
+            result.add(PartnershipConverter.writePartnershipResultDTO(p, contents, goodsBatches));
+        }
+        return result;
+    }
+
     private ActivationStatus parseStatus(String raw) {
         try {
             return ActivationStatus.valueOf(raw.trim().toUpperCase());
@@ -160,4 +292,36 @@ private ActivationStatus parseStatus(String raw) {
             throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
         }
     }
+
+    public Presigned putUrlForStore(Long storeId, String filename, String contentType, Duration ttl) {
+        String key = "stores/" + storeId + "/" + UUID.randomUUID() + "_" + filename;
+        return presignPut(key, contentType, ttl);
+    }
+
+    public Presigned putUrlForPartnership(Long paperId, String filename, String contentType, Duration ttl) {
+        String key = "partnerships/" + paperId + "/" + UUID.randomUUID() + "_" + filename;
+        return presignPut(key, contentType, ttl);
+    }
+
+    private Presigned presignPut(String key, String contentType, Duration ttl) {
+        PutObjectRequest por = PutObjectRequest.builder()
+                .bucket(amazonConfig.getBucket())
+                .key(key)
+                .contentType(contentType)
+                .build();
+
+        PutObjectPresignRequest preq = PutObjectPresignRequest.builder()
+                .signatureDuration(ttl == null ? Duration.ofMinutes(10) : ttl)
+                .putObjectRequest(por)
+                .build();
+
+        PresignedPutObjectRequest p = presigner.presignPutObject(preq);
+        return new Presigned(key, p.url().toString());
+    }
+
+    @Getter @AllArgsConstructor
+    public static class Presigned {
+        private String key;
+        private String url;
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java
index f808887..1cf47f6 100644
--- a/src/main/java/com/assu/server/domain/store/entity/Store.java
+++ b/src/main/java/com/assu/server/domain/store/entity/Store.java
@@ -12,10 +12,7 @@
 import jakarta.persistence.Id;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.ManyToOne;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 
 @Entity
@@ -34,12 +31,13 @@ public class Store extends BaseEntity {
 
 	private Integer rate;
 
+	@Setter
 	@Enumerated(EnumType.STRING)
 	private ActivationStatus isActivate;
 
 	private String name;
 
-	private String adderess;
+	private String address;
 
 	private String detailAddress;
 
diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index fb3611f..c3f7604 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -2,6 +2,12 @@
 
 import com.assu.server.domain.store.entity.Store;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.Optional;
 
 public interface StoreRepository extends JpaRepository {
+
+    Optional findByNameAndAddressAndDetailAddress(String name, String address, String detailAddress);
 }
diff --git a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
index dbac01c..c71ce29 100644
--- a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
+++ b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
@@ -22,7 +22,7 @@ public class SuggestionController {
             summary = "제휴 건의를 하는 API 입니다.",
             description = "건의대상, 제휴 희망 가게, 희망 혜택을 입력해주세요."
     )
-    @PostMapping()
+    @PostMapping
     public BaseResponse writeSuggestion(
             @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO
     ){
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index c17a186..68e093c 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,25 +1,11 @@
 spring:
-
-  batch:
-    jdbc:
-      initialize-schema: never
-    job:
-      enabled: false
-
-  datasource:
-    url: jdbc:mariadb://localhost:3306/assu_maria_db?allowPublicKeyRetrieval=true&useSSL=false
-    username: root
-    password: ${MARIA_DB_PASSWORD}
-    driver-class-name: org.mariadb.jdbc.Driver
-
-  profiles:
-    active: local # 여기에 local, blue, green 셋중 하나로 입력
   config:
     import:
-      - classpath:application-secret.yml
+      - optional:classpath:application-secret.yml
+      - optional:file:/app/config/application-secret.yml
   jpa:
     hibernate:
-      ddl-auto: update # 여기
+      ddl-auto: update
     properties:
       hibernate:
         jdbc:

From 8d4d838977c336357a8d945b3105fbb7d6e57e2d Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Mon, 25 Aug 2025 15:35:17 +0900
Subject: [PATCH 094/270] =?UTF-8?q?refactor/#38=20=20-=20JwtAuthFilter.jav?=
 =?UTF-8?q?a=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/auth/security/JwtAuthFilter.java   | 120 ------------------
 1 file changed, 120 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java

diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
deleted file mode 100644
index a33dac9..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.assu.server.domain.auth.security;
-
-import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import io.jsonwebtoken.Claims;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.security.authentication.InsufficientAuthenticationException;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.stereotype.Component;
-import org.springframework.util.AntPathMatcher;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import java.io.IOException;
-
-@RequiredArgsConstructor
-@Component
-@Slf4j
-public class JwtAuthFilter extends OncePerRequestFilter {
-
-    @Value("${jwt.header}")
-    private String jwtHeader;
-    private final JwtUtil jwtUtil;
-    private final RedisTemplate redisTemplate;
-
-    private static final AntPathMatcher PATH = new AntPathMatcher();
-    private static final String[] WHITELIST = {
-            "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
-            "/swagger-resources/**", "/webjars/**",
-            "/auth/**",           // ← 로그인/회원가입/인증 등은 토큰 없이 접근
-            "/chat/**", "/suggestion/**", "/review/**",
-            "/ws/**", "/pub/**", "/sub/**"
-    };
-
-    @Override
-    protected boolean shouldNotFilter(HttpServletRequest request) {
-        String uri = request.getRequestURI();
-        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; // CORS preflight 우회
-        if (PATH.match("/auth/refresh", uri)) return false;               // 토큰 재발급은 필터 적용
-        for (String p : WHITELIST) if (PATH.match(p, uri)) return true;   // 나머지 공개 경로 우회
-        return false;                                                     // 보호 자원은 필터 적용
-    }
-
-    private static void checkAuthorizationHeader(String header) {
-        log.info("-------------------#@@@@@------------------");
-        if(header == null) {
-            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
-        } else if (!header.startsWith("Bearer ")) {
-            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
-        }
-    }
-
-    @Override
-    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
-            throws ServletException, IOException {
-
-        final String authHeader = request.getHeader(jwtHeader);
-
-        log.debug("Auth header={}", request.getHeader("Authorization"));
-
-        // Refresh 전용 처리
-        if (PATH.match("/auth/refresh", request.getRequestURI())) {
-            final String refreshToken = request.getHeader("refreshToken");
-            try {
-                // 둘 다 필수
-                checkAuthorizationHeader(authHeader);
-                if (refreshToken == null) throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
-
-                String accessToken = JwtUtil.getTokenFromHeader(authHeader);
-                Claims claims = jwtUtil.validateTokenOnlySignature(accessToken); // 서명만 검증(만료 허용)
-                Authentication authentication = jwtUtil.getAuthenticationFromExpiredAccessToken(accessToken);
-                SecurityContextHolder.getContext().setAuthentication(authentication);
-
-                jwtUtil.validateRefreshToken(refreshToken); // RT는 만료 허용 X
-                chain.doFilter(request, response);
-                return;
-            } catch (Exception e) {
-                // EntryPoint로 넘겨 통일 처리
-                if (e instanceof CustomAuthException ce) {
-                    request.setAttribute("exceptionCode", ce.getCode());
-                    request.setAttribute("exceptionMessage", ce.getMessage());
-                    request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
-                }
-                throw new InsufficientAuthenticationException(e.getMessage(), e);
-            }
-        }
-
-        // 그 외(보호 자원): Authorization 헤더가 없으면 그냥 통과
-        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
-            chain.doFilter(request, response);
-            return;
-        }
-
-        try {
-            String accessToken = JwtUtil.getTokenFromHeader(authHeader);
-            jwtUtil.validateToken(accessToken);
-            jwtUtil.isTokenBlacklisted(accessToken); // accessToken 전달
-
-            Authentication authentication = jwtUtil.getAuthentication(accessToken);
-            SecurityContextHolder.getContext().setAuthentication(authentication);
-
-            chain.doFilter(request, response);
-        } catch (Exception e) {
-            if (e instanceof CustomAuthException ce) {
-                request.setAttribute("exceptionCode", ce.getCode());
-                request.setAttribute("exceptionMessage", ce.getMessage());
-                request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
-            }
-            throw new InsufficientAuthenticationException(e.getMessage(), e);
-        }
-    }
-}
-

From 02a86e1e036d9e9c5c1fe06cb95ab21192216233 Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Tue, 26 Aug 2025 14:04:51 +0900
Subject: [PATCH 095/270] =?UTF-8?q?[FEAT/#13]=20s3=EA=B8=B0=EB=B0=98=20?=
 =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../review/converter/ReviewConverter.java     | 14 +++++++--
 .../domain/review/dto/ReviewRequestDTO.java   |  5 +--
 .../domain/review/dto/ReviewResponseDTO.java  |  5 +--
 .../review/service/ReviewServiceImpl.java     | 31 ++++++++++++++++---
 .../store/service/StoreServiceImpl.java       |  2 +-
 .../apiPayload/code/status/ErrorStatus.java   |  3 +-
 6 files changed, 48 insertions(+), 12 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
index 74117f0..99b8dfb 100644
--- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
+++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
@@ -4,6 +4,7 @@
 import com.assu.server.domain.review.dto.ReviewRequestDTO;
 import com.assu.server.domain.review.dto.ReviewResponseDTO;
 import com.assu.server.domain.review.entity.Review;
+import com.assu.server.domain.review.entity.ReviewPhoto;
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.user.entity.Student;
 
@@ -19,6 +20,9 @@ public static ReviewResponseDTO.WriteReviewResponseDTO writeReviewResultDTO(Revi
                 .content(review.getContent())
 //                .memberId(review.getStudent().getId())
                 .createdAt(review.getCreatedAt())
+                .reviewImageUrls(review.getImageList().stream()
+                        .map(ReviewPhoto::getPhotoUrl)
+                        .collect(Collectors.toList()))
                 //한 리뷰 여러개 사진 but 하나로 묶임 추가 고려해보기 --추후에 !!
                 .build(); //리스폰스 리턴
     }
@@ -30,7 +34,7 @@ public static Review toReviewEntity(ReviewRequestDTO.WriteReviewRequestDTO  requ
                 .store(store)
                 .partner(partner)
                 .student(student)
-            //    .imageList(request.getReviewImage())
+                //    .imageList(request.getReviewImage())
                 .build();
     }
     public static ReviewResponseDTO.CheckStudentReviewResponseDTO checkStudentReviewResultDTO(Review review){
@@ -40,6 +44,9 @@ public static ReviewResponseDTO.CheckStudentReviewResponseDTO checkStudentReview
                 .content(review.getContent())
                 .createdAt(review.getCreatedAt())
                 .storeId(review.getStore().getId())
+                .reviewImageUrls(review.getImageList().stream()
+                        .map(ReviewPhoto::getPhotoUrl)
+                        .collect(Collectors.toList()))
                 .build();
     }
     public static List checkStudentReviewResultDTO(List reviews){
@@ -55,6 +62,9 @@ public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReview
                 .content(review.getContent())
                 .rate(review.getRate())
                 .createdAt(review.getCreatedAt())
+                .reviewImageUrls(review.getImageList().stream()
+                        .map(ReviewPhoto::getPhotoUrl)
+                        .collect(Collectors.toList()))
                 .build();
 
     }
@@ -68,4 +78,4 @@ public static ReviewResponseDTO.DeleteReviewResponseDTO deleteReviewResultDTO(Lo
                 .reviewId(reviewId)
                 .build();
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
index 791512a..676ab55 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
@@ -5,15 +5,16 @@
 import lombok.Builder;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
-public class ReviewRequestDTO {
+public class    ReviewRequestDTO {
     @Getter
     public static class WriteReviewRequestDTO {
         private String content;
         private Integer rate;
-        //private List reviewImage;
+        private List reviewImage;
         private Long storeId;
         private Long partnerId;
     }
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
index 6118065..638b79e 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
@@ -19,6 +19,7 @@ public static class WriteReviewResponseDTO {
         private Integer rate;
         private LocalDateTime createdAt;
         private Long memberId;
+        private List reviewImageUrls;
     }
     @Getter
     @NoArgsConstructor
@@ -30,7 +31,7 @@ public static class CheckStudentReviewResponseDTO { //내가 작성한 리뷰
         private String content;
         private Integer rate;
         private LocalDateTime createdAt;
-        //private List reviewImage;
+        private List reviewImageUrls;
     }
     @Getter
     @NoArgsConstructor
@@ -44,7 +45,7 @@ public static class CheckPartnerReviewResponseDTO {//partner의 리뷰 확인
         private Integer rate;
         private LocalDateTime createdAt;
         private String sortBy; // 정렬기준 -> 최신, 별점, 오래된 순
-        //private List reviewImage;
+        private List reviewImageUrls;
     }
 
 
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 041a416..3a5b820 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -1,22 +1,24 @@
 package com.assu.server.domain.review.service;
 
-import com.assu.server.domain.common.entity.Member;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.domain.review.converter.ReviewConverter;
 import com.assu.server.domain.review.dto.ReviewRequestDTO;
 import com.assu.server.domain.review.dto.ReviewResponseDTO;
 import com.assu.server.domain.review.entity.Review;
+import com.assu.server.domain.review.entity.ReviewPhoto;
 import com.assu.server.domain.review.repository.ReviewRepository;
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.user.entity.Student;
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
+import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.sql.SQLOutput;
 import java.util.List;
@@ -28,7 +30,7 @@ public class ReviewServiceImpl implements ReviewService {
     private final StoreRepository storeRepository;
     private final PartnerRepository partnerRepository;
     private final StudentRepository studentRepository;
-
+    private final AmazonS3Manager amazonS3Manager;
 
 
     @Override
@@ -36,17 +38,38 @@ public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.Wri
         //Long memberId = SecurityUtil.getCurrentUserId;
         Long memberId = 1L;
         Long storeId = request.getStoreId(); //변수 선언
+        List images = request.getReviewImage(); // 이미지 변수 선언
+
         //존재여부 검증
         Store store = storeRepository.findById(storeId)
                 .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); //없을 경우!!
         Partner partner = partnerRepository.findById(request.getPartnerId()) //파라미터 변수 선언 없이 바로 받기
                 .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-        Student student = studentRepository.findById(Math.toIntExact(memberId))
+        Student student = studentRepository.findById(memberId)
                 .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
 
         Review review = ReviewConverter.toReviewEntity(request, store, partner, student);
 
+        reviewRepository.save(review); //먼저 Review 저장
+
+        //이미지 업로드 처리
+        if (images != null && !images.isEmpty()) {
+            try {
+                for (MultipartFile image : images) {
+                    String keyName = amazonS3Manager.generateKeyName("review-images");
+                    amazonS3Manager.uploadFile(keyName, image);
+                    String presignedUrl = amazonS3Manager.generatePresignedUrl(keyName);
 
+                    ReviewPhoto reviewPhoto = ReviewPhoto.builder()
+                            .review(review)
+                            .photoUrl(presignedUrl)
+                            .build();
+                    review.getImageList().add(reviewPhoto);
+                }
+            } catch (Exception e) {
+                throw new DatabaseException(ErrorStatus.IMAGE_UPLOAD_FAILED); //ErrorStatus에 추가 필요
+            }
+        }
         reviewRepository.save(review);//rep에서 데이터 상하차 저장
         //잘 저장 됏어요!!
         return ReviewConverter.writeReviewResultDTO(review);//객체를 dto로 바꿔서 사용자에게 보여줌 -> controller
diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
index bf3ffee..a969f43 100644
--- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
@@ -7,7 +7,7 @@
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index b956c69..6c2a048 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -48,7 +48,8 @@ public enum ErrorStatus implements BaseErrorCode {
     EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."),
     EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."),
 
-
+    //리뷰 이미지 에러
+    IMAGE_UPLOAD_FAILED(HttpStatus.NOT_FOUND,"REVIEW_4001", "존재하지 않는 리뷰이미지 입니다"),
     // 채팅 에러
     NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."),
     NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."),

From 4096f9e4be54581488f270f56eb8dd34bcbbd24e Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Tue, 26 Aug 2025 16:47:13 +0900
Subject: [PATCH 096/270] =?UTF-8?q?[MOD/#13]=20pd=20=EB=B0=98=EC=98=81=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/StudentAdminController.java    | 49 +++++++++++++------
 .../mapping/service/StudentAdminService.java  | 10 ++--
 .../service/StudentAdminServiceImpl.java      | 25 ++++------
 .../review/controller/ReviewController.java   | 26 ++++++++--
 .../domain/review/service/ReviewService.java  |  6 +--
 .../review/service/ReviewServiceImpl.java     | 16 +++---
 .../store/controller/StoreController.java     | 20 +++++---
 .../domain/store/service/StoreService.java    |  4 +-
 .../store/service/StoreServiceImpl.java       | 10 ++--
 .../user/controller/StudentController.java    |  9 +++-
 .../domain/user/service/StudentService.java   |  2 +-
 .../user/service/StudentServiceImpl.java      |  8 ++-
 12 files changed, 108 insertions(+), 77 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
index 7d83a0a..dc10993 100644
--- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
+++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
@@ -4,61 +4,78 @@
 import com.assu.server.domain.mapping.service.StudentAdminService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
 @RequiredArgsConstructor
-@RequestMapping("/admin")
+@RequestMapping("/studentAdmin")
 public class StudentAdminController {
     private final StudentAdminService studentAdminService;
     @Operation(
-            summary = "누적 가입자 수 조회 API 입니다.",
+            summary = "누적 가입자 수 조회 API",
             description = "admin으로 접근해주세요."
     )
     @GetMapping
-    public BaseResponse getCountAdmin() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth());
+    public BaseResponse getCountAdmin(
+            @AuthenticationPrincipal PrincipalDetails pd
+            ) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth(memberId));
     }
     @Operation(
-            summary = "신규 한 달 가입자 수 조회 API 입니다.",
+            summary = "신규 한 달 가입자 수 조회 API",
             description = "admin으로 접근해주세요."
     )
     @GetMapping("/new")
-    public BaseResponse getNewStudentCountAdmin(){
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getNewStudentCountAdmin());
+    public BaseResponse getNewStudentCountAdmin(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ){
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getNewStudentCountAdmin(memberId));
     }
 
     @Operation(
-            summary = "오늘 제휴 사용자 수 조회 API 입니다.",
+            summary = "오늘 제휴 사용자 수 조회 API",
             description = "admin으로 접근해주세요."
     )
     @GetMapping("/countUser")
-    public BaseResponse getCountUser(){
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsagePerson());
+    public BaseResponse getCountUser(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ){
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsagePerson(memberId));
     }
     @Operation(
-            summary = "제휴업체 누적별 1위 업체 조회 API입니다.",
+            summary = "제휴업체 누적별 1위 업체 조회 API",
             description = "adminId로 접근해주세요."
     )
         @GetMapping("/top")
-        public BaseResponse getTopUsage() {
-            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsage());
+        public BaseResponse getTopUsage(
+                @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        Long memberId = pd.getMember().getId();
+            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsage(memberId));
         }
 
         /**
          * 제휴 업체별 누적 제휴 이용 현황 리스트 반환 (사용량 내림차순)
          */
         @Operation(
-                summary = "제휴업체 누적 사용 수 내림차순 조회 API입니다.",
+                summary = "제휴업체 누적 사용 수 내림차순 조회 API",
                 description = "adminId로 접근해주세요."
         )
         @GetMapping("/usage")
-        public BaseResponse getUsageList() {
-            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsageList());
+        public BaseResponse getUsageList(
+                @AuthenticationPrincipal PrincipalDetails pd
+        ) {
+            Long memberId = pd.getMember().getId();
+            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsageList(memberId));
         }
 
 }
diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java
index c6eb3e9..9504e7e 100644
--- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java
+++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminService.java
@@ -3,9 +3,9 @@
 import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO;
 
 public interface StudentAdminService {
-    StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth();
-    StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin();
-    StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson();
-    StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage();
-    StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList();
+    StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(Long memberId);
+    StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(Long memberId);
+    StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson(Long memberId);
+    StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage(Long memberId);
+    StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList(Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java
index 04ed053..5c91bb1 100644
--- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java
@@ -9,7 +9,7 @@
 import com.assu.server.domain.partnership.repository.PartnershipRepository;
 import com.assu.server.domain.user.service.StudentService;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -26,9 +26,8 @@ public class StudentAdminServiceImpl implements StudentAdminService {
 
     @Override
     @Transactional
-    public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth() {
-        //Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 6L;
+    public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(Long memberId) {
+
         Long total = studentAdminRepository.countAllByAdminId(memberId);
         Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
@@ -38,9 +37,8 @@ public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth() {
     }
     @Override
     @Transactional
-    public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin() {
-        //Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 5L;
+    public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(Long memberId) {
+
         Long total = studentAdminRepository.countThisMonthByAdminId(memberId);
         Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
@@ -50,9 +48,8 @@ public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(
 
     @Override
     @Transactional
-    public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson() {
-        //Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 5L;
+    public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson(Long memberId) {
+
         Long total = studentAdminRepository.countTodayUsersByAdmin(memberId);
         Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
@@ -62,9 +59,7 @@ public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson()
 
     @Override
     @Transactional
-    public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage() {
-        //Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 5L;
+    public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage(Long memberId) {
         Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
         String adminName =admin.getName();
@@ -79,9 +74,7 @@ public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage() {
 
     @Override
     @Transactional
-    public StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList() {
-        // Long memberId = SecurityUtil.getCurrentUserId();
-        Long memberId = 5L;
+    public StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList(Long memberId) {
 
         Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
index 599016f..1501333 100644
--- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
+++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
@@ -5,8 +5,10 @@
 import com.assu.server.domain.review.service.ReviewService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
@@ -21,8 +23,12 @@ public class ReviewController {
             description = "리뷰 내용과 별점, 리뷰 이미지를 입력해주세요."
     )
     @PostMapping()
-    public BaseResponse writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO writeReviewRequestDTO) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(writeReviewRequestDTO));
+    public BaseResponse writeReview(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @RequestBody ReviewRequestDTO.WriteReviewRequestDTO writeReviewRequestDTO
+    ) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(writeReviewRequestDTO, memberId));
     }
 
     @Operation(
@@ -30,7 +36,10 @@ public BaseResponse writeReview(@Reque
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/student")
-    public BaseResponse> checkStudent() {
+    public BaseResponse> checkStudent(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        Long memberId = pd.getMember().getId();
         return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview());
     }
 
@@ -39,7 +48,11 @@ public BaseResponse> check
             description = "삭제할 리뷰 ID를 입력해주세요."
     )
     @DeleteMapping("/{reviewId}")
-    public BaseResponse deleteReview(@PathVariable Long reviewId) {
+    public BaseResponse deleteReview(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @PathVariable Long reviewId) {
+        Long memberId = pd.getMember().getId();
+
         return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.deleteReview(reviewId));
     }
 
@@ -48,7 +61,10 @@ public BaseResponse deleteReview(@Pat
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/partner")
-    public BaseResponse> checkPartnerReview(){
+    public BaseResponse> checkPartnerReview(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ){
+        Long memberId = pd.getMember().getId();
         return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview());
     }
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
index 006fbdc..1600e07 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
@@ -8,8 +8,8 @@
 import java.util.List;
 
 public interface ReviewService {
-    ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request);
-    List checkStudentReview();
-    List checkPartnerReview();
+    ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId);
+    List checkStudentReview(Long memberId);
+    List checkPartnerReview(Long memberId);
     ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(@PathVariable Long reviewId);
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 3a5b820..712b58f 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.review.service;
 
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.domain.review.converter.ReviewConverter;
@@ -14,9 +15,11 @@
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
+import com.assu.server.global.util.PrincipalDetails;
 import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -34,9 +37,7 @@ public class ReviewServiceImpl implements ReviewService {
 
 
     @Override
-    public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request) {
-        //Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 1L;
+    public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId) {
         Long storeId = request.getStoreId(); //변수 선언
         List images = request.getReviewImage(); // 이미지 변수 선언
 
@@ -76,9 +77,7 @@ public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.Wri
     }
 
     @Override
-    public List checkStudentReview() {
-        //Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 1L;
+    public List checkStudentReview(Long memberId) {
         List reviews = reviewRepository.findByMemberId(memberId);
 
         return ReviewConverter.checkStudentReviewResultDTO(reviews);
@@ -86,10 +85,7 @@ public List checkStudentReview(
 
     @Override
     @Transactional
-    public List checkPartnerReview() {
-        //Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 2L;
-
+    public List checkPartnerReview(Long memberId) {
         Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
         System.out.println("파트너 id는 "+partner.getId());
diff --git a/src/main/java/com/assu/server/domain/store/controller/StoreController.java b/src/main/java/com/assu/server/domain/store/controller/StoreController.java
index 54c0f9a..221b955 100644
--- a/src/main/java/com/assu/server/domain/store/controller/StoreController.java
+++ b/src/main/java/com/assu/server/domain/store/controller/StoreController.java
@@ -5,8 +5,10 @@
 import com.assu.server.domain.store.service.StoreService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -20,20 +22,26 @@ public class StoreController {
     private final StoreService storeService;
 
     @Operation(
-            summary = "내 가게 순위 조회 API입니다.",
+            summary = "내 가게 순위 조회 API",
             description = "partnerId로 접근해주세요."
     )
     @GetMapping("/ranking")
-    public BaseResponse getWeeklyRank() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getWeeklyRank());
+    public BaseResponse getWeeklyRank(
+            @AuthenticationPrincipal PrincipalDetails pd
+            ) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getWeeklyRank(memberId));
     }
 
     @Operation(
-            summary = "내 가게 순위 6주치 조회 API입니다.",
+            summary = "내 가게 순위 6주치 조회 API",
             description = "partnerId로 접근해주세요"
     )
     @GetMapping("/ranking/weekly")
-    public BaseResponse> getWeeklyRankByPartnerId(){
-        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getListWeeklyRank().getItems());
+    public BaseResponse> getWeeklyRankByPartnerId(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ){
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getListWeeklyRank(memberId).getItems());
     }
 }
diff --git a/src/main/java/com/assu/server/domain/store/service/StoreService.java b/src/main/java/com/assu/server/domain/store/service/StoreService.java
index 340edc7..354bcd2 100644
--- a/src/main/java/com/assu/server/domain/store/service/StoreService.java
+++ b/src/main/java/com/assu/server/domain/store/service/StoreService.java
@@ -3,6 +3,6 @@
 import com.assu.server.domain.store.dto.StoreResponseDTO;
 
 public interface StoreService {
-    StoreResponseDTO.WeeklyRankResponseDTO getWeeklyRank();
-    StoreResponseDTO.ListWeeklyRankResponseDTO getListWeeklyRank();
+    StoreResponseDTO.WeeklyRankResponseDTO getWeeklyRank(Long memberId);
+    StoreResponseDTO.ListWeeklyRankResponseDTO getListWeeklyRank(Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
index a969f43..af2cb33 100644
--- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java
@@ -23,9 +23,8 @@ public class StoreServiceImpl implements StoreService {
 
     @Override
     @Transactional
-    public StoreResponseDTO.WeeklyRankResponseDTO getWeeklyRank() {
-        // Long memberId = SecurityUtil.getCurrentUserId();
-        Long memberId = 2L;
+    public StoreResponseDTO.WeeklyRankResponseDTO getWeeklyRank(Long memberId) {
+
         Optional partner = partnerRepository.findById(memberId);
         Store store = storeRepository.findByPartner(partner.orElse(null))
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
@@ -44,9 +43,8 @@ public StoreResponseDTO.WeeklyRankResponseDTO getWeeklyRank() {
 
     @Override
     @Transactional
-    public StoreResponseDTO.ListWeeklyRankResponseDTO getListWeeklyRank() {
-        // Long memberId = SecurityUtil.getCurrentUserId();
-        Long memberId = 2L;
+    public StoreResponseDTO.ListWeeklyRankResponseDTO getListWeeklyRank(Long memberId) {
+
         Optional partner = partnerRepository.findById(memberId);
         Store store = storeRepository.findByPartner(partner.orElse(null))
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
index d060c35..20bdd34 100644
--- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java
+++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
@@ -4,8 +4,10 @@
 import com.assu.server.domain.user.service.StudentService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -19,7 +21,10 @@ public class StudentController {
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/stamp")
-    public BaseResponse getStamp() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getStamp());
+    public BaseResponse getStamp(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getStamp(memberId));
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java
index 514ffac..ef698d8 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentService.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java
@@ -3,5 +3,5 @@
 import com.assu.server.domain.user.dto.StudentResponseDTO;
 
 public interface StudentService {
-    StudentResponseDTO.CheckStampResponseDTO getStamp();//조회
+    StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId);//조회
 }
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
index 7b08755..62bce96 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
@@ -5,7 +5,7 @@
 import com.assu.server.domain.user.entity.Student;
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -16,10 +16,8 @@ public class StudentServiceImpl implements StudentService {
         private final StudentRepository studentRepository;
     @Override
     @Transactional
-    public StudentResponseDTO.CheckStampResponseDTO getStamp() {
-        //Long studentId = SecurityUtil.getCurrentUserId;
-        Long studentId = 1L;
-        Student student = studentRepository.findById(Math.toIntExact(studentId))
+    public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) {
+        Student student = studentRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
 
         return StudentConverter.checkStampResponseDTO(student, "스탬프 조회 성공");

From 93ab9b7fe9888667e6c7cd077d08d23d151fb5d3 Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Tue, 26 Aug 2025 16:49:24 +0900
Subject: [PATCH 097/270] =?UTF-8?q?[MOD/#13]=20pd=20=EB=B0=98=EC=98=81=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/domain/review/controller/ReviewController.java     | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
index 1501333..312809d 100644
--- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
+++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
@@ -40,7 +40,7 @@ public BaseResponse> check
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
         Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview());
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview(memberId));
     }
 
     @Operation(
@@ -65,6 +65,6 @@ public BaseResponse> check
             @AuthenticationPrincipal PrincipalDetails pd
     ){
         Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview());
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview(memberId));
     }
 }

From 308a6a451d9ab4535420497f9cc4f3b4d8b7736d Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Tue, 26 Aug 2025 21:18:50 +0900
Subject: [PATCH 098/270] =?UTF-8?q?[Feat/#14]=20=20-=20pd=20=EB=A7=9E?=
 =?UTF-8?q?=EC=B6=B0=EC=84=9C=20ID=20=EC=B0=BE=EA=B8=B0=20=EB=B3=80?=
 =?UTF-8?q?=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/controller/AdminController.java     |   8 +-
 .../domain/admin/service/AdminService.java    |   2 +-
 .../admin/service/AdminServiceImpl.java       |   6 +-
 .../partner/controller/PartnerController.java |   9 +-
 .../partner/service/PartnerService.java       |   2 +-
 .../partner/service/PartnerServiceImpl.java   |   5 +-
 .../controller/PartnershipController.java     |  59 ++++--
 .../converter/PartnershipConverter.java       | 100 +++++++---
 .../dto/PartnershipRequestDTO.java            |   5 +-
 .../dto/PartnershipResponseDTO.java           |   1 -
 .../domain/partnership/entity/Paper.java      |   4 +-
 .../service/PartnershipService.java           |  15 +-
 .../service/PartnershipServiceImpl.java       | 182 ++++++++----------
 .../store/repository/StoreRepository.java     |   1 +
 .../controller/SuggestionController.java      |  21 +-
 .../suggestion/service/SuggestionService.java |   5 +-
 .../service/SuggestionServiceImpl.java        |  12 +-
 .../apiPayload/code/status/ErrorStatus.java   |  37 +---
 .../server/global/config/SecurityConfig.java  |  31 +--
 19 files changed, 255 insertions(+), 250 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java
index e51c526..3f72304 100644
--- a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java
+++ b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java
@@ -4,8 +4,10 @@
 import com.assu.server.domain.admin.service.AdminService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -23,7 +25,9 @@ public class AdminController {
     )
     @GetMapping("/partner-recommend")
     public BaseResponse randomPartnerRecommend(
-    ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, adminService.suggestRandomPartner());
+            @AuthenticationPrincipal PrincipalDetails pd
+            ) {
+        Long adminId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, adminService.suggestRandomPartner(adminId));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminService.java b/src/main/java/com/assu/server/domain/admin/service/AdminService.java
index ca13088..0533fc1 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminService.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminService.java
@@ -4,5 +4,5 @@
 
 public interface AdminService {
 
-    AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner();
+    AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId);
 }
diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
index 160fd84..83b98fe 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
@@ -20,9 +20,8 @@ public class AdminServiceImpl implements AdminService {
     private final PartnerRepository partnerRepository;
 
     @Override
-    public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner() {
-//        Long adminId = SecurityUtil.getCurrentId();
-        Long adminId = 1L;
+    public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId) {
+
         Admin admin = adminRepository.findById(adminId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
@@ -44,6 +43,5 @@ public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner() {
                 .partnerAddress(picked.getAddress())
                 .partnerDetailAddress(picked.getDetailAddress())
                 .build();
-
     }
 }
diff --git a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java
index 282acb8..b0792ca 100644
--- a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java
+++ b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java
@@ -4,8 +4,10 @@
 import com.assu.server.domain.partner.service.PartnerService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -20,7 +22,10 @@ public class PartnerController {
             description = "제휴하지 않은 어드민 중 두 곳을 랜덤으로 조회합니다."
     )
     @GetMapping("/admin-recommend")
-    public BaseResponse randomAdminRecommend(){
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnerService.getRandomAdmin());
+    public BaseResponse randomAdminRecommend(
+            @AuthenticationPrincipal PrincipalDetails pd
+            ){
+        Long partnerId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnerService.getRandomAdmin(partnerId));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/partner/service/PartnerService.java b/src/main/java/com/assu/server/domain/partner/service/PartnerService.java
index 269287e..7edd916 100644
--- a/src/main/java/com/assu/server/domain/partner/service/PartnerService.java
+++ b/src/main/java/com/assu/server/domain/partner/service/PartnerService.java
@@ -4,6 +4,6 @@
 
 public interface PartnerService {
 
-    PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin();
+    PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(Long partnerId);
 
 }
diff --git a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
index 18f9538..03ee1d1 100644
--- a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
@@ -23,10 +23,7 @@ public class PartnerServiceImpl implements PartnerService {
     private final AdminRepository adminRepository;
 
     @Override
-    public PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin() {
-        //        Long adminId = SecurityUtil.getCurrentId();
-        Long partnerId = 5L;
-
+    public PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(Long partnerId) {
         Partner partner = partnerRepository.findById(partnerId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
 
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 5813427..a1b3b39 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
@@ -5,9 +5,16 @@
 import com.assu.server.domain.partnership.service.PartnershipService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
@@ -19,53 +26,65 @@ public class PartnershipController {
     private final PartnershipService partnershipService;
 
     @Operation(
-            summary = "제휴 제안서를 작성하는 API 입니다.",
-            description = "제공 서비스 종류(서비스 제공, 할인), 서비스 제공 기준(금액, 인원수), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성해주세요."
+            summary = "제휴 제안서 작성 API",
+            description = "제공 서비스 종류(SERVICE, DISCOUNT), 서비스 제공 기준(PRICE, HEADCOUNT), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성해주세요."
     )
     @PostMapping("/proposal")
     public BaseResponse writePartnership(
-            @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO
+            @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request,
+            @AuthenticationPrincipal PrincipalDetails pd
     ){
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.writePartnership(partnershipRequestDTO));
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.writePartnershipAsPartner(request, memberId));
     }
 
     @Operation(
-            summary = "제휴 제안서를 수동으로 등록하는 API 입니다.",
-            description = "제공 서비스 종류(서비스 제공, 할인), 서비스 제공 기준(금액, 인원수), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성해주시고 파일명과 타입(png/jpeg)를 설정해주세요."
+            summary = "제휴 제안서 수동 등록 API",
+            description = "제공 서비스 종류(SERVICE, DISCOUNT), 서비스 제공 기준(PRICE, HEADCOUNT), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성하고, 계약서 이미지를 업로드하세요."
     )
-    @PostMapping("/passivity")
+    @PostMapping(value = "/passivity", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse createManualPartnership(
-            @RequestBody PartnershipRequestDTO.ManualPartnershipRequestDTO request,
-            @RequestParam(required = false) String filename,
-            @RequestParam(required = false) String contentType
+            @RequestPart("request") @Parameter PartnershipRequestDTO.ManualPartnershipRequestDTO request,
+            @Parameter(
+                    description = "계약서 이미지 파일",
+                    content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                            schema = @Schema(type = "string", format = "binary"))
+            )
+            MultipartFile contractImage,
+            @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createManualPartnership(request, filename, contentType));
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createManualPartnership(request, memberId, contractImage));
     }
 
     @Operation(
-            summary = "제휴 중인 가게를 조회하는 API 입니다.",
+            summary = "제휴 중인 가게 조회 API",
             description = "전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정해주세요."
     )
     @GetMapping("/admin")
     public BaseResponse> listForAdmin(
-            @RequestParam(name = "all", defaultValue = "false") boolean all
+            @RequestParam(name = "all", defaultValue = "false") boolean all,
+            @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForAdmin(all));
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForAdmin(all, memberId));
     }
 
     @Operation(
-            summary = "제휴 중인 관리자를 조회하는 API 입니다.",
+            summary = "제휴 중인 관리자 조회 API",
             description = "전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정해주세요."
     )
     @GetMapping("/partner")
     public BaseResponse> listForPartner(
-            @RequestParam(name = "all", defaultValue = "false") boolean all
+            @RequestParam(name = "all", defaultValue = "false") boolean all,
+            @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForPartner(all));
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForPartner(all, memberId));
     }
 
     @Operation(
-            summary = "제휴를 상세조회하는 API 입니다.",
+            summary = "제휴 상세조회 API",
             description = "제휴 아이디를 입력하세요."
     )
     @GetMapping("/{partnershipId}")
@@ -76,8 +95,8 @@ public BaseResponse getPartn
     }
 
     @Operation(
-            summary = "제휴 상태를 업데이트하는 API 입니다.",
-            description = "바꾸고 싶은 상태를 입력하세요(SUSPEND/ACTIVE/INACTIVE)"
+            summary = "제휴 상태 업데이트 API",
+            description = "제휴 ID와 바꾸고 싶은 상태를 입력하세요(SUSPEND/ACTIVE/INACTIVE)"
     )
     @PatchMapping("/{partnershipId}/status")
     public BaseResponse updatePartnershipStatus(
diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index 64c0d22..114b24a 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -10,6 +10,7 @@
 import com.assu.server.domain.partnership.entity.PaperContent;
 import com.assu.server.domain.store.entity.Store;
 
+import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -79,52 +80,99 @@ public static List> toGoodsBatches(
         return batches;
     }
 
+    public static Paper toPaperForManual(
+            Admin admin, Store store,
+            LocalDate start, LocalDate end,
+            ActivationStatus status
+    ) {
+        return Paper.builder()
+                .admin(admin)
+                .store(store)
+                .partner(null)
+                .isActivated(status)
+                .partnershipPeriodStart(start)
+                .partnershipPeriodEnd(end)
+                .build();
+    }
+
+    public static List toPaperContentsForManual(
+            List options,
+            Paper paper
+    ) {
+        if (options == null || options.isEmpty()) return List.of();
+        List list = new ArrayList<>(options.size());
+        for (var o : options) {
+            list.add(PaperContent.builder()
+                    .paper(paper)
+                    .optionType(o.getOptionType())
+                    .criterionType(o.getCriterionType())
+                    .people(o.getPeople())
+                    .cost(o.getCost())
+                    .category(o.getCategory())
+                    .discount(o.getDiscountRate())
+                    .build());
+        }
+        return list;
+    }
+
+    public static List toGoodsForContent(
+            PartnershipRequestDTO.PartnershipOptionRequestDTO option,
+            PaperContent content
+    ) {
+        if (option.getGoods() == null || option.getGoods().isEmpty()) return List.of();
+        List batch = new ArrayList<>(option.getGoods().size());
+        for (var g : option.getGoods()) {
+            batch.add(Goods.builder()
+                    .content(content)
+                    .belonging(g.getGoodsName())
+                    .build());
+        }
+        return batch;
+    }
+
+
     public static PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipResultDTO(
             Paper paper,
             List contents,
             List> goodsBatches
     ) {
         List optionDTOS = new ArrayList<>();
-        int n = contents == null ? 0 : contents.size();
-        for(int i = 0;i < n;i++){
-            PaperContent pc = contents.get(i);
-            List goods = (goodsBatches != null && goodsBatches.size() > i)
-                    ? goodsBatches.get(i) : List.of();
-            optionDTOS.add(optionResultDTO(pc, goods));
+        if (contents != null) {
+            for (int i = 0; i < contents.size(); i++) {
+                PaperContent pc = contents.get(i);
+                List goods = (goodsBatches != null && goodsBatches.size() > i)
+                        ? goodsBatches.get(i) : List.of();
+                optionDTOS.add(
+                        PartnershipResponseDTO.PartnershipOptionResponseDTO.builder()
+                                .optionType(pc.getOptionType())
+                                .criterionType(pc.getCriterionType())
+                                .people(pc.getPeople())
+                                .cost(pc.getCost())
+                                .category(pc.getCategory())
+                                .discountRate(pc.getDiscount())
+                                .goods(goodsResultDTO(goods))
+                                .build()
+                );
+            }
         }
-
         return PartnershipResponseDTO.WritePartnershipResponseDTO.builder()
                 .partnershipId(paper.getId())
                 .partnershipPeriodStart(paper.getPartnershipPeriodStart())
                 .partnershipPeriodEnd(paper.getPartnershipPeriodEnd())
-                .adminId(paper.getAdmin() == null ? null : paper.getAdmin().getId())
-                .partnerId(paper.getStore() == null ? null : paper.getPartner().getId())
-                .storeId(paper.getStore() == null ? null : paper.getStore().getId())
+                .adminId(paper.getAdmin()    != null ? paper.getAdmin().getId()     : null)
+                .partnerId(paper.getPartner()!= null ? paper.getPartner().getId()   : null) // 수동등록이면 null
+                .storeId(paper.getStore()    != null ? paper.getStore().getId()     : null)
                 .options(optionDTOS)
                 .build();
     }
 
-    public static PartnershipResponseDTO.PartnershipOptionResponseDTO optionResultDTO(
-            PaperContent pc, List goods
-    ) {
-        return PartnershipResponseDTO.PartnershipOptionResponseDTO.builder()
-                .optionType(pc.getOptionType())
-                .criterionType(pc.getCriterionType())
-                .people(pc.getPeople())
-                .cost(pc.getCost())
-                .category(pc.getCategory())
-                .discountRate(pc.getDiscount())
-                .goods(goodsResultDTO(goods))
-                .build();
-    }
-
     public static List goodsResultDTO(List goods) {
-        if(goods == null || goods.isEmpty()) return List.of();
+        if (goods == null || goods.isEmpty()) return List.of();
         return goods.stream()
                 .map(g -> PartnershipResponseDTO.PartnershipGoodsResponseDTO.builder()
                         .goodsId(g.getId())
                         .goodsName(g.getBelonging())
                         .build())
-                .collect(Collectors.toList());
+                .toList();
     }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
index fbd5f2b..0ee7a67 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
@@ -13,11 +13,9 @@ public class PartnershipRequestDTO {
 
     @Getter
     public static class WritePartnershipRequestDTO {
+        private Long adminId; // 제안 학생회 아이디
         private LocalDate partnershipPeriodStart;
         private LocalDate partnershipPeriodEnd;
-        private Long adminId; // 제안 학생회 아이디
-        private Long partnerId; // 제안자  아이디
-        private Long storeId; // 제안 가게 아이디
         private List options; // 동적으로 받는 제안 항목
     }
 
@@ -49,7 +47,6 @@ public static class UpdateRequestDTO {
     @Setter
     @NoArgsConstructor
     public static class ManualPartnershipRequestDTO {
-        private Long adminId;
         private String storeName;
         private String storeAddress;
         private String storeDetailAddress;
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
index c37d872..7aea47c 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
@@ -73,7 +73,6 @@ public static class ManualPartnershipResponseDTO {
         private boolean storeActivated;
         private String status;
         private String contractImageUrl;
-        private String objectKey;
         private WritePartnershipResponseDTO partnership;
     }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
index 5510780..40f32ba 100644
--- a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
+++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
@@ -41,5 +41,7 @@ public class Paper extends BaseEntity {
 	@JoinColumn(name = "store_id")
 	private Store store;
 
-	private String contractImageUrl;
+	private String contractImageKey;
+
+	public void updateContractImageKey(String key) { this.contractImageKey = key; }
 }
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 4a21ca7..24e78fd 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
@@ -2,18 +2,22 @@
 
 import com.assu.server.domain.partnership.dto.PartnershipRequestDTO;
 import com.assu.server.domain.partnership.dto.PartnershipResponseDTO;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
 public interface PartnershipService {
 
-    PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(
-            @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request
+    PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipAsPartner(
+            @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request,
+            Long memberId
     );
 
-    List listPartnershipsForAdmin(boolean all);
-    List listPartnershipsForPartner(boolean all);
+    List listPartnershipsForAdmin(boolean all, Long partnerId);
+    List listPartnershipsForPartner(boolean all, Long adminId);
 
 
     PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId);
@@ -22,6 +26,7 @@ PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(
 
     PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership(
             PartnershipRequestDTO.ManualPartnershipRequestDTO request,
-            String filename, String contentType
+            Long adminId,
+            MultipartFile contractImage
     );
 }
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 9793ff7..e41c042 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
@@ -19,6 +19,8 @@
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.config.AmazonConfig;
 import com.assu.server.global.exception.DatabaseException;
+import com.assu.server.global.util.PrincipalDetails;
+import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
@@ -27,11 +29,13 @@
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 import software.amazon.awssdk.services.s3.model.PutObjectRequest;
 import software.amazon.awssdk.services.s3.presigner.S3Presigner;
 import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
 import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
 
+import java.security.Principal;
 import java.time.Duration;
 import java.time.LocalDateTime;
 import java.util.*;
@@ -49,18 +53,32 @@ public class PartnershipServiceImpl implements PartnershipService {
     private final PartnerRepository partnerRepository;
     private final StoreRepository storeRepository;
 
-    private final S3Presigner presigner;
+    private final AmazonS3Manager amazonS3Manager;
     private final AmazonConfig amazonConfig;
 
+
     @Override
-    public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(PartnershipRequestDTO.WritePartnershipRequestDTO request) {
+    public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipAsPartner(
+            PartnershipRequestDTO.WritePartnershipRequestDTO request,
+            Long memberId
+    ) {
+        if (request == null || memberId == null) {
+            throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
+        }
+
+        Partner partner = partnerRepository.findById(memberId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
 
         Admin admin = adminRepository.findById(request.getAdminId())
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
-        Partner partner = partnerRepository.findById(request.getPartnerId())
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-        Store store = storeRepository.findById(request.getStoreId())
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
+
+        Store store = storeRepository.findByPartner_Id(partner.getId())
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+
+        return writePartnership(request, admin, partner, store);
+    }
+
+    public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(PartnershipRequestDTO.WritePartnershipRequestDTO request, Admin admin, Partner partner, Store store) {
 
         Paper paper = PartnershipConverter.toPaperEntity(request, admin, partner, store);
         paper = paperRepository.save(paper);
@@ -92,15 +110,11 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(Partn
         if(!toPersist.isEmpty()){
             goodsRepository.saveAll(toPersist);
         }
-
         return PartnershipConverter.writePartnershipResultDTO(paper, contents, attachedGoodsBatches);
     }
 
     @Override
-    public List listPartnershipsForAdmin(boolean all) {
-//        Long adminId = SecurityUtil.getCurrentUserId();
-        Long adminId = 1L;
-
+    public List listPartnershipsForAdmin(boolean all, Long adminId) {
         Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
         List papers = all
                 ? paperRepository.findByAdmin_IdAndIsActivated(adminId, ActivationStatus.ACTIVE, sort)
@@ -114,10 +128,7 @@ public List listPartnerships
     }
 
     @Override
-    public List listPartnershipsForPartner(boolean all) {
-        //        Long partnerId = SecurityUtil.getCurrentUserId();
-        Long partnerId = 3L;
-
+    public List listPartnershipsForPartner(boolean all, Long partnerId) {
         Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
         List papers = all
                 ? paperRepository.findByPartner_IdAndIsActivated(partnerId, ActivationStatus.ACTIVE, sort)
@@ -169,10 +180,15 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par
 
     @Override
     @Transactional
-    public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership(PartnershipRequestDTO.ManualPartnershipRequestDTO request, String filename, String contentType) {
-        if(request == null || request.getAdminId() == null || request.getStoreAddress() == null) throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
+    public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership(
+            PartnershipRequestDTO.ManualPartnershipRequestDTO request,
+            Long adminId,
+            MultipartFile contractImage) {
 
-        Admin admin = adminRepository.findById(request.getAdminId())
+        if (request == null || adminId == null || request.getStoreAddress() == null)
+            throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
+
+        Admin admin = adminRepository.findById(adminId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
         Store store = storeRepository
@@ -192,76 +208,64 @@ public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnersh
                     .build();
             store = storeRepository.save(store);
             created = true;
-        } else {
-            if(store.getIsActivate() == ActivationStatus.INACTIVE) {
-                store.setIsActivate(ActivationStatus.SUSPEND);
-                reactivated = true;
-            }
+        } else if (store.getIsActivate() == ActivationStatus.INACTIVE) {
+            store.setIsActivate(ActivationStatus.SUSPEND);
+            reactivated = true;
         }
 
-        Presigned presigned = null;
-        if (filename != null && !filename.isBlank()) {
-            presigned = putUrlForStore(
-                    store.getId(), filename,
-                    (contentType == null || contentType.isBlank()) ? "image/jpeg" : contentType,
-                    Duration.ofMinutes(10)
-            );
-        }
-
-        Paper paper = Paper.builder()
-                .admin(admin)
-                .store(store)
-                .partner(null)
-                .isActivated(ActivationStatus.SUSPEND)
-                .partnershipPeriodStart(request.getPartnershipPeriodStart())
-                .partnershipPeriodEnd(request.getPartnershipPeriodEnd())
-                .build();
+        Paper paper = PartnershipConverter.toPaperForManual(
+                admin, store,
+                request.getPartnershipPeriodStart(),
+                request.getPartnershipPeriodEnd(),
+                ActivationStatus.SUSPEND
+        );
         paper = paperRepository.save(paper);
 
-        List contents = new ArrayList<>();
-        if (request.getOptions() != null) {
-            for (PartnershipRequestDTO.PartnershipOptionRequestDTO o : request.getOptions()) {
-                PaperContent content = PaperContent.builder()
-                        .paper(paper)
-                        .optionType(o.getOptionType())
-                        .criterionType(o.getCriterionType())
-                        .people(o.getPeople())
-                        .cost(o.getCost())
-                        .category(o.getCategory())
-                        .discount(o.getDiscountRate())
-                        .build();
-                content = paperContentRepository.save(content);
-
-                if(o.getGoods() != null && !o.getGoods().isEmpty()) {
-                    List batch = new ArrayList<>(o.getGoods().size());
-                    for (var g : o.getGoods()) {
-                        Goods entity = Goods.builder()
-                                .content(content)
-                                .belonging(g.getGoodsName())
-                                .build();
-                        batch.add(entity);
-                    }
-                    goodsRepository.saveAll(batch);
-                }
-                contents.add(content);
+        if (contractImage != null && !contractImage.isEmpty()) {
+            try {
+                String keyName = amazonS3Manager.generateKeyName("contract-images");
+                amazonS3Manager.uploadFile(keyName, contractImage);
+                paper.updateContractImageKey(keyName);
+                paperRepository.save(paper);
+                String url = amazonS3Manager.generatePresignedUrl(keyName);
+            } catch (Exception e) {
+                throw new DatabaseException(ErrorStatus.IMAGE_UPLOAD_FAILED);
             }
         }
 
-        List> goodsBatches = contents.stream()
-                .map(pc -> pc.getGoods() == null ? Collections.emptyList() : pc.getGoods())
+        List savedContents = new ArrayList<>();
+        if (request.getOptions() != null && !request.getOptions().isEmpty()) {
+            List contents = PartnershipConverter.toPaperContentsForManual(request.getOptions(), paper);
+            savedContents = paperContentRepository.saveAll(contents);
+
+            List toPersist = new ArrayList<>();
+            for (int i = 0; i < savedContents.size(); i++) {
+                var opt = request.getOptions().get(i);
+                var content = savedContents.get(i);
+                var batch = PartnershipConverter.toGoodsForContent(opt, content);
+                if (!batch.isEmpty()) toPersist.addAll(batch);
+            }
+            if (!toPersist.isEmpty()) goodsRepository.saveAll(toPersist);
+        }
+
+        List contentsWithGoods = paperContentRepository.findAllByOnePaperIdInFetchGoods(paper.getId());
+        List> goodsBatches = contentsWithGoods.stream()
+                .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods())
                 .toList();
 
-        PartnershipResponseDTO.WritePartnershipResponseDTO partnershipResponseDTO =
-                PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches);
+        var partnership = PartnershipConverter.writePartnershipResultDTO(paper, contentsWithGoods, goodsBatches);
+
+        String url = (paper.getContractImageKey() == null)
+                ? null
+                :amazonS3Manager.generatePresignedUrl(paper.getContractImageKey());
 
         return PartnershipResponseDTO.ManualPartnershipResponseDTO.builder()
                 .storeId(store.getId())
                 .storeCreated(created)
                 .storeActivated(reactivated)
                 .status(store.getIsActivate() == null ? null : store.getIsActivate().name())
-                .contractImageUrl(presigned == null ? null : presigned.getUrl())
-                .objectKey(presigned == null ? null : presigned.getKey())
-                .partnership(partnershipResponseDTO)
+                .contractImageUrl(url)
+                .partnership(partnership)
                 .build();
     }
 
@@ -292,36 +296,4 @@ private ActivationStatus parseStatus(String raw) {
             throw new DatabaseException(ErrorStatus.INVALID_REQUEST);
         }
     }
-
-    public Presigned putUrlForStore(Long storeId, String filename, String contentType, Duration ttl) {
-        String key = "stores/" + storeId + "/" + UUID.randomUUID() + "_" + filename;
-        return presignPut(key, contentType, ttl);
-    }
-
-    public Presigned putUrlForPartnership(Long paperId, String filename, String contentType, Duration ttl) {
-        String key = "partnerships/" + paperId + "/" + UUID.randomUUID() + "_" + filename;
-        return presignPut(key, contentType, ttl);
-    }
-
-    private Presigned presignPut(String key, String contentType, Duration ttl) {
-        PutObjectRequest por = PutObjectRequest.builder()
-                .bucket(amazonConfig.getBucket())
-                .key(key)
-                .contentType(contentType)
-                .build();
-
-        PutObjectPresignRequest preq = PutObjectPresignRequest.builder()
-                .signatureDuration(ttl == null ? Duration.ofMinutes(10) : ttl)
-                .putObjectRequest(por)
-                .build();
-
-        PresignedPutObjectRequest p = presigner.presignPutObject(preq);
-        return new Presigned(key, p.url().toString());
-    }
-
-    @Getter @AllArgsConstructor
-    public static class Presigned {
-        private String key;
-        private String url;
-    }
 }
diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index c3f7604..bfe7e69 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -8,6 +8,7 @@
 import java.util.Optional;
 
 public interface StoreRepository extends JpaRepository {
+    Optional findByPartner_Id(Long partnerId);
 
     Optional findByNameAndAddressAndDetailAddress(String name, String address, String detailAddress);
 }
diff --git a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
index c71ce29..7fd5b20 100644
--- a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
+++ b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
@@ -5,8 +5,10 @@
 import com.assu.server.domain.suggestion.service.SuggestionService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
@@ -19,22 +21,27 @@ public class SuggestionController {
     private final SuggestionService suggestionService;
 
     @Operation(
-            summary = "제휴 건의를 하는 API 입니다.",
-            description = "건의대상, 제휴 희망 가게, 희망 혜택을 입력해주세요."
+            summary = "제휴 건의 API",
+            description = "건의대상, 제휴 희망 가게, 희망 혜택을 입력하세요."
     )
     @PostMapping
     public BaseResponse writeSuggestion(
-            @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO
+            @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO,
+            @AuthenticationPrincipal PrincipalDetails pd
     ){
-        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO));
+        Long userId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO, userId));
     }
 
     @Operation(
-            summary = "제휴 건의를 조회하는 API 입니다.",
+            summary = "제휴 건의 조회 API",
             description = "모든 제휴 건의를 조회합니다."
     )
     @GetMapping("/list")
-    public BaseResponse> getSuggestions() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.getSuggestions());
+    public BaseResponse> getSuggestions(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        Long adminId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.getSuggestions(adminId));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java
index b8e710f..716ccd3 100644
--- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java
+++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java
@@ -13,9 +13,10 @@
 public interface SuggestionService {
 
     SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(
-            @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO request
+            @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO request,
+            Long userId
     );
 
-    List getSuggestions();
+    List getSuggestions(Long adminId);
 
 }
diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
index f02b0fc..1508dc5 100644
--- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
@@ -34,13 +34,12 @@ public class SuggestionServiceImpl implements SuggestionService {
     private final StudentRepository studentRepository;
 
     @Override
-    public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(SuggestionRequestDTO.WriteSuggestionRequestDTO request) {
-//        Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 9L;
+    public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(SuggestionRequestDTO.WriteSuggestionRequestDTO request, Long userId) {
+
         Admin admin = adminRepository.findById(request.getAdminId())
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
-        Student student = studentRepository.findById(memberId)
+        Student student = studentRepository.findById(userId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
 
         Suggestion suggestion = SuggestionConverter.toSuggestionEntity(request, admin, student);
@@ -50,10 +49,7 @@ public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(Suggesti
     }
 
     @Override
-    public List getSuggestions() {
-        // Long adminId = SecurityUtil.getCurrentUserId();
-        Long adminId = 1L;
-
+    public List getSuggestions(Long adminId) {
         List list = suggestionRepository
                 .findAllSuggestions(adminId);
 
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 72473f8..2def4e6 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -39,7 +39,7 @@ public enum ErrorStatus implements BaseErrorCode {
     EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4005","이미 존재하는 전화번호입니다."),
     EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."),
     EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."),
-
+    NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "MEMBER_4008", "제휴업체를 찾을 수 없습니다."),
 
     // 채팅 에러
     NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."),
@@ -48,37 +48,16 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_MESSAGE(HttpStatus.NOT_FOUND, "CHATTING_5004", "해당 방에는 메시지가 아무것 없습니다."),
 
     // 스토어 에러
-    NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_6001", "존재하지 않는 가게입니다."),
-
-    // 혜택 없음 에러
-    OPTION_NOT_EMPTY(HttpStatus.BAD_REQUEST, "OPTION_7001", "혜택은 한 가지 이상이어야 합니다."),
-
-    // 벨류(금액, 인원수) 에러
-    VALUE_IS_REQUIRED(HttpStatus.NOT_FOUND, "VALUE_8001", "값을 알 수 없습니다."),
-
-    // 서비스 아이템 에러
-    SERVICE_ITEM_REQUIRED(HttpStatus.NOT_FOUND, "SERVICE_ITEM_9001", "서비스 품목은 한 가지 이상이어야 합니다."),
-
-    // 카테고리 에러
-    CATEGORY_REQUIRED(HttpStatus.NOT_FOUND, "CATEGORY_10001", "품목에 대한 카테고리가 설정되어야 합니다."),
+    NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_6001", "존재하지 않는 가게입니다"),
 
-    // 할인율 에러
-    DISCOUNT_RATE_REQUIRED(HttpStatus.NOT_FOUND, "DISCOUNT_11001", "할인율 값을 알 수 없습니다."),
-
-    // 혜택 타입 에러
-    UNSUPPORTED_OPTION_TYPE(HttpStatus.NOT_FOUND, "OPTION_7002", "지원하지 않는 혜택 항목입니다."),
-
-    // 제휴 아이디 에러
-    NO_SUCH_PAPER(HttpStatus.NOT_FOUND, "PAPER_12001", "존재하지 않는 제휴입니다."),
-
-    // 어드민 찾기 에러
-    NO_AVAILABLE_ADMIN(HttpStatus.NOT_FOUND, "ADMIN_5002", "제휴단체를 찾을 수 없습니다."),
+    // 유효하지 않은 요청
+    INVALID_REQUEST(HttpStatus.NOT_FOUND, "INVALID_7001", "유효하지 않은 요청입니다."),
 
-    // 파트너 찾기 에러
-    NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "PARTNER_5502", "제휴업체를 찾을 수 없습니다."),
+    // 제휴 에러
+    NO_SUCH_PAPER(HttpStatus.NOT_FOUND, "PAPER_9001", "제휴를 찾을 수 없습니다."),
 
-    // 유효하지 않은 요청
-    INVALID_REQUEST(HttpStatus.NOT_FOUND, "INVALID_13001", "유효하지 않은 요청입니다."),
+    // 이미지 업로드 실패
+    IMAGE_UPLOAD_FAILED(HttpStatus.NOT_FOUND,"IMAGE_8001", "이미지 업로드에 실패했습니다."),
 
     ;
 
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 bc449a4..20d61c5 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -16,36 +16,11 @@ public class SecurityConfig {
     public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
         http
                 .csrf(csrf -> csrf.disable())
-                .cors(cors -> {}) // 기본 CORS 구성 사용(필요하면 CorsConfigurationSource 빈 추가)
-                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                 .authorizeHttpRequests(auth -> auth
-                        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
-
-                        // Swagger 등 공개 리소스
-                        .requestMatchers(
-                                "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
-                                "/swagger-resources/**", "/webjars/**"
-                        ).permitAll()
-
-                        // 로그인/회원가입/재발급만 공개
-                        .requestMatchers(
-                                "/auth/login/common",
-                                "/auth/login/student",
-                                "/auth/signup/**",
-                                "/auth/refresh",
-                                "/auth/phone-numbers/**"
-                        ).permitAll()
-
-                        // 로그아웃은 인증 필요
-                        .requestMatchers("/auth/logout").authenticated()
-
-                        // 나머지는 인증 필요
-                        .anyRequest().authenticated()
+                        .anyRequest().permitAll()
                 )
-                .formLogin(login -> login.disable())
-                .httpBasic(basic -> basic.disable())
-                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
-
+                .formLogin(form -> form.disable())
+                .httpBasic(basic -> basic.disable());
         return http.build();
     }
 }

From 41f34ed4e8d2fec0248d9cc536dfdf52f85cee13 Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Tue, 26 Aug 2025 22:56:30 +0900
Subject: [PATCH 099/270] =?UTF-8?q?[FIX/#13]=20CustomAuthException=20?=
 =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/domain/admin/entity/Admin.java     |  1 -
 ...hHandler.java => CustomAuthException.java} |  5 ++--
 .../security/adapter/CommonAuthAdapter.java   |  8 +++---
 .../auth/security/adapter/SSUAuthAdapter.java |  8 +++---
 .../auth/security/jwt/JwtAuthFilter.java      | 18 ++++++------
 .../domain/auth/security/jwt/JwtUtil.java     | 28 +++++++++----------
 .../domain/auth/service/LoginServiceImpl.java |  4 +--
 .../auth/service/LogoutServiceImpl.java       |  5 ----
 .../auth/service/PhoneAuthServiceImpl.java    |  4 +--
 .../auth/service/SignUpServiceImpl.java       | 10 +++----
 .../controller/StudentAdminController.java    |  2 +-
 .../user/controller/StudentController.java    |  2 +-
 12 files changed, 44 insertions(+), 51 deletions(-)
 rename src/main/java/com/assu/server/domain/auth/exception/{CustomAuthHandler.java => CustomAuthException.java} (56%)

diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
index 856e05b..358bbfd 100644
--- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java
+++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
@@ -6,7 +6,6 @@
 import jakarta.persistence.MapsId;
 import jakarta.persistence.OneToOne;
 import jakarta.persistence.Id;
-import com.assu.server.domain.common.entity.Member;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
similarity index 56%
rename from src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java
rename to src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
index 3ca144a..138384d 100644
--- a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java
+++ b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
@@ -2,11 +2,10 @@
 
 import com.assu.server.global.apiPayload.code.BaseErrorCode;
 import com.assu.server.global.exception.GeneralException;
-import org.springframework.http.HttpStatus;
 
-public class CustomAuthHandler extends GeneralException {
+public class CustomAuthException extends GeneralException {
 
-    public CustomAuthHandler(BaseErrorCode errorCode) {
+    public CustomAuthException(BaseErrorCode errorCode) {
         super(errorCode);
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
index bb5fc56..60b941b 100644
--- a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
@@ -2,7 +2,7 @@
 
 import com.assu.server.domain.auth.entity.AuthRealm;
 import com.assu.server.domain.auth.entity.CommonAuth;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.repository.CommonAuthRepository;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.member.entity.Member;
@@ -23,7 +23,7 @@ public class CommonAuthAdapter implements RealmAuthAdapter {
     @Override
     public UserDetails loadUserDetails(String email) {
         CommonAuth ca = commonAuthRepository.findByEmail(email)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
         var m = ca.getMember();
         boolean enabled = m.getIsActivated() == ActivationStatus.ACTIVE;
         String authority = "ROLE_" + m.getRole().name();
@@ -39,14 +39,14 @@ public UserDetails loadUserDetails(String email) {
 
     @Override public Member loadMember(String email) {
         return commonAuthRepository.findByEmail(email)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER))
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER))
                 .getMember();
     }
 
     @Override
     public void registerCredentials(Member member, String email, String rawPassword) {
         if (commonAuthRepository.existsByEmail(email)) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_EMAIL);
+            throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL);
         }
         String hash = passwordEncoder.encode(rawPassword);
         commonAuthRepository.save(
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
index 808cd63..0f763c6 100644
--- a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
@@ -3,7 +3,7 @@
 import com.assu.server.domain.auth.crypto.StudentPasswordEncoder;
 import com.assu.server.domain.auth.entity.AuthRealm;
 import com.assu.server.domain.auth.entity.SSUAuth;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.repository.SSUAuthRepository;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.member.entity.Member;
@@ -24,7 +24,7 @@ public class SSUAuthAdapter implements RealmAuthAdapter {
     @Override
     public UserDetails loadUserDetails(String studentNumber) {
         SSUAuth sa = ssuAuthRepository.findByStudentNumber(studentNumber)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
         var m = sa.getMember();
         boolean enabled = m.getIsActivated() == ActivationStatus.ACTIVE;
         String authority = "ROLE_" + m.getRole().name();
@@ -40,14 +40,14 @@ public UserDetails loadUserDetails(String studentNumber) {
 
     @Override public Member loadMember(String studentNumber) {
         return ssuAuthRepository.findByStudentNumber(studentNumber)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER))
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER))
                 .getMember();
     }
 
     @Override
     public void registerCredentials(Member member, String studentNumber, String rawPassword) {
         if (ssuAuthRepository.existsByStudentNumber(studentNumber)) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_STUDENT);
+            throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
         }
         String cipher = studentPasswordEncoder.encode(rawPassword);
         ssuAuthRepository.save(
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 4eac706..d5fbf70 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
@@ -1,6 +1,6 @@
 package com.assu.server.domain.auth.security.jwt;
 
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import io.jsonwebtoken.Claims;
 import jakarta.servlet.FilterChain;
@@ -71,14 +71,14 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
     /**
      * Authorization 헤더가 존재하고 Bearer 포맷인지 확인한다.
      * 
-     * @throws CustomAuthHandler 헤더 누락/형식 오류
+     * @throws CustomAuthException 헤더 누락/형식 오류
      */
     private static void requireBearerAuthorizationHeader(String authorizationHeader) {
         if (authorizationHeader == null) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
         }
         if (!authorizationHeader.startsWith("Bearer ")) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
         }
     }
 
@@ -104,7 +104,7 @@ protected void doFilterInternal(
                 // Bearer 헤더 검증
                 requireBearerAuthorizationHeader(authorizationHeader);
                 if (refreshToken == null) {
-                    throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+                    throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
                 }
 
                 // Access 토큰: 서명만 검증(만료 허용) 및 블랙리스트 확인(JTI)
@@ -113,7 +113,7 @@ protected void doFilterInternal(
                 String accessJti = accessClaims.getId();
                 Boolean accessBlacklisted = redisTemplate.hasKey("blacklist:" + accessJti);
                 if (Boolean.TRUE.equals(accessBlacklisted)) {
-                    throw new CustomAuthHandler(ErrorStatus.LOGOUT_USER);
+                    throw new CustomAuthException(ErrorStatus.LOGOUT_USER);
                 }
 
                 // Refresh 토큰: 서명/만료 검증 + Redis 저장 여부 확인
@@ -125,7 +125,7 @@ protected void doFilterInternal(
                 Boolean refreshExists = redisTemplate.hasKey(refreshKey);
                 if (Boolean.FALSE.equals(refreshExists)) {
                     // 저장된 RT가 없으면 유효하지 않은 재발급 시도
-                    throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+                    throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
                 }
 
                 // 컨텍스트에 만료된 Access 토큰으로부터 Authentication 복원
@@ -136,7 +136,7 @@ protected void doFilterInternal(
                 return;
             } catch (Exception exception) {
                 log.error("인증 과정 중, 예상치 못한 예외 발생: {}", exception.getMessage(), exception);
-                throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+                throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
             }
         }
 
@@ -158,7 +158,7 @@ protected void doFilterInternal(
             chain.doFilter(request, response);
         } catch (Exception exception) {
             log.error("인증 과정 중, 예상치 못한 예외 발생: {}", exception.getMessage(), exception);
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 }
diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
index 31a19d1..84238bf 100644
--- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
@@ -2,7 +2,7 @@
 
 import com.assu.server.domain.auth.dto.signup.Tokens;
 import com.assu.server.domain.auth.entity.AuthRealm;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.member.repository.MemberRepository;
@@ -125,15 +125,15 @@ public Tokens issueTokens(Long memberId, String username, UserRole role, String
      * Access 토큰 서명/만료 검증.
      * @param token Access 토큰
      * @return 유효한 Claims
-     * @throws CustomAuthHandler 만료/서명 오류
+     * @throws CustomAuthException 만료/서명 오류
      */
     public Claims validateToken(String token) {
         try {
             return Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(token).getBody();
         } catch (ExpiredJwtException exception) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_ACCESS_TOKEN_EXPIRED);
+            throw new CustomAuthException(ErrorStatus.JWT_ACCESS_TOKEN_EXPIRED);
         } catch (Exception exception) {
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 
@@ -142,7 +142,7 @@ public Claims validateToken(String token) {
      * - 재발급 시 사용.
      * @param token Access 토큰
      * @return Claims(만료된 토큰도 반환)
-     * @throws CustomAuthHandler 서명 오류
+     * @throws CustomAuthException 서명 오류
      */
     public Claims validateTokenOnlySignature(String token) {
         try {
@@ -150,7 +150,7 @@ public Claims validateTokenOnlySignature(String token) {
         } catch (ExpiredJwtException exception) {
             return exception.getClaims(); // 만료되어도 Claims는 사용
         } catch (Exception exception) {
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 
@@ -158,15 +158,15 @@ public Claims validateTokenOnlySignature(String token) {
      * Refresh 토큰 서명/만료 검증.
      * - Redis 저장값과의 매칭은 호출부 정책에 따라 별도로 수행 가능.
      * @param refreshToken Refresh 토큰
-     * @throws CustomAuthHandler 만료/서명 오류
+     * @throws CustomAuthException 만료/서명 오류
      */
     public void validateRefreshToken(String refreshToken) {
         try {
             Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(refreshToken).getBody();
         } catch (ExpiredJwtException exception) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_REFRESH_TOKEN_EXPIRED);
+            throw new CustomAuthException(ErrorStatus.JWT_REFRESH_TOKEN_EXPIRED);
         } catch (Exception exception) {
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 
@@ -184,7 +184,7 @@ public Authentication getAuthentication(String accessToken) {
 
         // DB 조회
         Member member = memberRepository.findById(memberId)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
 
         // PrincipalDetails 빌드
         PrincipalDetails principal = PrincipalDetails.builder()
@@ -214,7 +214,7 @@ public Authentication getAuthenticationFromExpiredAccessToken(String expiredAcce
 
         Long userId = ((Number) claims.get("userId")).longValue();
         Member member = memberRepository.findById(userId)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_MEMBER));
 
         UserRole role = UserRole.valueOf((String) claims.get("role"));
         String authRealmString = (String) claims.get("authRealm");
@@ -255,14 +255,14 @@ public Authentication getAuthenticationFromExpiredAccessToken(String expiredAcce
     /**
      * Access 토큰이 블랙리스트에 포함되어 있지 않은지 확인.
      * @param accessToken Access 토큰
-     * @throws CustomAuthHandler 블랙리스트에 포함된 경우
+     * @throws CustomAuthException 블랙리스트에 포함된 경우
      */
     public void assertNotBlacklisted(String accessToken) {
         Claims claims = validateTokenOnlySignature(accessToken);
         String jti = claims.getId();
         Boolean exists = redisTemplate.hasKey("blacklist:" + jti);
         if (Boolean.TRUE.equals(exists)) {
-            throw new CustomAuthHandler(ErrorStatus.LOGOUT_USER);
+            throw new CustomAuthException(ErrorStatus.LOGOUT_USER);
         }
     }
 
@@ -314,7 +314,7 @@ public Tokens rotateRefreshToken(String refreshToken) {
         String refreshKey = String.format("refresh:%d:%s", memberId, refreshJti);
         String savedRefreshToken = redisTemplate.opsForValue().get(refreshKey);
         if (savedRefreshToken == null || !savedRefreshToken.equals(refreshToken)) {
-            throw new CustomAuthHandler(ErrorStatus.REFRESH_TOKEN_NOT_EQUAL);
+            throw new CustomAuthException(ErrorStatus.REFRESH_TOKEN_NOT_EQUAL);
         }
 
         // 4) 기존 RT 삭제 후 새 토큰 발급
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
index 76e89de..5d38188 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -9,7 +9,7 @@
 import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
 import com.assu.server.domain.auth.security.token.LoginUsernamePasswordAuthenticationToken;
 import com.assu.server.domain.member.entity.Member;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import lombok.RequiredArgsConstructor;
@@ -33,7 +33,7 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) {
         return realmAuthAdapters.stream()
                 .filter(a -> a.supports(realm))
                 .findFirst()
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION));
     }
 
     /**
diff --git a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
index b80f695..af98c3e 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
@@ -1,16 +1,11 @@
 package com.assu.server.domain.auth.service;
 
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import io.jsonwebtoken.Claims;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
 @Service
 @RequiredArgsConstructor
 public class LogoutServiceImpl implements LogoutService {
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 70104cc..bd73185 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
@@ -2,7 +2,7 @@
 
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.util.RandomNumberUtil;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.core.ValueOperations;
@@ -38,7 +38,7 @@ public void verifyAuthNumber(String phoneNumber, String authNumber) {
         String stored = valueOps.get(phoneNumber);
 
         if (stored == null || !stored.equals(authNumber)) {
-            throw new CustomAuthHandler(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
+            throw new CustomAuthException(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
         }
 
         // 인증 성공 시 Redis에서 삭제(Optional)
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 f1b081d..a8ffd67 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
@@ -6,7 +6,7 @@
 import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
 import com.assu.server.domain.auth.dto.signup.student.StudentInfoPayload;
 import com.assu.server.domain.auth.entity.AuthRealm;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.domain.common.enums.ActivationStatus;
@@ -47,7 +47,7 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) {
         return realmAuthAdapters.stream()
                 .filter(a -> a.supports(realm))
                 .findFirst()
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION));
     }
 
     /* 학생: JSON */
@@ -56,7 +56,7 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) {
     public SignUpResponse signupStudent(StudentSignUpRequest req) {
         // 중복 체크
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
         }
 
         // 1) member 생성
@@ -120,7 +120,7 @@ public SignUpResponse signupStudent(StudentSignUpRequest req) {
     @Transactional
     public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage) {
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
         }
 
         // 1) member 생성
@@ -175,7 +175,7 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
     @Transactional
     public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImage) {
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
         }
 
         // 1) member 생성
diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
index dc10993..5cc4f3c 100644
--- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
+++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
@@ -14,7 +14,7 @@
 
 @RestController
 @RequiredArgsConstructor
-@RequestMapping("/studentAdmin")
+@RequestMapping("/dashBoard")
 public class StudentAdminController {
     private final StudentAdminService studentAdminService;
     @Operation(
diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
index 20bdd34..92af18c 100644
--- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java
+++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
@@ -17,7 +17,7 @@ public class StudentController {
     private final StudentService studentService;
 
     @Operation(
-            summary = "스탬프 조회",
+            summary = "스탬프 조회 API",
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/stamp")

From cd009e0633257bb6dfc6f0ef5214fb4c27107342 Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Wed, 27 Aug 2025 03:56:20 +0900
Subject: [PATCH 100/270] =?UTF-8?q?[Feat/#24]=20=20-=20=EC=9E=A5=EC=86=8C?=
 =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?=
 =?UTF-8?q?=20=20-=20partner=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?=
 =?UTF-8?q?=EC=8B=9C=20store=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/domain/admin/entity/Admin.java     |   9 +
 .../admin/repository/AdminRepository.java     |  33 ++
 ...hHandler.java => CustomAuthException.java} |   5 +-
 .../security/adapter/CommonAuthAdapter.java   |   8 +-
 .../auth/security/adapter/SSUAuthAdapter.java |   8 +-
 .../auth/security/jwt/JwtAuthFilter.java      |  18 +-
 .../domain/auth/security/jwt/JwtUtil.java     |  28 +-
 .../domain/auth/service/LoginServiceImpl.java |   4 +-
 .../auth/service/LogoutServiceImpl.java       |   5 -
 .../auth/service/PhoneAuthServiceImpl.java    |   4 +-
 .../auth/service/SignUpServiceImpl.java       |  74 +++-
 .../domain/map/controller/MapController.java  |  75 ++--
 .../domain/map/converter/MapConverter.java    |  19 -
 .../server/domain/map/dto/MapRequestDTO.java  |  10 +
 .../server/domain/map/dto/MapResponseDTO.java |  45 +-
 .../server/domain/map/entity/Location.java    |  31 --
 .../map/entity/enums/LocationOwnerType.java   |   5 -
 .../domain/map/repository/MapRepository.java  |  33 --
 .../server/domain/map/service/MapService.java |  19 +-
 .../domain/map/service/MapServiceImpl.java    | 393 +++++++-----------
 .../server/domain/partner/entity/Partner.java |   9 +
 .../partner/repository/PartnerRepository.java |  30 ++
 .../repository/PaperRepository.java           |  26 --
 .../server/domain/store/entity/Store.java     |  18 +
 .../store/repository/StoreRepository.java     |  26 ++
 .../apiPayload/code/status/ErrorStatus.java   |   4 +-
 .../global/config/KakaoLocalClient.java       |  83 ++--
 27 files changed, 519 insertions(+), 503 deletions(-)
 rename src/main/java/com/assu/server/domain/auth/exception/{CustomAuthHandler.java => CustomAuthException.java} (56%)
 delete mode 100644 src/main/java/com/assu/server/domain/map/converter/MapConverter.java
 delete mode 100644 src/main/java/com/assu/server/domain/map/entity/Location.java
 delete mode 100644 src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java
 delete mode 100644 src/main/java/com/assu/server/domain/map/repository/MapRepository.java

diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
index 613554d..c5bccf5 100644
--- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java
+++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
@@ -7,6 +7,9 @@
 import jakarta.persistence.OneToOne;
 import jakarta.persistence.Id;
 import lombok.*;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+import org.locationtech.jts.geom.Point;
 
 import java.time.LocalDateTime;
 
@@ -37,4 +40,10 @@ public class Admin {
     private Boolean isSignVerified;
 
     private LocalDateTime signVerifiedAt;
+
+    @JdbcTypeCode(SqlTypes.GEOMETRY)
+    private Point point;
+
+    private double latitude;
+    private double longitude;
 }
diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
index 4fd442a..e09b096 100644
--- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
+++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
@@ -1,7 +1,40 @@
 package com.assu.server.domain.admin.repository;
 
 import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.common.enums.ActivationStatus;
+import com.assu.server.domain.member.entity.Member;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
 
 public interface AdminRepository extends JpaRepository {
+
+    @Query(value = """
+        SELECT a.*
+        FROM admin a
+        WHERE a.point IS NOT NULL
+          AND ST_Contains(ST_GeomFromText(:wkt, 4326), a.point)
+        """, nativeQuery = true)
+    List findAllWithinViewport(@Param("wkt") String wkt);
+
+    @Query("""
+        select distinct a
+        from Admin a
+        where lower(a.name) like lower(concat('%', :keyword, '%'))
+          and exists (
+              select 1 from Paper pc
+              where pc.admin = a
+                and pc.partner.id = :partnerId
+                and pc.isActivated = :status
+          )
+        """)
+    List searchPartneredByName(
+            @Param("partnerId") Long partnerId,
+            @Param("status") ActivationStatus status,
+            @Param("keyword") String keyword
+    );
+
+    Long member(Member member);
 }
diff --git a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
similarity index 56%
rename from src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java
rename to src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
index 3ca144a..138384d 100644
--- a/src/main/java/com/assu/server/domain/auth/exception/CustomAuthHandler.java
+++ b/src/main/java/com/assu/server/domain/auth/exception/CustomAuthException.java
@@ -2,11 +2,10 @@
 
 import com.assu.server.global.apiPayload.code.BaseErrorCode;
 import com.assu.server.global.exception.GeneralException;
-import org.springframework.http.HttpStatus;
 
-public class CustomAuthHandler extends GeneralException {
+public class CustomAuthException extends GeneralException {
 
-    public CustomAuthHandler(BaseErrorCode errorCode) {
+    public CustomAuthException(BaseErrorCode errorCode) {
         super(errorCode);
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
index bb5fc56..60b941b 100644
--- a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
@@ -2,7 +2,7 @@
 
 import com.assu.server.domain.auth.entity.AuthRealm;
 import com.assu.server.domain.auth.entity.CommonAuth;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.repository.CommonAuthRepository;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.member.entity.Member;
@@ -23,7 +23,7 @@ public class CommonAuthAdapter implements RealmAuthAdapter {
     @Override
     public UserDetails loadUserDetails(String email) {
         CommonAuth ca = commonAuthRepository.findByEmail(email)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
         var m = ca.getMember();
         boolean enabled = m.getIsActivated() == ActivationStatus.ACTIVE;
         String authority = "ROLE_" + m.getRole().name();
@@ -39,14 +39,14 @@ public UserDetails loadUserDetails(String email) {
 
     @Override public Member loadMember(String email) {
         return commonAuthRepository.findByEmail(email)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER))
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER))
                 .getMember();
     }
 
     @Override
     public void registerCredentials(Member member, String email, String rawPassword) {
         if (commonAuthRepository.existsByEmail(email)) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_EMAIL);
+            throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL);
         }
         String hash = passwordEncoder.encode(rawPassword);
         commonAuthRepository.save(
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
index 808cd63..0f763c6 100644
--- a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
@@ -3,7 +3,7 @@
 import com.assu.server.domain.auth.crypto.StudentPasswordEncoder;
 import com.assu.server.domain.auth.entity.AuthRealm;
 import com.assu.server.domain.auth.entity.SSUAuth;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.repository.SSUAuthRepository;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.member.entity.Member;
@@ -24,7 +24,7 @@ public class SSUAuthAdapter implements RealmAuthAdapter {
     @Override
     public UserDetails loadUserDetails(String studentNumber) {
         SSUAuth sa = ssuAuthRepository.findByStudentNumber(studentNumber)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
         var m = sa.getMember();
         boolean enabled = m.getIsActivated() == ActivationStatus.ACTIVE;
         String authority = "ROLE_" + m.getRole().name();
@@ -40,14 +40,14 @@ public UserDetails loadUserDetails(String studentNumber) {
 
     @Override public Member loadMember(String studentNumber) {
         return ssuAuthRepository.findByStudentNumber(studentNumber)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER))
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER))
                 .getMember();
     }
 
     @Override
     public void registerCredentials(Member member, String studentNumber, String rawPassword) {
         if (ssuAuthRepository.existsByStudentNumber(studentNumber)) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_STUDENT);
+            throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
         }
         String cipher = studentPasswordEncoder.encode(rawPassword);
         ssuAuthRepository.save(
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 4eac706..d5fbf70 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
@@ -1,6 +1,6 @@
 package com.assu.server.domain.auth.security.jwt;
 
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import io.jsonwebtoken.Claims;
 import jakarta.servlet.FilterChain;
@@ -71,14 +71,14 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
     /**
      * Authorization 헤더가 존재하고 Bearer 포맷인지 확인한다.
      * 
-     * @throws CustomAuthHandler 헤더 누락/형식 오류
+     * @throws CustomAuthException 헤더 누락/형식 오류
      */
     private static void requireBearerAuthorizationHeader(String authorizationHeader) {
         if (authorizationHeader == null) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
         }
         if (!authorizationHeader.startsWith("Bearer ")) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
         }
     }
 
@@ -104,7 +104,7 @@ protected void doFilterInternal(
                 // Bearer 헤더 검증
                 requireBearerAuthorizationHeader(authorizationHeader);
                 if (refreshToken == null) {
-                    throw new CustomAuthHandler(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+                    throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
                 }
 
                 // Access 토큰: 서명만 검증(만료 허용) 및 블랙리스트 확인(JTI)
@@ -113,7 +113,7 @@ protected void doFilterInternal(
                 String accessJti = accessClaims.getId();
                 Boolean accessBlacklisted = redisTemplate.hasKey("blacklist:" + accessJti);
                 if (Boolean.TRUE.equals(accessBlacklisted)) {
-                    throw new CustomAuthHandler(ErrorStatus.LOGOUT_USER);
+                    throw new CustomAuthException(ErrorStatus.LOGOUT_USER);
                 }
 
                 // Refresh 토큰: 서명/만료 검증 + Redis 저장 여부 확인
@@ -125,7 +125,7 @@ protected void doFilterInternal(
                 Boolean refreshExists = redisTemplate.hasKey(refreshKey);
                 if (Boolean.FALSE.equals(refreshExists)) {
                     // 저장된 RT가 없으면 유효하지 않은 재발급 시도
-                    throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+                    throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
                 }
 
                 // 컨텍스트에 만료된 Access 토큰으로부터 Authentication 복원
@@ -136,7 +136,7 @@ protected void doFilterInternal(
                 return;
             } catch (Exception exception) {
                 log.error("인증 과정 중, 예상치 못한 예외 발생: {}", exception.getMessage(), exception);
-                throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+                throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
             }
         }
 
@@ -158,7 +158,7 @@ protected void doFilterInternal(
             chain.doFilter(request, response);
         } catch (Exception exception) {
             log.error("인증 과정 중, 예상치 못한 예외 발생: {}", exception.getMessage(), exception);
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 }
diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
index 31a19d1..84238bf 100644
--- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
@@ -2,7 +2,7 @@
 
 import com.assu.server.domain.auth.dto.signup.Tokens;
 import com.assu.server.domain.auth.entity.AuthRealm;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.member.repository.MemberRepository;
@@ -125,15 +125,15 @@ public Tokens issueTokens(Long memberId, String username, UserRole role, String
      * Access 토큰 서명/만료 검증.
      * @param token Access 토큰
      * @return 유효한 Claims
-     * @throws CustomAuthHandler 만료/서명 오류
+     * @throws CustomAuthException 만료/서명 오류
      */
     public Claims validateToken(String token) {
         try {
             return Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(token).getBody();
         } catch (ExpiredJwtException exception) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_ACCESS_TOKEN_EXPIRED);
+            throw new CustomAuthException(ErrorStatus.JWT_ACCESS_TOKEN_EXPIRED);
         } catch (Exception exception) {
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 
@@ -142,7 +142,7 @@ public Claims validateToken(String token) {
      * - 재발급 시 사용.
      * @param token Access 토큰
      * @return Claims(만료된 토큰도 반환)
-     * @throws CustomAuthHandler 서명 오류
+     * @throws CustomAuthException 서명 오류
      */
     public Claims validateTokenOnlySignature(String token) {
         try {
@@ -150,7 +150,7 @@ public Claims validateTokenOnlySignature(String token) {
         } catch (ExpiredJwtException exception) {
             return exception.getClaims(); // 만료되어도 Claims는 사용
         } catch (Exception exception) {
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 
@@ -158,15 +158,15 @@ public Claims validateTokenOnlySignature(String token) {
      * Refresh 토큰 서명/만료 검증.
      * - Redis 저장값과의 매칭은 호출부 정책에 따라 별도로 수행 가능.
      * @param refreshToken Refresh 토큰
-     * @throws CustomAuthHandler 만료/서명 오류
+     * @throws CustomAuthException 만료/서명 오류
      */
     public void validateRefreshToken(String refreshToken) {
         try {
             Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(refreshToken).getBody();
         } catch (ExpiredJwtException exception) {
-            throw new CustomAuthHandler(ErrorStatus.JWT_REFRESH_TOKEN_EXPIRED);
+            throw new CustomAuthException(ErrorStatus.JWT_REFRESH_TOKEN_EXPIRED);
         } catch (Exception exception) {
-            throw new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION);
+            throw new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION);
         }
     }
 
@@ -184,7 +184,7 @@ public Authentication getAuthentication(String accessToken) {
 
         // DB 조회
         Member member = memberRepository.findById(memberId)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_SUCH_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
 
         // PrincipalDetails 빌드
         PrincipalDetails principal = PrincipalDetails.builder()
@@ -214,7 +214,7 @@ public Authentication getAuthenticationFromExpiredAccessToken(String expiredAcce
 
         Long userId = ((Number) claims.get("userId")).longValue();
         Member member = memberRepository.findById(userId)
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.NO_MEMBER));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_MEMBER));
 
         UserRole role = UserRole.valueOf((String) claims.get("role"));
         String authRealmString = (String) claims.get("authRealm");
@@ -255,14 +255,14 @@ public Authentication getAuthenticationFromExpiredAccessToken(String expiredAcce
     /**
      * Access 토큰이 블랙리스트에 포함되어 있지 않은지 확인.
      * @param accessToken Access 토큰
-     * @throws CustomAuthHandler 블랙리스트에 포함된 경우
+     * @throws CustomAuthException 블랙리스트에 포함된 경우
      */
     public void assertNotBlacklisted(String accessToken) {
         Claims claims = validateTokenOnlySignature(accessToken);
         String jti = claims.getId();
         Boolean exists = redisTemplate.hasKey("blacklist:" + jti);
         if (Boolean.TRUE.equals(exists)) {
-            throw new CustomAuthHandler(ErrorStatus.LOGOUT_USER);
+            throw new CustomAuthException(ErrorStatus.LOGOUT_USER);
         }
     }
 
@@ -314,7 +314,7 @@ public Tokens rotateRefreshToken(String refreshToken) {
         String refreshKey = String.format("refresh:%d:%s", memberId, refreshJti);
         String savedRefreshToken = redisTemplate.opsForValue().get(refreshKey);
         if (savedRefreshToken == null || !savedRefreshToken.equals(refreshToken)) {
-            throw new CustomAuthHandler(ErrorStatus.REFRESH_TOKEN_NOT_EQUAL);
+            throw new CustomAuthException(ErrorStatus.REFRESH_TOKEN_NOT_EQUAL);
         }
 
         // 4) 기존 RT 삭제 후 새 토큰 발급
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
index 76e89de..5d38188 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -9,7 +9,7 @@
 import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
 import com.assu.server.domain.auth.security.token.LoginUsernamePasswordAuthenticationToken;
 import com.assu.server.domain.member.entity.Member;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import lombok.RequiredArgsConstructor;
@@ -33,7 +33,7 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) {
         return realmAuthAdapters.stream()
                 .filter(a -> a.supports(realm))
                 .findFirst()
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION));
     }
 
     /**
diff --git a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
index b80f695..af98c3e 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LogoutServiceImpl.java
@@ -1,16 +1,11 @@
 package com.assu.server.domain.auth.service;
 
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import io.jsonwebtoken.Claims;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
 @Service
 @RequiredArgsConstructor
 public class LogoutServiceImpl implements LogoutService {
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 70104cc..bd73185 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
@@ -2,7 +2,7 @@
 
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.util.RandomNumberUtil;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.core.ValueOperations;
@@ -38,7 +38,7 @@ public void verifyAuthNumber(String phoneNumber, String authNumber) {
         String stored = valueOps.get(phoneNumber);
 
         if (stored == null || !stored.equals(authNumber)) {
-            throw new CustomAuthHandler(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
+            throw new CustomAuthException(ErrorStatus.NOT_VERIFIED_PHONE_NUMBER);
         }
 
         // 인증 성공 시 Redis에서 삭제(Optional)
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 f1b081d..5530733 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
@@ -6,7 +6,7 @@
 import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
 import com.assu.server.domain.auth.dto.signup.student.StudentInfoPayload;
 import com.assu.server.domain.auth.entity.AuthRealm;
-import com.assu.server.domain.auth.exception.CustomAuthHandler;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.domain.common.enums.ActivationStatus;
@@ -15,13 +15,19 @@
 import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.store.entity.Store;
+import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.domain.user.entity.Student;
 import com.assu.server.domain.user.entity.enums.Major;
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.global.config.KakaoLocalClient;
 import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -43,11 +49,15 @@ public class SignUpServiceImpl implements SignUpService {
     private final AmazonS3Manager amazonS3Manager;
     private final JwtUtil jwtUtil;
 
+    private final KakaoLocalClient kakaoLocalClient;
+    private final GeometryFactory geometryFactory;
+    private final StoreRepository storeRepository;
+
     private RealmAuthAdapter pickAdapter(AuthRealm realm) {
         return realmAuthAdapters.stream()
                 .filter(a -> a.supports(realm))
                 .findFirst()
-                .orElseThrow(() -> new CustomAuthHandler(ErrorStatus.AUTHORIZATION_EXCEPTION));
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION));
     }
 
     /* 학생: JSON */
@@ -56,7 +66,7 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) {
     public SignUpResponse signupStudent(StudentSignUpRequest req) {
         // 중복 체크
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
         }
 
         // 1) member 생성
@@ -120,7 +130,7 @@ public SignUpResponse signupStudent(StudentSignUpRequest req) {
     @Transactional
     public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage) {
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
         }
 
         // 1) member 생성
@@ -143,17 +153,47 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
         String licenseUrl = amazonS3Manager.uploadFile(keyName, licenseImage);
         CommonInfoPayload info = req.getCommonInfo();
 
+        String query = joinAddress(info.getAddress(), info.getDetailAddress());
+        var geo = kakaoLocalClient.searchAddress(query, null, null);
+        Double lat = geo != null ? geo.getLat() : null;
+        Double lng = geo != null ? geo.getLng() : null;
+        Point point = toPoint(lat, lng);
+
         // 3) Partner 프로필 생성
-        partnerRepository.save(
+        Partner partner = partnerRepository.save(
                 Partner.builder()
                         .member(member)
                         .name(info.getName())
                         .address(info.getAddress())
                         .detailAddress(info.getDetailAddress())
                         .licenseUrl(licenseUrl)
+                        .point(point)
+                        .latitude(lat)
+                        .longitude(lng)
                         .build()
         );
 
+        storeRepository.findBySameAddress(info.getAddress(), info.getDetailAddress())
+                .ifPresentOrElse(store -> {
+                    if (store.getPartner() != null || store.getPartner().getId().equals(partner.getId())) {
+                        store.linkPartner(partner);
+                        storeRepository.save(store);
+                    }
+                }, () -> {
+                    Store newly = Store.builder()
+                            .partner(partner)
+                            .rate(0)
+                            .isActivate(ActivationStatus.ACTIVE)
+                            .name(info.getName())
+                            .address(info.getAddress())
+                            .detailAddress(info.getDetailAddress())
+                            .latitude(lat)
+                            .longitude(lng)
+                            .point(point)
+                            .build();
+                    storeRepository.save(newly);
+                });
+
         // 4) 토큰 발급
         Tokens tokens = jwtUtil.issueTokens(
                 member.getId(),
@@ -175,7 +215,7 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
     @Transactional
     public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImage) {
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
-            throw new CustomAuthHandler(ErrorStatus.EXISTED_PHONE);
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
         }
 
         // 1) member 생성
@@ -198,6 +238,12 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
         String signUrl = amazonS3Manager.uploadFile(keyName, signImage);
         CommonInfoPayload info = req.getCommonInfo();
 
+        String query = joinAddress(info.getAddress(), info.getDetailAddress());
+        var geo = kakaoLocalClient.geocodeByAddress(query);
+        Double lat = geo != null ? geo.getLat() : null;
+        Double lng = geo != null ? geo.getLng() : null;
+        Point point = toPoint(lat, lng);
+
         // 3) Partner 프로필 생성
         adminRepository.save(
                 Admin.builder()
@@ -206,6 +252,9 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
                         .officeAddress(info.getAddress())
                         .detailAddress(info.getDetailAddress())
                         .signUrl(signUrl)
+                        .point(point)
+                        .latitude(lat)
+                        .longitude(lng)
                         .build()
         );
 
@@ -224,4 +273,17 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
                 .tokens(tokens)
                 .build();
     }
+
+    public Point toPoint(Double lat, Double lng) {
+        if (lat == null || lng == null) return null;
+        Point p = geometryFactory.createPoint(new Coordinate(lng, lat)); // x=lng, y=lat
+        p.setSRID(4326);
+        return p;
+    }
+
+    private String joinAddress(String addr, String detail) {
+        String a = (addr == null) ? "" : addr.trim();
+        String d = (detail == null) ? "" : detail.trim();
+        return (a + " " + d).trim();
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/map/controller/MapController.java b/src/main/java/com/assu/server/domain/map/controller/MapController.java
index 4ea1ef1..a51cea9 100644
--- a/src/main/java/com/assu/server/domain/map/controller/MapController.java
+++ b/src/main/java/com/assu/server/domain/map/controller/MapController.java
@@ -1,14 +1,17 @@
 package com.assu.server.domain.map.controller;
 
+import com.assu.server.domain.common.enums.UserRole;
 import com.assu.server.domain.map.dto.MapRequestDTO;
 import com.assu.server.domain.map.dto.MapResponseDTO;
 import com.assu.server.domain.map.service.MapService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
@@ -21,74 +24,48 @@ public class MapController {
     private final MapService mapService;
 
     @Operation(
-            summary = "관리자 위치 및 정보를 저장하는 API 입니다.",
-            description = "로그인된 관리자 프로필의 주소를 사용해 위치를 저장/갱신합니다."
-    )
-    @PostMapping("/locations/admin")
-    public BaseResponse saveAdminPin() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, mapService.saveAdminPin());
-    }
-
-    @Operation(
-            summary = "파트너 위치 및 정보를 저장하는 API 입니다.",
-            description = "로그인된 파트너 프로필의 주소를 사용해 위치를 저장/갱신합니다."
-    )
-    @PostMapping("/locations/partner")
-    public BaseResponse savePartnerPin() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, mapService.savePartnerPin());
-    }
-
-    @Operation(
-            summary = "가게 위치 및 정보를 저장하는 API 입니다.",
-            description = "storeId로 스토어를 조회하고 그 주소로 위치를 저장/갱신합니다."
-    )
-    @PostMapping("/locations/store/{storeId}")
-    public BaseResponse saveStorePin(
-            @PathVariable Long storeId
-    ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, mapService.saveStorePin(storeId));
-    }
-
-    @Operation(
-            summary = "주변 장소를 조회하는 API 입니다.",
-            description = "유저의 타입과 공간 인덱싱에 들어갈 좌표 4개를 경도, 위도 순서로 입력해주세요 (type=user -> store 조회 / type=admin -> partner 조회 / type=partner -> admin 조회)"
+            summary = "주변 장소 조회 API",
+            description = "공간 인덱싱에 들어갈 좌표 4개를 경도, 위도 순서로 입력해주세요 (user -> store 조회 / admin -> partner 조회 / partner -> admin 조회)"
     )
     @GetMapping("/nearby")
     public BaseResponse getLocations(
-            @RequestParam("type") String type,
-            @ModelAttribute MapRequestDTO.ViewOnMapDTO viewport
+            @ModelAttribute MapRequestDTO.ViewOnMapDTO viewport,
+            @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        String t = type.trim().toLowerCase();
+        Long memberId = pd.getMember().getId();
+        UserRole role = pd.getMember().getRole();
 
-        return switch (t) {
-            case "user" -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getStores(viewport));
-            case "admin" -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getPartners(viewport));
-            case "partner" -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getAdmins(viewport));
+        return switch (role) {
+            case STUDENT -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getStores(viewport, memberId));
+            case ADMIN -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getPartners(viewport, memberId));
+            case PARTNER -> BaseResponse.onSuccess(SuccessStatus._OK, mapService.getAdmins(viewport, memberId));
             default -> BaseResponse.onFailure(ErrorStatus._BAD_REQUEST, null);
         };
     }
 
     @Operation(
-            summary = "검색어 기반 장소 조회 API 입니다.",
-            description = "유저의 타입과 검색어를 입력해주세요 (type=user → STORE 전체조회 / type=admin → 제휴중인 PARTNER 조회 / type=partner → 제휴중인 ADMIN 조회)"
+            summary = "검색어 기반 장소 조회 API",
+            description = "검색어를 입력해주세요. (user → store 전체조회 / admin → 제휴중인 partner 조회 / partner → 제휴중인 admin 조회)"
     )
     @GetMapping("/search")
     public BaseResponse search(
-            @RequestParam("type") String type,
-            @RequestParam("q") @NotNull String keyword
+            @RequestParam("searchKeyword") @NotNull String keyword,
+            @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        String t = type.trim().toLowerCase();
-        return switch (t) {
-            case "user" -> {
+        Long memberId = pd.getMember().getId();
+        UserRole role = pd.getMember().getRole();
+
+        return switch (role) {
+            case STUDENT -> {
                 List list = mapService.searchStores(keyword);
                 yield BaseResponse.onSuccess(SuccessStatus._OK, list);
             }
-            case "admin" -> {
-                List list = mapService.searchPartner(keyword);
+            case ADMIN -> {
+                List list = mapService.searchPartner(keyword, memberId);
                 yield BaseResponse.onSuccess(SuccessStatus._OK, list);
             }
-            case "partner" -> {
-                List list = mapService.searchAdmin(keyword);
+            case PARTNER -> {
+                List list = mapService.searchAdmin(keyword, memberId);
                 yield BaseResponse.onSuccess(SuccessStatus._OK, list);
             }
             default -> BaseResponse.onFailure(ErrorStatus._BAD_REQUEST, null);
diff --git a/src/main/java/com/assu/server/domain/map/converter/MapConverter.java b/src/main/java/com/assu/server/domain/map/converter/MapConverter.java
deleted file mode 100644
index 971b053..0000000
--- a/src/main/java/com/assu/server/domain/map/converter/MapConverter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.assu.server.domain.map.converter;
-
-import com.assu.server.domain.map.dto.MapResponseDTO;
-import com.assu.server.domain.map.entity.Location;
-
-public class MapConverter {
-
-    public static MapResponseDTO.SavePinResponseDTO toSavePinResponseDTO(Location location) {
-        return MapResponseDTO.SavePinResponseDTO.builder()
-                .pinId(location.getId())
-                .ownerType(location.getOwnerType().name())
-                .ownerId(location.getOwnerId())
-                .name(location.getName())
-                .address(location.getRoadAddress() != null ? location.getRoadAddress() : location.getAddress())
-                .latitude(location.getLatitude())
-                .longitude(location.getLongitude())
-                .build();
-    }
-}
diff --git a/src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java
index 4e112ce..6b97d05 100644
--- a/src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/map/dto/MapRequestDTO.java
@@ -17,4 +17,14 @@ public static class ViewOnMapDTO {
         private double lng4;
         private double lat4;
     }
+
+    @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
+    public static class ConfirmRequest {
+        private String placeId;
+        private String name;
+        private String address;     // 지번
+        private String roadAddress; // 도로명
+        private Double longitude;   // x
+        private Double latitude;    // y
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
index 03d656f..556411b 100644
--- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
@@ -2,12 +2,10 @@
 
 import com.assu.server.domain.partnership.entity.enums.CriterionType;
 import com.assu.server.domain.partnership.entity.enums.OptionType;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 import java.time.LocalDate;
+import java.util.List;
 
 public class MapResponseDTO {
 
@@ -16,7 +14,6 @@ public class MapResponseDTO {
     @AllArgsConstructor
     @Builder
     public static class PartnerMapResponseDTO {
-        private Long pinId;
         private Long partnerId;
         private String name;
         private String address;
@@ -33,7 +30,6 @@ public static class PartnerMapResponseDTO {
     @AllArgsConstructor
     @Builder
     public static class AdminMapResponseDTO {
-        private Long pinId;
         private Long adminId;
         private String name;
         private String address;
@@ -50,7 +46,6 @@ public static class AdminMapResponseDTO {
     @AllArgsConstructor
     @Builder
     public static class StoreMapResponseDTO {
-        private Long pinId;
         private Long storeId;
         private Long adminId;
         private String name;
@@ -67,17 +62,35 @@ public static class StoreMapResponseDTO {
         private Double longitude;
     }
 
-    @Getter
-    @NoArgsConstructor
-    @AllArgsConstructor
-    @Builder
-    public static class SavePinResponseDTO {
-        private Long pinId;
-        private String ownerType;   // ADMIN / PARTNER / STORE
+    @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
+    public static class PlaceItem {
+        private String placeId;         // Kakao place id (문자열)
+        private String name;            // place_name
+        private String category;        // category_name
+        private String phone;           // phone
+        private String address;         // address_name(지번)
+        private String roadAddress;     // road_address_name(도로명)
+        private Double longitude;       // x
+        private Double latitude;        // y
+        private String distance;        // 기준좌표 주면 미터(문자열)
+        private String placeUrl;        // place_url
+    }
+
+    @Getter @Setter
+    @NoArgsConstructor @AllArgsConstructor @Builder
+    public static class PlaceSearchResponse {
+        private List items;
+        private Integer totalCount;
+        private Boolean isEnd;
+    }
+
+    @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
+    public static class ConfirmResponse {
         private Long ownerId;
+        private String ownerType;   // ADMIN / PARTNER / STORE
         private String name;
-        private String address;
-        private Double latitude;
+        private String address;     // 저장된 대표 주소(도로명 우선)
         private Double longitude;
+        private Double latitude;
     }
 }
diff --git a/src/main/java/com/assu/server/domain/map/entity/Location.java b/src/main/java/com/assu/server/domain/map/entity/Location.java
deleted file mode 100644
index 4712cc3..0000000
--- a/src/main/java/com/assu/server/domain/map/entity/Location.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.assu.server.domain.map.entity;
-
-import com.assu.server.domain.common.entity.BaseEntity;
-import com.assu.server.domain.map.entity.enums.LocationOwnerType;
-import jakarta.persistence.*;
-import lombok.*;
-import org.locationtech.jts.geom.Point;
-
-@Entity
-@Getter @Setter @Builder
-@NoArgsConstructor @AllArgsConstructor
-public class Location extends BaseEntity {
-
-    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private Long Id;
-
-    @Enumerated(EnumType.STRING)
-    private LocationOwnerType ownerType;
-
-    private Long ownerId;
-
-    private String name;
-    private String address;
-    private String roadAddress;
-
-    private Double latitude;
-    private Double longitude;
-
-    @Column(columnDefinition = "POINT SRID 4326")
-    private Point point;
-}
diff --git a/src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java b/src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java
deleted file mode 100644
index 9a15314..0000000
--- a/src/main/java/com/assu/server/domain/map/entity/enums/LocationOwnerType.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.assu.server.domain.map.entity.enums;
-
-public enum LocationOwnerType {
-    ADMIN, PARTNER, STORE
-}
diff --git a/src/main/java/com/assu/server/domain/map/repository/MapRepository.java b/src/main/java/com/assu/server/domain/map/repository/MapRepository.java
deleted file mode 100644
index e5527e6..0000000
--- a/src/main/java/com/assu/server/domain/map/repository/MapRepository.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.assu.server.domain.map.repository;
-
-import com.assu.server.domain.map.entity.Location;
-import com.assu.server.domain.map.entity.enums.LocationOwnerType;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-
-public interface MapRepository extends JpaRepository {
-
-    Optional findByOwnerTypeAndOwnerId(LocationOwnerType ownerType, Long ownerId);
-
-    List findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType ownerType, Collection ownerIds);
-
-    @Query(value = """
-        SELECT l.*
-        FROM location l
-        WHERE l.owner_type = :ownerType
-            AND l.point IS NOT NULL
-            AND ST_Contains(
-                ST_GeomFromText(:wkt, 4326),
-                l.point
-            )
-    """, nativeQuery = true)
-    List findAllByCoordinates(
-            @Param("ownerType") String ownerType,
-            @Param("wkt") String wkt
-    );
-}
diff --git a/src/main/java/com/assu/server/domain/map/service/MapService.java b/src/main/java/com/assu/server/domain/map/service/MapService.java
index 61f10c3..6df2f6a 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapService.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapService.java
@@ -2,20 +2,19 @@
 
 import com.assu.server.domain.map.dto.MapRequestDTO;
 import com.assu.server.domain.map.dto.MapResponseDTO;
-import com.assu.server.domain.map.entity.Location;
 
 import java.util.List;
 
 public interface MapService {
-    MapResponseDTO.SavePinResponseDTO saveAdminPin();
-    MapResponseDTO.SavePinResponseDTO savePartnerPin();
-    MapResponseDTO.SavePinResponseDTO saveStorePin(Long storeId);
-
-    List getAdmins(MapRequestDTO.ViewOnMapDTO viewport);
-    List getPartners(MapRequestDTO.ViewOnMapDTO viewport);
-    List getStores(MapRequestDTO.ViewOnMapDTO viewport);
+    List getAdmins(MapRequestDTO.ViewOnMapDTO viewport, Long memberId);
+    List getPartners(MapRequestDTO.ViewOnMapDTO viewport, Long memberId);
+    List getStores(MapRequestDTO.ViewOnMapDTO viewport, Long memberId);
 
     List   searchStores(String keyword);
-    List searchPartner(String keyword);
-    List   searchAdmin(String keyword);
+    List searchPartner(String keyword, Long memberId);
+    List   searchAdmin(String keyword, Long memberId);
+
+    MapResponseDTO.PlaceSearchResponse search(String query, Double x, Double y, Integer radius, Integer page, Integer size, String sort);
+    MapResponseDTO.ConfirmResponse confirmForAdmin(MapRequestDTO.ConfirmRequest request, Long adminId);
+    MapResponseDTO.ConfirmResponse confirmForPartner(MapRequestDTO.ConfirmRequest request, Long partnerId);
 }
diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index 52740a4..bc4c86b 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -3,12 +3,8 @@
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
 import com.assu.server.domain.common.enums.ActivationStatus;
-import com.assu.server.domain.map.converter.MapConverter;
 import com.assu.server.domain.map.dto.MapRequestDTO;
 import com.assu.server.domain.map.dto.MapResponseDTO;
-import com.assu.server.domain.map.entity.Location;
-import com.assu.server.domain.map.entity.enums.LocationOwnerType;
-import com.assu.server.domain.map.repository.MapRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.domain.partnership.entity.Paper;
@@ -19,7 +15,7 @@
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.config.KakaoLocalClient;
-import com.assu.server.global.exception.exception.DatabaseException;
+import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.locationtech.jts.geom.Coordinate;
@@ -27,18 +23,13 @@
 import org.locationtech.jts.geom.Point;
 import org.springframework.stereotype.Service;
 
-import java.awt.*;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 @Service
 @RequiredArgsConstructor
 public class MapServiceImpl implements MapService {
 
-    private final MapRepository mapRepository;
     private final AdminRepository adminRepository;
     private final PartnerRepository partnerRepository;
     private final StoreRepository storeRepository;
@@ -48,163 +39,79 @@ public class MapServiceImpl implements MapService {
     private final GeometryFactory geometryFactory;
 
     @Override
-    @Transactional
-    public MapResponseDTO.SavePinResponseDTO saveAdminPin() {
-//        Long adminId = SecurityUtil.getCurrentId();
-        Long adminId = 1L;
-
-        Admin admin = adminRepository.findById(adminId)
+    public List getPartners(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) {
+        Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
-        String query = joinAddress(admin.getOfficeAddress(), admin.getDetailAddress());
-        if (query.isBlank()) throw new IllegalArgumentException("관리자 주소가 비어 있습니다.");
-
-        var geo = kakaoLocalClient.geocodeByAddress(query);
-        Location loc = upsert(LocationOwnerType.ADMIN, admin.getId(), admin.getName(), query, geo.getRoadAddress(), geo.getLat(), geo.getLng());
-
-        return MapConverter.toSavePinResponseDTO(loc);
-    }
-
-    @Override
-    @Transactional
-    public MapResponseDTO.SavePinResponseDTO savePartnerPin() {
-        //        Long partnerId = SecurityUtil.getCurrentId();
-        Long partnerId = 2L;
-
-        Partner partner = partnerRepository.findById(partnerId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-
-        String query = joinAddress(partner.getAddress(), partner.getDetailAddress());
-        if (query.isBlank()) throw new IllegalArgumentException("파트너 주소가 비어 있습니다.");
-
-        var geo = kakaoLocalClient.geocodeByAddress(query);
-        Location loc = upsert(LocationOwnerType.PARTNER, partner.getId(), partner.getName(), query, geo.getRoadAddress(), geo.getLat(), geo.getLng());
-
-        return MapConverter.toSavePinResponseDTO(loc);
-    }
-
-    @Override
-    @Transactional
-    public MapResponseDTO.SavePinResponseDTO saveStorePin(Long storeId) {
-        Store store = storeRepository.findById(storeId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
-
-        String query = joinAddress(store.getAddress(), store.getDetailAddress());
-        if (query.isBlank()) throw new IllegalArgumentException("스토어 주소가 비어 있습니다.");
-
-        var geo = kakaoLocalClient.geocodeByAddress(query);
-        Location loc = upsert(LocationOwnerType.STORE, store.getId(), store.getName(), query, geo.getRoadAddress(), geo.getLat(), geo.getLng());
-
-        return MapConverter.toSavePinResponseDTO(loc);
-    }
-
-    @Override
-    public List getPartners(MapRequestDTO.ViewOnMapDTO viewport) {
-//        Long adminId = SecurityUtil.getCurrentId();
-        Long adminId = 1L;
-        Admin admin = adminRepository.findById(adminId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
         String wkt = toWKT(viewport);
+        List partners = partnerRepository.findAllWithinViewport(wkt);
 
-        List pins = mapRepository.findAllByCoordinates(LocationOwnerType.PARTNER.name(), wkt);
-
-        return pins.stream().map(pin -> {
-            Long partnerId = pin.getOwnerId();
-            Partner partner = partnerRepository.findById(partnerId)
-                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-
-            Paper activePaper = paperRepository
-                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(admin.getId(), partnerId, ActivationStatus.ACTIVE)
+        return partners.stream().map(p -> {
+            Paper active = paperRepository.findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(memberId, p.getId(), ActivationStatus.ACTIVE)
                     .orElse(null);
 
-            boolean isPartnered = (activePaper != null);
-            Long partnershipId = (activePaper != null ? activePaper.getId() : null);
-            var start = (activePaper != null ? activePaper.getPartnershipPeriodStart() : null);
-            var end = (activePaper != null ? activePaper.getPartnershipPeriodEnd() : null);
-
             return MapResponseDTO.PartnerMapResponseDTO.builder()
-                    .pinId(pin.getId())
-                    .partnerId(partnerId)
-                    .name(partner != null ? partner.getName() : pin.getName())
-                    .address(pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress())
-                    .isPartnered(isPartnered)
-                    .partnershipId(partnershipId)
-                    .partnershipStartDate(start)
-                    .partnershipEndDate(end)
-                    .latitude(pin.getLatitude())
-                    .longitude(pin.getLongitude())
+                    .partnerId(p.getId())
+                    .name(p.getName())
+                    .address(p.getAddress() != null ? p.getAddress() : p.getDetailAddress())
+                    .isPartnered(active != null)
+                    .partnershipId(active != null ? active.getId() : null)
+                    .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
+                    .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
+                    .latitude(p.getLatitude())
+                    .longitude(p.getLongitude())
                     .build();
         }).toList();
     }
 
     @Override
-    public List getAdmins(MapRequestDTO.ViewOnMapDTO viewport) {
-//        Long partnerId = SecurityUtil.getCurrentId();
-        Long partnerId = 2L;
+    public List getAdmins(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) {
 
-        Partner partner = partnerRepository.findById(partnerId)
+        Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-        String wkt = toWKT(viewport);
 
-        List pins = mapRepository.findAllByCoordinates(LocationOwnerType.ADMIN.name(), wkt);
-
-        return pins.stream().map(pin -> {
-            Long adminId = pin.getOwnerId();
-            Admin admin = adminRepository.findById(adminId)
-                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+        String wkt = toWKT(viewport);
+        List admins = adminRepository.findAllWithinViewport(wkt);
 
-            Paper activePaper = paperRepository
-                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(adminId, partner.getId(), ActivationStatus.ACTIVE)
+        return admins.stream().map(a -> {
+            Paper active = paperRepository.findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(a.getId(), memberId, ActivationStatus.ACTIVE)
                     .orElse(null);
 
-            boolean isPartnered = (activePaper != null);
-            Long partnershipId = (activePaper != null ? activePaper.getId() : null);
-            var start = (activePaper != null ? activePaper.getPartnershipPeriodStart() : null);
-            var end = (activePaper != null ? activePaper.getPartnershipPeriodEnd() : null);
-
             return MapResponseDTO.AdminMapResponseDTO.builder()
-                    .pinId(pin.getId())
-                    .adminId(adminId)
-                    .name(admin != null ? admin.getName() : pin.getName())
-                    .address(pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress())
-                    .isPartnered(isPartnered)
-                    .partnershipId(partnershipId)
-                    .partnershipStartDate(start)
-                    .partnershipEndDate(end)
-                    .latitude(pin.getLatitude())
-                    .longitude(pin.getLongitude())
+                    .adminId(a.getId())
+                    .name(a.getName())
+                    .address(a.getOfficeAddress() != null ? a.getOfficeAddress() : a.getDetailAddress())
+                    .isPartnered(active != null)
+                    .partnershipId(active != null ? active.getId() : null)
+                    .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
+                    .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
+                    .latitude(a.getLatitude())
+                    .longitude(a.getLongitude())
                     .build();
         }).toList();
     }
 
     @Override
-    public List getStores(MapRequestDTO.ViewOnMapDTO viewport) {
+    public List getStores(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) {
         String wkt = toWKT(viewport);
+        List stores = storeRepository.findAllWithinViewport(wkt);
 
-        List pins = mapRepository.findAllByCoordinates(LocationOwnerType.STORE.name(), wkt);
-
-        return pins.stream().map(pin -> {
-            Long storeId = pin.getOwnerId();
-            Store store = storeRepository.findById(storeId)
-                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
-
-            boolean hasPartner = store.getPartner() != null;
+        return stores.stream().map(s -> {
+            boolean hasPartner = (s.getPartner() != null);
 
-            PaperContent content = paperContentRepository
-                    .findTopByPaperStoreIdOrderByIdDesc(storeId)
-                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_CONTENT));
+            PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId())
+                    .orElse(null);
 
-            Long adminId = paperRepository.findTopPaperByStoreId(storeId)
+            Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
                     .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
-                    .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+                    .orElse(null);
 
             return MapResponseDTO.StoreMapResponseDTO.builder()
-                    .pinId(pin.getId())
-                    .storeId(storeId)
+                    .storeId(s.getId())
                     .adminId(adminId)
-                    .name(store.getName())
-                    .address(pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress())
-                    .rate(store.getRate())
+                    .name(s.getName())
+                    .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress())
+                    .rate(s.getRate())
                     .criterionType(content != null ? content.getCriterionType() : null)
                     .optionType(content != null ? content.getOptionType() : null)
                     .people(content != null ? content.getPeople() : null)
@@ -212,8 +119,8 @@ public List getStores(MapRequestDTO.ViewOnMa
                     .category(content != null ? content.getCategory() : null)
                     .discountRate(content != null ? content.getDiscount() : null)
                     .hasPartner(hasPartner)
-                    .latitude(pin.getLatitude())
-                    .longitude(pin.getLongitude())
+                    .latitude(s.getLatitude())
+                    .longitude(s.getLongitude())
                     .build();
         }).toList();
     }
@@ -222,28 +129,20 @@ public List getStores(MapRequestDTO.ViewOnMa
     public List searchStores(String keyword) {
         List stores = storeRepository.findByNameContainingIgnoreCaseOrderByIdDesc(keyword);
 
-        Map locationMap = mapRepository
-                .findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType.STORE, stores.stream().map(Store::getId).toList())
-                .stream().collect(Collectors.toMap(Location::getOwnerId, Function.identity()));
-
-        List result =  new ArrayList<>();
-        for(Store s : stores) {
-            Location pin = locationMap.get(s.getId());
-
-            Paper latest = paperRepository.findTopPaperByStoreId(s.getId()).orElse(null);
-            Long adminId = (latest != null && latest.getAdmin() != null) ? latest.getAdmin().getId() : null;
+        return stores.stream().map(s -> {
+            boolean hasPartner = s.getPartner() != null;
+            PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId())
+                    .orElse(null);
 
-            PaperContent content = paperContentRepository
-                    .findTopByPaperStoreIdOrderByIdDesc(s.getId())
+            Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
+                    .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
                     .orElse(null);
 
-            result.add(MapResponseDTO.StoreMapResponseDTO.builder()
-                    .pinId(pin != null ? pin.getId() : null)
+            return MapResponseDTO.StoreMapResponseDTO.builder()
                     .storeId(s.getId())
                     .adminId(adminId)
                     .name(s.getName())
-                    .address(pin != null
-                            ? (pin.getRoadAddress() != null ? pin.getRoadAddress() : pin.getAddress()) : null)
+                    .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress())
                     .rate(s.getRate())
                     .criterionType(content != null ? content.getCriterionType() : null)
                     .optionType(content != null ? content.getOptionType() : null)
@@ -251,86 +150,129 @@ public List searchStores(String keyword) {
                     .cost(content != null ? content.getCost() : null)
                     .category(content != null ? content.getCategory() : null)
                     .discountRate(content != null ? content.getDiscount() : null)
-                    .hasPartner(s.getPartner() != null)
-                    .latitude(pin != null ? pin.getLatitude() : null)
-                    .longitude(pin != null ? pin.getLongitude() : null)
-                    .build());
-        }
-        return result;
+                    .hasPartner(hasPartner)
+                    .latitude(s.getLatitude())
+                    .longitude(s.getLongitude())
+                    .build();
+        }).toList();
     }
 
     @Override
-    public List searchPartner(String keyword) {
-//        Long adminId = SecurityUtil.getCurrentId();
-        Long adminId = 1L;
+    public List searchPartner(String keyword, Long memberId) {
 
-        List partners = paperRepository
-                .findActivePartnersForAdminByKeyword(adminId, ActivationStatus.ACTIVE, keyword);
+        Admin admin = adminRepository.findById(memberId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
-        Map locationMap = mapRepository
-                .findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType.PARTNER,
-                        partners.stream().map(Partner::getId).toList())
-                .stream().collect(Collectors.toMap(Location::getOwnerId, Function.identity()));
+        List partners = partnerRepository.searchPartneredByName(memberId, ActivationStatus.ACTIVE, keyword);
 
-        List result =  new ArrayList<>();
-        for(Partner p : partners) {
-            Location pin = locationMap.get(p.getId());
-            Paper active = paperRepository
-                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(adminId, p.getId(), ActivationStatus.ACTIVE)
-                    .orElse(null);
+        return partners.stream().map(p -> {
+                Paper active = paperRepository
+                                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(memberId, p.getId(), ActivationStatus.ACTIVE)
+                                    .orElse(null);
 
-            result.add(MapResponseDTO.PartnerMapResponseDTO.builder()
-                            .pinId(pin != null ? pin.getId() : null)
-                            .partnerId(p.getId())
-                            .name(p.getName())
-                            .address(pin != null && pin.getRoadAddress() != null ? pin.getRoadAddress() : (pin != null ? pin.getAddress() : null))
-                            .isPartnered(active != null)
-                            .partnershipId(active != null ? active.getId() : null)
-                            .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
-                            .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
-                            .latitude(pin != null ? pin.getLatitude() : null)
-                            .longitude(pin != null ? pin.getLongitude() : null)
-                    .build());
-        }
-        return result;
+                return MapResponseDTO.PartnerMapResponseDTO.builder()
+                    .partnerId(p.getId())
+                    .name(p.getName())
+                    .address(p.getAddress() != null ? p.getAddress() : p.getDetailAddress())
+                    .isPartnered(true)
+                    .partnershipId(active != null ? active.getId() : null)
+                    .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
+                    .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
+                    .latitude(p.getLatitude())
+                    .longitude(p.getLongitude())
+                    .build();
+        }).toList();
     }
 
     @Override
-    public List searchAdmin(String keyword) {
-//        Long partnerId = SecurityUtil.getCurrentId();
-        Long partnerId = 2L;
+    public List searchAdmin(String keyword, Long memberId) {
 
-        List admins = paperRepository
-                .findActiveAdminsForPartnerByKeyword(partnerId, ActivationStatus.ACTIVE, keyword);
+        Partner partner = partnerRepository.findById(memberId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
 
-        Map locMap = mapRepository
-                .findAllByOwnerTypeAndOwnerIdIn(LocationOwnerType.ADMIN,
-                        admins.stream().map(Admin::getId).toList())
-                .stream().collect(Collectors.toMap(Location::getOwnerId, Function.identity()));
+        List admins = adminRepository.searchPartneredByName(memberId, ActivationStatus.ACTIVE, keyword);
 
-        List result = new ArrayList<>();
-        for (Admin a : admins) {
-            Location pin = locMap.get(a.getId());
+        return admins.stream().map(a -> {
             Paper active = paperRepository
-                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(
-                            a.getId(), partnerId, ActivationStatus.ACTIVE)
+                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(a.getId(), memberId, ActivationStatus.ACTIVE)
                     .orElse(null);
 
-            result.add(MapResponseDTO.AdminMapResponseDTO.builder()
-                    .pinId(pin != null ? pin.getId() : null)
+            return MapResponseDTO.AdminMapResponseDTO.builder()
                     .adminId(a.getId())
                     .name(a.getName())
-                    .address(pin != null && pin.getRoadAddress() != null ? pin.getRoadAddress()
-                            : (pin != null ? pin.getAddress() : null))
-                    .isPartnered(active != null)
+                    .address(a.getOfficeAddress() != null ? a.getOfficeAddress() : a.getDetailAddress())
+                    .isPartnered(true)
                     .partnershipId(active != null ? active.getId() : null)
                     .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
                     .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
-                    .latitude(pin != null ? pin.getLatitude() : null)
-                    .longitude(pin != null ? pin.getLongitude() : null)
-                    .build());
-        }
-        return result;
+                    .latitude(a.getLatitude())
+                    .longitude(a.getLongitude())
+                    .build();
+        }).toList();
+    }
+
+    @Override
+    public MapResponseDTO.PlaceSearchResponse search(String query, Double x, Double y, Integer radius, Integer page, Integer size, String sort) {
+        var resp = kakaoLocalClient.searchByKeyword(query, x, y, radius, page, size, sort);
+        var items = resp.getDocuments().stream().map(d ->
+                MapResponseDTO.PlaceItem.builder()
+                        .placeId(d.getId())
+                        .name(d.getPlace_name())
+                        .category(d.getCategory_name())
+                        .phone(d.getPhone())
+                        .address(d.getAddress_name())
+                        .roadAddress(d.getRoad_address_name())
+                        .longitude(d.getX() != null ? Double.valueOf(d.getX()) : null)
+                        .latitude(d.getY() != null ? Double.valueOf(d.getY()) : null)
+                        .distance(d.getDistance())
+                        .placeUrl(d.getPlace_url())
+                        .build()
+        ).collect(Collectors.toList());
+
+        return MapResponseDTO.PlaceSearchResponse.builder()
+                .items(items)
+                .totalCount(resp.getMeta() != null ? resp.getMeta().getTotal_count() : null)
+                .isEnd(resp.getMeta() != null ? resp.getMeta().getIs_end() : null)
+                .build();
+    }
+
+    @Override
+    @Transactional
+    public MapResponseDTO.ConfirmResponse confirmForAdmin(MapRequestDTO.ConfirmRequest request, Long adminId) {
+        Admin admin = adminRepository.findById(adminId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+        admin.setLatitude(request.getLatitude());
+        admin.setLongitude(request.getLongitude());
+        admin.setPoint(toPoint(request.getLongitude(), request.getLatitude())); // SRID=4326
+        // (주소를 바꿀지 여부는 정책대로) - 도로명이 있으면 대표주소로 사용
+        String display = pickDisplayAddress(request.getRoadAddress(), request.getAddress());
+        if (display != null) admin.setOfficeAddress(display);
+        return MapResponseDTO.ConfirmResponse.builder()
+                .ownerId(admin.getId()).ownerType("ADMIN")
+                .name(admin.getName())
+                .address(display)
+                .longitude(admin.getLongitude())
+                .latitude(admin.getLatitude())
+                .build();
+    }
+
+    @Override
+    @Transactional
+    public MapResponseDTO.ConfirmResponse confirmForPartner(MapRequestDTO.ConfirmRequest request, Long partnerId) {
+        Partner partner = partnerRepository.findById(partnerId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+        partner.setLatitude(request.getLatitude());
+        partner.setLongitude(request.getLongitude());
+        partner.setPoint(toPoint(request.getLongitude(), request.getLatitude()));
+        String display = pickDisplayAddress(request.getRoadAddress(), request.getAddress());
+        if (display != null) partner.setAddress(display);
+        return MapResponseDTO.ConfirmResponse.builder()
+                .ownerId(partner.getId()).ownerType("PARTNER")
+                .name(partner.getName())
+                .address(display)
+                .longitude(partner.getLongitude())
+                .latitude(partner.getLatitude())
+                .build();
     }
 
     private String toWKT(MapRequestDTO.ViewOnMapDTO v) {
@@ -344,27 +286,14 @@ private String toWKT(MapRequestDTO.ViewOnMapDTO v) {
         );
     }
 
-    private String joinAddress(String addr, String detail) {
-        String a = (addr == null) ? "" : addr.trim();
-        String d = (detail == null) ? "" : detail.trim();
-        return (a + " " + d).trim();
-    }
-
-    public Location upsert(LocationOwnerType ownerType, Long ownerId, String name, String plainAddress, String roadAddress, Double lat, Double lng) {
-
-        Location loc = mapRepository.findByOwnerTypeAndOwnerId(ownerType, ownerId)
-                .orElseGet(() -> Location.builder().ownerType(ownerType).ownerId(ownerId).build());
-
-        loc.setName(name);
-        loc.setAddress(plainAddress);
-        loc.setRoadAddress(roadAddress);
-        loc.setLatitude(lat);
-        loc.setLongitude(lng);
-
+    private Point toPoint(Double lng, Double lat) {
+        if (lng == null || lat == null) return null;
         Point p = geometryFactory.createPoint(new Coordinate(lng, lat));
-        loc.setPoint(p);
-
-        return mapRepository.save(loc);
+        p.setSRID(4326);
+        return p;
     }
 
+    private String pickDisplayAddress(String road, String jibun) {
+        return (road != null && !road.isBlank()) ? road : jibun;
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/partner/entity/Partner.java b/src/main/java/com/assu/server/domain/partner/entity/Partner.java
index 9fc67ca..260b946 100644
--- a/src/main/java/com/assu/server/domain/partner/entity/Partner.java
+++ b/src/main/java/com/assu/server/domain/partner/entity/Partner.java
@@ -8,6 +8,9 @@
 import jakarta.persistence.OneToOne;
 import jakarta.persistence.Id;
 import lombok.*;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+import org.locationtech.jts.geom.Point;
 
 import java.time.LocalDateTime;
 
@@ -38,4 +41,10 @@ public class Partner {
     private Boolean isLicenseVerified;
 
     private LocalDateTime licenseVerifiedAt;
+
+    @JdbcTypeCode(SqlTypes.GEOMETRY)
+    private Point point;
+
+    private double latitude;
+    private double longitude;
 }
diff --git a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java
index ae4ef46..a75a657 100644
--- a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java
+++ b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java
@@ -1,7 +1,37 @@
 package com.assu.server.domain.partner.repository;
 
+import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partner.entity.Partner;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
 
 public interface PartnerRepository extends JpaRepository {
+
+    @Query(value = """
+        SELECT p.*
+        FROM partner p
+        WHERE p.point IS NOT NULL
+          AND ST_Contains(ST_GeomFromText(:wkt, 4326), p.point)
+        """, nativeQuery = true)
+    List findAllWithinViewport(@Param("wkt") String wkt);
+
+    @Query("""
+        select distinct p
+        from Partner p
+        where lower(p.name) like lower(concat('%', :keyword, '%'))
+          and exists (
+              select 1 from Paper pc
+              where pc.partner = p
+                and pc.admin.id = :adminId
+                and pc.isActivated = :status
+          )
+        """)
+    List searchPartneredByName(
+            @Param("adminId") Long adminId,
+            @Param("status") ActivationStatus status,
+            @Param("keyword") String keyword
+    );
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
index bd60c56..37fb7af 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
@@ -18,30 +18,4 @@ Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(
     );
 
     Optional findTopPaperByStoreId(Long storeId);
-
-    // 로그인 admin과 활성 제휴 중이며 파트너 이름이 키워드와 매칭
-    @Query("""
-        select distinct p.partner
-        from Paper p
-        where p.admin.id = :adminId
-          and p.isActivated = :status
-          and lower(p.partner.name) like lower(concat('%', :keyword, '%'))
-        order by p.id desc
-    """)
-    List findActivePartnersForAdminByKeyword(@Param("adminId") Long adminId,
-                                                      @Param("status") ActivationStatus status,
-                                                      @Param("keyword") String keyword);
-
-    // 로그인 partner와 활성 제휴 중이며 관리자 이름이 키워드와 매칭
-    @Query("""
-        select distinct p.admin
-        from Paper p
-        where p.partner.id = :partnerId
-          and p.isActivated = :status
-          and lower(p.admin.name) like lower(concat('%', :keyword, '%'))
-        order by p.id desc
-    """)
-    List findActiveAdminsForPartnerByKeyword(@Param("partnerId") Long partnerId,
-                                                    @Param("status") ActivationStatus status,
-                                                    @Param("keyword") String keyword);
 }
diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java
index 733f5fe..178806c 100644
--- a/src/main/java/com/assu/server/domain/store/entity/Store.java
+++ b/src/main/java/com/assu/server/domain/store/entity/Store.java
@@ -16,6 +16,9 @@
 import lombok.Builder;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+import org.locationtech.jts.geom.Point;
 
 
 @Entity
@@ -43,4 +46,19 @@ public class Store extends BaseEntity {
 
 	private String detailAddress;
 
+	@JdbcTypeCode(SqlTypes.GEOMETRY)
+	private Point point;
+
+	private double latitude;
+	private double longitude;
+
+	public void linkPartner(Partner partner) {
+		this.partner = partner;
+	}
+	public void setGeo(Double lat, Double lng, Point point) {
+		this.latitude = lat;
+		this.longitude = lng;
+		this.point = point;
+	}
+
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index 86d2aaf..4f2c866 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -2,9 +2,35 @@
 
 import com.assu.server.domain.store.entity.Store;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
 import java.util.List;
+import java.util.Optional;
 
 public interface StoreRepository extends JpaRepository {
+
+    @Query("""
+        select s
+        from Store s
+        where lower(s.address) = lower(:address)
+          and (
+                (:detail is null and (s.detailAddress is null or s.detailAddress = ''))
+             or (lower(coalesce(s.detailAddress, '')) = lower(coalesce(:detail, '')))
+          )
+        """)
+    Optional findBySameAddress(
+            @Param("address") String address,
+            @Param("detail") String detail
+    );
+
+    @Query(value = """
+        SELECT s.*
+        FROM store s
+        WHERE s.point IS NOT NULL
+          AND ST_Contains(ST_GeomFromText(:wkt, 4326), s.point)
+        """, nativeQuery = true)
+    List findAllWithinViewport(@Param("wkt") String wkt);
+
     List findByNameContainingIgnoreCaseOrderByIdDesc(String name);
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index b72008e..0604fac 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -75,8 +75,10 @@ public enum ErrorStatus implements BaseErrorCode {
 
     // 주소 에러
     NO_SUCH_ADDRESS(HttpStatus.NOT_FOUND, "ADDRESS_7001", "주소를 찾을 수 없습니다."),
+    // Paper 에러
+    NO_SUCH_PAPER(HttpStatus.NOT_FOUND, "PAPER_4001", "제휴를 찾을 수 없습니다."),
     // PaperContent 에러
-    NO_SUCH_CONTENT(HttpStatus.NOT_FOUND, "CONTENT_8001", "제휴 내용을 찾을 수 없습니다."),
+    NO_SUCH_CONTENT(HttpStatus.NOT_FOUND, "PAPER_4002", "제휴 내용을 찾을 수 없습니다."),
 
     ;
 
diff --git a/src/main/java/com/assu/server/global/config/KakaoLocalClient.java b/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
index ec93149..4864f9a 100644
--- a/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
+++ b/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
@@ -1,9 +1,8 @@
 package com.assu.server.global.config;
 
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.exception.exception.GeneralException;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
 import org.springframework.stereotype.Component;
 import org.springframework.web.reactive.function.client.WebClient;
 import org.springframework.web.util.UriBuilder;
@@ -15,6 +14,7 @@
 public class KakaoLocalClient {
     private final WebClient kakaoWebClient;
 
+    /* ---------- 기존 키워드 검색 ---------- */
     @Data
     public static class KakaoKeywordResp {
         private List documents;
@@ -26,8 +26,8 @@ public static class KakaoKeywordResp {
             private String phone;
             private String address_name;
             private String road_address_name;
-            private String x; // 경도 문자열
-            private String y; // 위도 문자열
+            private String x; // 경도
+            private String y; // 위도
             private String place_url;
             private String distance; // 기준좌표 주면 미터 문자열
         }
@@ -35,59 +35,78 @@ public static class KakaoKeywordResp {
     }
 
     public KakaoKeywordResp searchByKeyword(String query, Double x, Double y,
-                                            Integer radius, Integer page, Integer size) {
+                                            Integer radius, Integer page, Integer size, String sort) {
         return kakaoWebClient.get()
                 .uri(uri -> {
                     UriBuilder b = uri.path("/v2/local/search/keyword.json")
                             .queryParam("query", query)
                             .queryParam("page", page == null ? 1 : page)
-                            .queryParam("size", size == null ? 15 : size); // 카카오 최대 15
+                            .queryParam("size", size == null ? 15 : size);
                     if (x != null && y != null) {
                         b.queryParam("x", x).queryParam("y", y);
                         if (radius != null) b.queryParam("radius", radius);
+                        if (sort != null) b.queryParam("sort", sort); // accuracy|distance
                     }
                     return b.build();
                 })
+                .accept(MediaType.APPLICATION_JSON)
                 .retrieve()
                 .bodyToMono(KakaoKeywordResp.class)
                 .block();
     }
 
+    /* ---------- 주소 검색 ---------- */
     @Data
     public static class KakaoAddressResp {
         private List documents;
-        @Data
-        public static class Document {
-            private String x;                 // 경도
-            private String y;                 // 위도
-            private RoadAddress road_address; // 있을 수도/없을 수도
-        }
-        @Data
-        public static class RoadAddress {
-            private String address_name;      // 도로명 전체
-        }
-    }
+        private Meta meta;
+        @Data public static class Document {
+            // 도로명주소
+            private RoadAddress road_address;
+            // 지번주소
+            private Address address;
 
-    @Data
-    public static class Geo {
-        private final Double lat;         // y (latitude)
-        private final Double lng;         // x (longitude)
-        private final String roadAddress; // nullable
+            @Data public static class RoadAddress {
+                private String address_name;
+                private String region_1depth_name;
+                private String region_2depth_name;
+                private String region_3depth_name;
+                private String road_name;
+                private String underground_yn;
+                private String main_building_no;
+                private String sub_building_no;
+                private String building_name;
+                private String zone_no;
+                private Double x; // 경도
+                private Double y; // 위도
+            }
+
+            @Data public static class Address {
+                private String address_name;
+                private String region_1depth_name;
+                private String region_2depth_name;
+                private String region_3depth_name;
+                private String mountain_yn;
+                private String main_address_no;
+                private String sub_address_no;
+                private Double x; // 경도
+                private Double y; // 위도
+            }
+        }
+        @Data public static class Meta { private Integer total_count; }
     }
 
-    public Geo geocodeByAddress(String query) {
-        KakaoAddressResp resp = kakaoWebClient.get()
-                .uri(u -> u.path("/v2/local/search/address.json").queryParam("query", query).build())
+    public KakaoAddressResp searchAddress(String query, Integer page, Integer size) {
+        return kakaoWebClient.get()
+                .uri(uri -> uri.path("/v2/local/search/address.json")
+                        .queryParam("query", query)
+                        .queryParam("page", page == null ? 1 : page)
+                        .queryParam("size", size == null ? 15 : size)
+                        .build())
+                .accept(MediaType.APPLICATION_JSON)
                 .retrieve()
                 .bodyToMono(KakaoAddressResp.class)
                 .block();
-        if (resp == null || resp.getDocuments() == null || resp.getDocuments().isEmpty())
-            throw new GeneralException(ErrorStatus.NO_SUCH_ADDRESS);
-        var d = resp.getDocuments().get(0);
-        Double lng = Double.valueOf(d.getX());
-        Double lat = Double.valueOf(d.getY());
-        String road = d.getRoad_address() == null ? null : d.getRoad_address().getAddress_name();
-        return new Geo(lat, lng, road);
     }
 }
 

From 709ca7586170489990ca14d3dbb5315fc308403d Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Wed, 27 Aug 2025 18:39:38 +0900
Subject: [PATCH 101/270] =?UTF-8?q?[Feat/#24]=20=20-=20=EC=9E=A5=EC=86=8C?=
 =?UTF-8?q?=20=EA=B2=80=EC=83=89=20api=20=EC=B6=94=EA=B0=80=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84=20=20-=20admin/store=20=EC=A3=BC=EC=86=8C=20=EC=84=A0?=
 =?UTF-8?q?=ED=83=9D=20=EC=8B=9C=20json=20=ED=98=95=EC=8B=9D=EC=9C=BC?=
 =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../dto/signup/common/CommonInfoPayload.java  |   3 +
 .../auth/service/SignUpServiceImpl.java       |  77 +++++++-----
 .../domain/map/controller/MapController.java  |  17 ++-
 .../server/domain/map/dto/MapResponseDTO.java |  38 ++----
 .../domain/map/dto/SelectedPlacePayload.java  |  18 +++
 .../server/domain/map/service/MapService.java |   4 -
 .../domain/map/service/MapServiceImpl.java    |  64 ----------
 .../map/service/PlaceSearchService.java       |  10 ++
 .../map/service/PlaceSearchServiceImpl.java   |  98 +++++++++++++++
 .../server/domain/store/entity/Store.java     |   6 +-
 .../store/repository/StoreRepository.java     |  13 +-
 .../global/config/KakaoLocalClient.java       | 117 ++++++++++--------
 .../server/global/config/SecurityConfig.java  |   3 +
 13 files changed, 272 insertions(+), 196 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/map/dto/SelectedPlacePayload.java
 create mode 100644 src/main/java/com/assu/server/domain/map/service/PlaceSearchService.java
 create mode 100644 src/main/java/com/assu/server/domain/map/service/PlaceSearchServiceImpl.java

diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java
index f9b8cb0..65f7add 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonInfoPayload.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.auth.dto.signup.common;
 
+import com.assu.server.domain.map.dto.SelectedPlacePayload;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Size;
 import lombok.AllArgsConstructor;
@@ -20,4 +21,6 @@ public class CommonInfoPayload {
 
     @Size(max = 255)
     private String detailAddress;
+
+    private SelectedPlacePayload selectedPlace;
 }
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 5530733..4a98d56 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
@@ -32,6 +32,7 @@
 import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
+import java.util.Optional;
 
 
 @Service
@@ -153,10 +154,14 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
         String licenseUrl = amazonS3Manager.uploadFile(keyName, licenseImage);
         CommonInfoPayload info = req.getCommonInfo();
 
-        String query = joinAddress(info.getAddress(), info.getDetailAddress());
-        var geo = kakaoLocalClient.searchAddress(query, null, null);
-        Double lat = geo != null ? geo.getLat() : null;
-        Double lng = geo != null ? geo.getLng() : null;
+        Double lat = null, lng = null;
+        String displayAddr = info.getAddress();
+        if (info.getSelectedPlace() != null) {
+            var sp = info.getSelectedPlace();
+            lat = sp.getLatitude();
+            lng = sp.getLongitude();
+            displayAddr = pickDisplayAddress(sp.getRoadAddress(), sp.getAddress());
+        }
         Point point = toPoint(lat, lng);
 
         // 3) Partner 프로필 생성
@@ -164,7 +169,7 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
                 Partner.builder()
                         .member(member)
                         .name(info.getName())
-                        .address(info.getAddress())
+                        .address(displayAddr)
                         .detailAddress(info.getDetailAddress())
                         .licenseUrl(licenseUrl)
                         .point(point)
@@ -173,26 +178,28 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
                         .build()
         );
 
-        storeRepository.findBySameAddress(info.getAddress(), info.getDetailAddress())
-                .ifPresentOrElse(store -> {
-                    if (store.getPartner() != null || store.getPartner().getId().equals(partner.getId())) {
-                        store.linkPartner(partner);
-                        storeRepository.save(store);
-                    }
-                }, () -> {
-                    Store newly = Store.builder()
-                            .partner(partner)
-                            .rate(0)
-                            .isActivate(ActivationStatus.ACTIVE)
-                            .name(info.getName())
-                            .address(info.getAddress())
-                            .detailAddress(info.getDetailAddress())
-                            .latitude(lat)
-                            .longitude(lng)
-                            .point(point)
-                            .build();
-                    storeRepository.save(newly);
-                });
+        // store 생성/연결
+        Optional storeOpt = storeRepository.findBySameAddress(displayAddr, info.getDetailAddress());
+        if (storeOpt.isPresent()) {
+            Store store = storeOpt.get();
+            store.linkPartner(partner);
+            store.setName(info.getName());
+            store.setGeo(lat, lng, point);
+            storeRepository.save(store);
+        } else {
+            Store newly = Store.builder()
+                    .partner(partner)
+                    .rate(0)
+                    .isActivate(ActivationStatus.ACTIVE)
+                    .name(info.getName())
+                    .address(displayAddr)
+                    .detailAddress(info.getDetailAddress())
+                    .latitude(lat)
+                    .longitude(lng)
+                    .point(point)
+                    .build();
+            storeRepository.save(newly);
+        }
 
         // 4) 토큰 발급
         Tokens tokens = jwtUtil.issueTokens(
@@ -238,10 +245,14 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
         String signUrl = amazonS3Manager.uploadFile(keyName, signImage);
         CommonInfoPayload info = req.getCommonInfo();
 
-        String query = joinAddress(info.getAddress(), info.getDetailAddress());
-        var geo = kakaoLocalClient.geocodeByAddress(query);
-        Double lat = geo != null ? geo.getLat() : null;
-        Double lng = geo != null ? geo.getLng() : null;
+        Double lat = null, lng = null;
+        String displayAddr = info.getAddress();
+        if (info.getSelectedPlace() != null) {
+            var sp = info.getSelectedPlace();
+            lat = sp.getLatitude();
+            lng = sp.getLongitude();
+            displayAddr = pickDisplayAddress(sp.getRoadAddress(), sp.getAddress());
+        }
         Point point = toPoint(lat, lng);
 
         // 3) Partner 프로필 생성
@@ -249,7 +260,7 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
                 Admin.builder()
                         .member(member)
                         .name(info.getName())
-                        .officeAddress(info.getAddress())
+                        .officeAddress(displayAddr)
                         .detailAddress(info.getDetailAddress())
                         .signUrl(signUrl)
                         .point(point)
@@ -281,9 +292,7 @@ public Point toPoint(Double lat, Double lng) {
         return p;
     }
 
-    private String joinAddress(String addr, String detail) {
-        String a = (addr == null) ? "" : addr.trim();
-        String d = (detail == null) ? "" : detail.trim();
-        return (a + " " + d).trim();
+    private String pickDisplayAddress(String road, String jibun) {
+        return (road != null && !road.isBlank()) ? road : jibun;
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/map/controller/MapController.java b/src/main/java/com/assu/server/domain/map/controller/MapController.java
index a51cea9..86f948b 100644
--- a/src/main/java/com/assu/server/domain/map/controller/MapController.java
+++ b/src/main/java/com/assu/server/domain/map/controller/MapController.java
@@ -4,6 +4,7 @@
 import com.assu.server.domain.map.dto.MapRequestDTO;
 import com.assu.server.domain.map.dto.MapResponseDTO;
 import com.assu.server.domain.map.service.MapService;
+import com.assu.server.domain.map.service.PlaceSearchService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
@@ -22,6 +23,7 @@
 public class MapController {
 
     private final MapService mapService;
+    private final PlaceSearchService placeSearchService;
 
     @Operation(
             summary = "주변 장소 조회 API",
@@ -48,7 +50,7 @@ public BaseResponse getLocations(
             description = "검색어를 입력해주세요. (user → store 전체조회 / admin → 제휴중인 partner 조회 / partner → 제휴중인 admin 조회)"
     )
     @GetMapping("/search")
-    public BaseResponse search(
+    public BaseResponse getLocationsByKeyword(
             @RequestParam("searchKeyword") @NotNull String keyword,
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
@@ -72,4 +74,17 @@ public BaseResponse search(
         };
     }
 
+    @Operation(
+            summary = "주소 입력 시 장소 검색용 API",
+            description = "검색어를 기반으로 장소를 검색하여 리스트로 반환합니다. limit로 개수를 제한할 수 있습니다."
+    )
+    @GetMapping("/place")
+    public BaseResponse> search(
+            @RequestParam("searchKeyword") String query,
+            @RequestParam(value = "limit", required = false) Integer size
+    ) {
+        List list = placeSearchService.unifiedSearch(query, size);
+        return BaseResponse.onSuccess(SuccessStatus._OK, list);
+    }
+
 }
diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
index 556411b..90d78f2 100644
--- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
@@ -62,35 +62,17 @@ public static class StoreMapResponseDTO {
         private Double longitude;
     }
 
-    @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
-    public static class PlaceItem {
-        private String placeId;         // Kakao place id (문자열)
+    @Getter @NoArgsConstructor @AllArgsConstructor @Builder
+    public static class PlaceSuggestionDTO {
+        private String placeId;         // kakao place id
         private String name;            // place_name
-        private String category;        // category_name
-        private String phone;           // phone
-        private String address;         // address_name(지번)
-        private String roadAddress;     // road_address_name(도로명)
-        private Double longitude;       // x
+        private String category;        // category_name or category_group_name
+        private String address;         // 지번 주소
+        private String roadAddress;     // 도로명 주소
+        private String phone;           // 전화
+        private String placeUrl;        // 카카오 상세 URL
         private Double latitude;        // y
-        private String distance;        // 기준좌표 주면 미터(문자열)
-        private String placeUrl;        // place_url
-    }
-
-    @Getter @Setter
-    @NoArgsConstructor @AllArgsConstructor @Builder
-    public static class PlaceSearchResponse {
-        private List items;
-        private Integer totalCount;
-        private Boolean isEnd;
-    }
-
-    @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
-    public static class ConfirmResponse {
-        private Long ownerId;
-        private String ownerType;   // ADMIN / PARTNER / STORE
-        private String name;
-        private String address;     // 저장된 대표 주소(도로명 우선)
-        private Double longitude;
-        private Double latitude;
+        private Double longitude;       // x
+        private Integer distance;       // m (좌표바이어스/카테고리 검색 시 제공)
     }
 }
diff --git a/src/main/java/com/assu/server/domain/map/dto/SelectedPlacePayload.java b/src/main/java/com/assu/server/domain/map/dto/SelectedPlacePayload.java
new file mode 100644
index 0000000..bbe70a5
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/dto/SelectedPlacePayload.java
@@ -0,0 +1,18 @@
+package com.assu.server.domain.map.dto;
+
+import lombok.*;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SelectedPlacePayload {
+
+    private String placeId;
+    private String name;
+    private String address;
+    private String roadAddress;
+    private Double longitude;
+    private Double latitude;
+}
diff --git a/src/main/java/com/assu/server/domain/map/service/MapService.java b/src/main/java/com/assu/server/domain/map/service/MapService.java
index 6df2f6a..2448293 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapService.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapService.java
@@ -13,8 +13,4 @@ public interface MapService {
     List   searchStores(String keyword);
     List searchPartner(String keyword, Long memberId);
     List   searchAdmin(String keyword, Long memberId);
-
-    MapResponseDTO.PlaceSearchResponse search(String query, Double x, Double y, Integer radius, Integer page, Integer size, String sort);
-    MapResponseDTO.ConfirmResponse confirmForAdmin(MapRequestDTO.ConfirmRequest request, Long adminId);
-    MapResponseDTO.ConfirmResponse confirmForPartner(MapRequestDTO.ConfirmRequest request, Long partnerId);
 }
diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index bc4c86b..58b0af3 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -211,70 +211,6 @@ public List searchAdmin(String keyword, Long
         }).toList();
     }
 
-    @Override
-    public MapResponseDTO.PlaceSearchResponse search(String query, Double x, Double y, Integer radius, Integer page, Integer size, String sort) {
-        var resp = kakaoLocalClient.searchByKeyword(query, x, y, radius, page, size, sort);
-        var items = resp.getDocuments().stream().map(d ->
-                MapResponseDTO.PlaceItem.builder()
-                        .placeId(d.getId())
-                        .name(d.getPlace_name())
-                        .category(d.getCategory_name())
-                        .phone(d.getPhone())
-                        .address(d.getAddress_name())
-                        .roadAddress(d.getRoad_address_name())
-                        .longitude(d.getX() != null ? Double.valueOf(d.getX()) : null)
-                        .latitude(d.getY() != null ? Double.valueOf(d.getY()) : null)
-                        .distance(d.getDistance())
-                        .placeUrl(d.getPlace_url())
-                        .build()
-        ).collect(Collectors.toList());
-
-        return MapResponseDTO.PlaceSearchResponse.builder()
-                .items(items)
-                .totalCount(resp.getMeta() != null ? resp.getMeta().getTotal_count() : null)
-                .isEnd(resp.getMeta() != null ? resp.getMeta().getIs_end() : null)
-                .build();
-    }
-
-    @Override
-    @Transactional
-    public MapResponseDTO.ConfirmResponse confirmForAdmin(MapRequestDTO.ConfirmRequest request, Long adminId) {
-        Admin admin = adminRepository.findById(adminId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
-        admin.setLatitude(request.getLatitude());
-        admin.setLongitude(request.getLongitude());
-        admin.setPoint(toPoint(request.getLongitude(), request.getLatitude())); // SRID=4326
-        // (주소를 바꿀지 여부는 정책대로) - 도로명이 있으면 대표주소로 사용
-        String display = pickDisplayAddress(request.getRoadAddress(), request.getAddress());
-        if (display != null) admin.setOfficeAddress(display);
-        return MapResponseDTO.ConfirmResponse.builder()
-                .ownerId(admin.getId()).ownerType("ADMIN")
-                .name(admin.getName())
-                .address(display)
-                .longitude(admin.getLongitude())
-                .latitude(admin.getLatitude())
-                .build();
-    }
-
-    @Override
-    @Transactional
-    public MapResponseDTO.ConfirmResponse confirmForPartner(MapRequestDTO.ConfirmRequest request, Long partnerId) {
-        Partner partner = partnerRepository.findById(partnerId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-        partner.setLatitude(request.getLatitude());
-        partner.setLongitude(request.getLongitude());
-        partner.setPoint(toPoint(request.getLongitude(), request.getLatitude()));
-        String display = pickDisplayAddress(request.getRoadAddress(), request.getAddress());
-        if (display != null) partner.setAddress(display);
-        return MapResponseDTO.ConfirmResponse.builder()
-                .ownerId(partner.getId()).ownerType("PARTNER")
-                .name(partner.getName())
-                .address(display)
-                .longitude(partner.getLongitude())
-                .latitude(partner.getLatitude())
-                .build();
-    }
-
     private String toWKT(MapRequestDTO.ViewOnMapDTO v) {
         return String.format(
                 "POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))",
diff --git a/src/main/java/com/assu/server/domain/map/service/PlaceSearchService.java b/src/main/java/com/assu/server/domain/map/service/PlaceSearchService.java
new file mode 100644
index 0000000..f16f4fd
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/service/PlaceSearchService.java
@@ -0,0 +1,10 @@
+package com.assu.server.domain.map.service;
+
+import com.assu.server.domain.map.dto.MapResponseDTO;
+
+import java.util.List;
+
+public interface PlaceSearchService {
+
+    List unifiedSearch(String query, Integer size);
+}
diff --git a/src/main/java/com/assu/server/domain/map/service/PlaceSearchServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/PlaceSearchServiceImpl.java
new file mode 100644
index 0000000..243d509
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/service/PlaceSearchServiceImpl.java
@@ -0,0 +1,98 @@
+package com.assu.server.domain.map.service;
+
+import com.assu.server.domain.map.dto.MapResponseDTO;
+import com.assu.server.global.config.KakaoLocalClient;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+@Service
+@RequiredArgsConstructor
+public class PlaceSearchServiceImpl implements PlaceSearchService {
+
+    private final KakaoLocalClient kakaoLocalClient;
+    private static final int NEARBY_DEFAULT_RADIUS = 500;
+
+    @Override
+    public List unifiedSearch(String query, Integer size) {
+        int kSize = (size == null ? 15 : size);
+
+        // 1) 주소로도 시도 → 좌표 얻기 (성공/실패 무관)
+        Double x = null, y = null; // 경도/위도
+        KakaoLocalClient.KakaoAddressResp addrResp = kakaoLocalClient.searchByAddress(query, 1, 5);
+        if (addrResp != null && addrResp.getDocuments() != null && !addrResp.getDocuments().isEmpty()) {
+            var d = addrResp.getDocuments().get(0);
+            // road_address 우선, 없으면 address
+            String sx = d.getRoad_address() != null ? d.getRoad_address().getX()
+                    : (d.getAddress() != null ? d.getAddress().getX() : d.getX());
+            String sy = d.getRoad_address() != null ? d.getRoad_address().getY()
+                    : (d.getAddress() != null ? d.getAddress().getY() : d.getY());
+            if (sx != null && sy != null) {
+                try {
+                    x = Double.parseDouble(sx);
+                    y = Double.parseDouble(sy);
+                } catch (NumberFormatException ignore) {}
+            }
+        }
+
+        // 2) 키워드 검색 (좌표가 있으면 바이어스)
+        var kw = kakaoLocalClient.searchByKeyword(query, x, y, null, 1, kSize);
+        List kwList = convertKeyword(kw);
+
+        // 3) 좌표가 있으면 카테고리 근접 검색 보강 (음식점/카페 등)
+        List nearby = Collections.emptyList();
+        if (x != null && y != null) {
+            List cats = List.of("FD6", "CE7"); // 음식점/카페 (필요시 카테고리 추가)
+            List merged = new ArrayList<>();
+            for (String c : cats) {
+                var r = kakaoLocalClient.searchByCategory(c, x, y, NEARBY_DEFAULT_RADIUS, 1, kSize);
+                merged.addAll(convertKeyword(r));
+            }
+            // 거리 오름차순
+            merged.sort(Comparator.comparing(dto -> Optional.ofNullable(dto.getDistance()).orElse(Integer.MAX_VALUE)));
+            nearby = merged;
+        }
+
+        // 4) 결과 합치기 (키워드 우선 → 근접 결과 추가, id로 dedupe)
+        Map dedupe = new LinkedHashMap<>();
+        Stream.concat(kwList.stream(), nearby.stream())
+                .forEach(dto -> dedupe.putIfAbsent(dto.getPlaceId(), dto));
+
+        // 최종 상위 size 개 제한
+        return dedupe.values().stream().limit(kSize).toList();
+    }
+
+    private List convertKeyword(KakaoLocalClient.KakaoKeywordResp resp) {
+        if (resp == null || resp.getDocuments() == null) return List.of();
+        List out = new ArrayList<>();
+        for (var d : resp.getDocuments()) {
+            Double x = safeParse(d.getX());
+            Double y = safeParse(d.getY());
+            Integer dist = safeParseInt(d.getDistance()); // null 가능
+            out.add(MapResponseDTO.PlaceSuggestionDTO.builder()
+                    .placeId(d.getId())
+                    .name(d.getPlace_name())
+                    .category(d.getCategory_group_name() != null ? d.getCategory_group_name() : d.getCategory_name())
+                    .address(d.getAddress_name())
+                    .roadAddress(d.getRoad_address_name())
+                    .phone(d.getPhone())
+                    .placeUrl(d.getPlace_url())
+                    .longitude(x)
+                    .latitude(y)
+                    .distance(dist)
+                    .build());
+        }
+        return out;
+    }
+
+    private Double safeParse(String s) {
+        if (s == null) return null;
+        try { return Double.parseDouble(s); } catch (NumberFormatException e) { return null; }
+    }
+    private Integer safeParseInt(String s) {
+        if (s == null) return null;
+        try { return Integer.parseInt(s); } catch (NumberFormatException e) { return null; }
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java
index 178806c..64a1236 100644
--- a/src/main/java/com/assu/server/domain/store/entity/Store.java
+++ b/src/main/java/com/assu/server/domain/store/entity/Store.java
@@ -12,10 +12,7 @@
 import jakarta.persistence.Id;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.ManyToOne;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import lombok.*;
 import org.hibernate.annotations.JdbcTypeCode;
 import org.hibernate.type.SqlTypes;
 import org.locationtech.jts.geom.Point;
@@ -40,6 +37,7 @@ public class Store extends BaseEntity {
 	@Enumerated(EnumType.STRING)
 	private ActivationStatus isActivate;
 
+	@Setter
 	private String name;
 
 	private String address;
diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index 4f2c866..53f048b 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -11,14 +11,11 @@
 public interface StoreRepository extends JpaRepository {
 
     @Query("""
-        select s
-        from Store s
-        where lower(s.address) = lower(:address)
-          and (
-                (:detail is null and (s.detailAddress is null or s.detailAddress = ''))
-             or (lower(coalesce(s.detailAddress, '')) = lower(coalesce(:detail, '')))
-          )
-        """)
+        SELECT s FROM Store s
+        WHERE s.address = :address
+          AND ((:detail IS NULL AND s.detailAddress IS NULL) OR s.detailAddress = :detail)
+    """)
+
     Optional findBySameAddress(
             @Param("address") String address,
             @Param("detail") String detail
diff --git a/src/main/java/com/assu/server/global/config/KakaoLocalClient.java b/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
index 4864f9a..b6483ee 100644
--- a/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
+++ b/src/main/java/com/assu/server/global/config/KakaoLocalClient.java
@@ -12,9 +12,10 @@
 @Component
 @RequiredArgsConstructor
 public class KakaoLocalClient {
+
     private final WebClient kakaoWebClient;
 
-    /* ---------- 기존 키워드 검색 ---------- */
+    /* ========= 공용 DTO ========= */
     @Data
     public static class KakaoKeywordResp {
         private List documents;
@@ -23,19 +24,52 @@ public static class KakaoKeywordResp {
             private String id;
             private String place_name;
             private String category_name;
+            private String category_group_code;
+            private String category_group_name;
             private String phone;
-            private String address_name;
-            private String road_address_name;
-            private String x; // 경도
-            private String y; // 위도
+            private String address_name;       // 지번
+            private String road_address_name;  // 도로명
+            private String x;                  // 경도
+            private String y;                  // 위도
             private String place_url;
-            private String distance; // 기준좌표 주면 미터 문자열
+            private String distance;           // 좌표 바이어스/카테고리 검색시 제공 (문자열 m)
+        }
+        @Data public static class Meta {
+            private Integer total_count;
+            private Boolean is_end;
         }
-        @Data public static class Meta { private Integer total_count; private Boolean is_end; }
     }
 
+    @Data
+    public static class KakaoAddressResp {
+        private List documents;
+        private Meta meta;
+        @Data public static class Document {
+            private Address address;
+            private RoadAddress road_address;
+            private String x;  // 일부 응답에는 상위에 x/y가 직접 들어오기도 함 (카카오 문서 참고)
+            private String y;
+
+            @Data public static class Address {
+                private String address_name;
+                private String x;
+                private String y;
+            }
+            @Data public static class RoadAddress {
+                private String address_name;
+                private String x;
+                private String y;
+            }
+        }
+        @Data public static class Meta {
+            private Integer total_count;
+            private Boolean is_end;
+        }
+    }
+
+    /* ========= 1) 키워드 검색 ========= */
     public KakaoKeywordResp searchByKeyword(String query, Double x, Double y,
-                                            Integer radius, Integer page, Integer size, String sort) {
+                                            Integer radius, Integer page, Integer size) {
         return kakaoWebClient.get()
                 .uri(uri -> {
                     UriBuilder b = uri.path("/v2/local/search/keyword.json")
@@ -45,7 +79,6 @@ public KakaoKeywordResp searchByKeyword(String query, Double x, Double y,
                     if (x != null && y != null) {
                         b.queryParam("x", x).queryParam("y", y);
                         if (radius != null) b.queryParam("radius", radius);
-                        if (sort != null) b.queryParam("sort", sort); // accuracy|distance
                     }
                     return b.build();
                 })
@@ -55,58 +88,36 @@ public KakaoKeywordResp searchByKeyword(String query, Double x, Double y,
                 .block();
     }
 
-    /* ---------- 주소 검색 ---------- */
-    @Data
-    public static class KakaoAddressResp {
-        private List documents;
-        private Meta meta;
-        @Data public static class Document {
-            // 도로명주소
-            private RoadAddress road_address;
-            // 지번주소
-            private Address address;
-
-            @Data public static class RoadAddress {
-                private String address_name;
-                private String region_1depth_name;
-                private String region_2depth_name;
-                private String region_3depth_name;
-                private String road_name;
-                private String underground_yn;
-                private String main_building_no;
-                private String sub_building_no;
-                private String building_name;
-                private String zone_no;
-                private Double x; // 경도
-                private Double y; // 위도
-            }
-
-            @Data public static class Address {
-                private String address_name;
-                private String region_1depth_name;
-                private String region_2depth_name;
-                private String region_3depth_name;
-                private String mountain_yn;
-                private String main_address_no;
-                private String sub_address_no;
-                private Double x; // 경도
-                private Double y; // 위도
-            }
-        }
-        @Data public static class Meta { private Integer total_count; }
-    }
-
-    public KakaoAddressResp searchAddress(String query, Integer page, Integer size) {
+    /* ========= 2) 주소 지오코딩 ========= */
+    public KakaoAddressResp searchByAddress(String query, Integer page, Integer size) {
         return kakaoWebClient.get()
                 .uri(uri -> uri.path("/v2/local/search/address.json")
                         .queryParam("query", query)
                         .queryParam("page", page == null ? 1 : page)
-                        .queryParam("size", size == null ? 15 : size)
+                        .queryParam("size", size == null ? 10 : size)
                         .build())
                 .accept(MediaType.APPLICATION_JSON)
                 .retrieve()
                 .bodyToMono(KakaoAddressResp.class)
                 .block();
     }
-}
 
+    /* ========= 3) 카테고리 근접 검색 ========= */
+    public KakaoKeywordResp searchByCategory(String categoryGroupCode,
+                                             Double x, Double y,
+                                             Integer radius, Integer page, Integer size) {
+        return kakaoWebClient.get()
+                .uri(uri -> uri.path("/v2/local/search/category.json")
+                        .queryParam("category_group_code", categoryGroupCode)
+                        .queryParam("x", x)
+                        .queryParam("y", y)
+                        .queryParam("radius", radius == null ? 500 : radius) // m
+                        .queryParam("page", page == null ? 1 : page)
+                        .queryParam("size", size == null ? 15 : size)
+                        .build())
+                .accept(MediaType.APPLICATION_JSON)
+                .retrieve()
+                .bodyToMono(KakaoKeywordResp.class)
+                .block();
+    }
+}
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 bc449a4..6d40c74 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -36,6 +36,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/auth/phone-numbers/**"
                         ).permitAll()
 
+                        // 지도 API 공개
+                        .requestMatchers("/map/**").permitAll()
+
                         // 로그아웃은 인증 필요
                         .requestMatchers("/auth/logout").authenticated()
 

From 809f64505af7ccedb6485ad4e62f0f8525012aaa Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Thu, 28 Aug 2025 01:16:49 +0900
Subject: [PATCH 102/270] =?UTF-8?q?[MOD/#13]=20S3=20=ED=8C=8C=EC=9D=BC=20?=
 =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../review/controller/ReviewController.java   | 22 ++++--
 .../domain/review/dto/ReviewRequestDTO.java   | 19 ++++-
 .../server/domain/review/entity/Review.java   |  8 +-
 .../domain/review/entity/ReviewPhoto.java     |  8 ++
 .../domain/review/service/ReviewService.java  |  3 +-
 .../review/service/ReviewServiceImpl.java     | 74 +++++++++++++------
 .../apiPayload/code/status/ErrorStatus.java   |  3 +-
 7 files changed, 102 insertions(+), 35 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
index 312809d..c71f982 100644
--- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
+++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
@@ -7,10 +7,16 @@
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Encoding;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
+import org.springframework.web.multipart.MultipartFile;
 import java.util.List;
 
 @RestController
@@ -19,20 +25,22 @@
 public class ReviewController {
     private final ReviewService reviewService;
     @Operation(
-            summary = "리뷰 작성 API입니다.",
+            summary = "리뷰 작성 API",
             description = "리뷰 내용과 별점, 리뷰 이미지를 입력해주세요."
+
     )
-    @PostMapping()
+    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse writeReview(
             @AuthenticationPrincipal PrincipalDetails pd,
-            @RequestBody ReviewRequestDTO.WriteReviewRequestDTO writeReviewRequestDTO
+            @RequestPart("request") ReviewRequestDTO.WriteReviewRequestDTO request,
+            @RequestPart(value = "reviewImages", required = false) List reviewImages
     ) {
         Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(writeReviewRequestDTO, memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(request, memberId, reviewImages));
     }
 
     @Operation(
-            summary = "내가 쓴 리뷰 조회 API입니다.",
+            summary = "내가 쓴 리뷰 조회 API",
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/student")
@@ -44,7 +52,7 @@ public BaseResponse> check
     }
 
     @Operation(
-            summary = "내가 쓴 리뷰 삭제 API입니다.",
+            summary = "내가 쓴 리뷰 삭제 API",
             description = "삭제할 리뷰 ID를 입력해주세요."
     )
     @DeleteMapping("/{reviewId}")
@@ -57,7 +65,7 @@ public BaseResponse deleteReview(
     }
 
     @Operation(
-            summary = "내 가게 리뷰 조회 API입니다.",
+            summary = "내 가게 리뷰 조회 API",
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/partner")
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
index 676ab55..5dfa6be 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
@@ -1,21 +1,32 @@
 package com.assu.server.domain.review.dto;
 
 import com.assu.server.domain.review.entity.ReviewPhoto;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
 public class    ReviewRequestDTO {
     @Getter
+    @Setter
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
     public static class WriteReviewRequestDTO {
+        @Schema(description = "리뷰 내용", example = "정말 맛있었어요!")
         private String content;
+
+        @Schema(description = "별점 (1-10)", example = "5", minimum = "1", maximum = "10")
         private Integer rate;
+
+        @Schema(hidden = true)
         private List reviewImage;
+
+        @Schema(description = "가게 ID", example = "3")
         private Long storeId;
+
+        @Schema(description = "파트너 ID", example = "2")
         private Long partnerId;
     }
 }
diff --git a/src/main/java/com/assu/server/domain/review/entity/Review.java b/src/main/java/com/assu/server/domain/review/entity/Review.java
index 0045706..51a6c5f 100644
--- a/src/main/java/com/assu/server/domain/review/entity/Review.java
+++ b/src/main/java/com/assu/server/domain/review/entity/Review.java
@@ -44,8 +44,14 @@ public class Review extends BaseEntity {
 	@JoinColumn(name = "store_id")
 	private Store store;
 
-	@OneToMany(mappedBy= "review", cascade = CascadeType.ALL)
+	@OneToMany(mappedBy = "review", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
 	private List imageList = new ArrayList<>();
+	public List getImageList() {
+		if (imageList == null) {
+			imageList = new ArrayList<>();
+		}
+		return imageList;
+	}
 
 	private Integer rate;
 	private String content;
diff --git a/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java b/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java
index 187b36a..ab49c15 100644
--- a/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java
+++ b/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java
@@ -31,4 +31,12 @@ public class ReviewPhoto extends BaseEntity {
 	private Review review;
 
 	private String photoUrl;
+
+	@JoinColumn(name = "key_name") // S3 키 이름 저장 (조회 시 새 URL 생성용)
+	private String keyName;
+
+	public void updatePhotoUrl(String newPhotoUrl) {
+		this.photoUrl = newPhotoUrl; //일시적 저장
+	}
+
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
index 1600e07..16c808d 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
@@ -4,11 +4,12 @@
 import com.assu.server.domain.review.dto.ReviewResponseDTO;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
 public interface ReviewService {
-    ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId);
+    ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages);
     List checkStudentReview(Long memberId);
     List checkPartnerReview(Long memberId);
     ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(@PathVariable Long reviewId);
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 712b58f..0fab833 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -24,7 +24,11 @@
 import org.springframework.web.multipart.MultipartFile;
 
 import java.sql.SQLOutput;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
 
 @Service
 @RequiredArgsConstructor
@@ -37,49 +41,65 @@ public class ReviewServiceImpl implements ReviewService {
 
 
     @Override
-    public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId) {
-        Long storeId = request.getStoreId(); //변수 선언
-        List images = request.getReviewImage(); // 이미지 변수 선언
+    public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages) {
+        // createReview 메서드 호출로 통합
+        Review review = createReview(memberId, request.getStoreId(), request, reviewImages);
+        return ReviewConverter.writeReviewResultDTO(review);
+    }
 
-        //존재여부 검증
+    private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteReviewRequestDTO request, List images) {
+        // 존재여부 검증
         Store store = storeRepository.findById(storeId)
-                .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); //없을 경우!!
-        Partner partner = partnerRepository.findById(request.getPartnerId()) //파라미터 변수 선언 없이 바로 받기
-                .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+        Partner partner = partnerRepository.findById(request.getPartnerId())
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
         Student student = studentRepository.findById(memberId)
-                .orElseThrow(()-> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
 
+        // 리뷰 엔티티 생성 및 저장
         Review review = ReviewConverter.toReviewEntity(request, store, partner, student);
+        reviewRepository.save(review); // ID 생성을 위해 먼저 저장
 
-        reviewRepository.save(review); //먼저 Review 저장
-
-        //이미지 업로드 처리
+        // 이미지 처리
         if (images != null && !images.isEmpty()) {
             try {
-                for (MultipartFile image : images) {
-                    String keyName = amazonS3Manager.generateKeyName("review-images");
-                    amazonS3Manager.uploadFile(keyName, image);
+                for (int i = 0; i < images.size(); i++) {
+                    String keyName = generateReviewImageKeyName(memberId, review.getId(), i + 1);
+                    amazonS3Manager.uploadFile(keyName, images.get(i));
                     String presignedUrl = amazonS3Manager.generatePresignedUrl(keyName);
 
                     ReviewPhoto reviewPhoto = ReviewPhoto.builder()
                             .review(review)
                             .photoUrl(presignedUrl)
+                            .keyName(keyName)
                             .build();
+
                     review.getImageList().add(reviewPhoto);
                 }
             } catch (Exception e) {
-                throw new DatabaseException(ErrorStatus.IMAGE_UPLOAD_FAILED); //ErrorStatus에 추가 필요
+                throw new DatabaseException(ErrorStatus.IMAGE_UPLOAD_FAILED);
             }
         }
-        reviewRepository.save(review);//rep에서 데이터 상하차 저장
-        //잘 저장 됏어요!!
-        return ReviewConverter.writeReviewResultDTO(review);//객체를 dto로 바꿔서 사용자에게 보여줌 -> controller
+
+        return reviewRepository.save(review);
+    }    private String generateReviewImageKeyName(Long memberId, Long reviewId, int imageIndex) {
+        LocalDateTime now = LocalDateTime.now();
+        String year = String.valueOf(now.getYear());
+        String month = String.format("%02d", now.getMonthValue());
+
+        // 기존 generateKeyName 방식을 참고하되 더 체계적으로
+        return String.format("reviews/images/%s/%s/user%d/review%d_img%d_%s",
+                year, month, memberId, reviewId, imageIndex, UUID.randomUUID());
     }
 
     @Override
     public List checkStudentReview(Long memberId) {
         List reviews = reviewRepository.findByMemberId(memberId);
 
+        for (Review review : reviews) {
+            updateReviewImageUrls(review);
+        }
+
         return ReviewConverter.checkStudentReviewResultDTO(reviews);
     }
 
@@ -88,12 +108,15 @@ public List checkStudentReview(
     public List checkPartnerReview(Long memberId) {
         Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-        System.out.println("파트너 id는 "+partner.getId());
         Store store = storeRepository.findByPartner(partner)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
 
-        //List reviews = reviewRepository.findByStoreId(store.getId());
-        List reviews = reviewRepository.findByStoreIdOrderByCreatedAtDesc(store.getId()); //최신순 정렬
+        List reviews = reviewRepository.findByStoreIdOrderByCreatedAtDesc(store.getId());
+
+        for (Review review : reviews) {
+            updateReviewImageUrls(review);
+        }
+
         return ReviewConverter.checkPartnerReviewResultDTO(reviews);
     }
     @Override
@@ -102,4 +125,13 @@ public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) {
         reviewRepository.deleteById(reviewId);
         return ReviewConverter.deleteReviewResultDTO(reviewId);
     }
+    private void updateReviewImageUrls(Review review) {
+        for (ReviewPhoto reviewPhoto : review.getImageList()) {
+            if (reviewPhoto.getKeyName() != null) {
+                String freshUrl = amazonS3Manager.generatePresignedUrl(reviewPhoto.getKeyName());
+                // ReviewPhoto 엔티티에 URL 업데이트 (일시적으로, DB에는 저장하지 않음)
+                reviewPhoto.updatePhotoUrl(freshUrl);
+            }
+        }
+    }
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 6c2a048..cc8c282 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -49,7 +49,8 @@ public enum ErrorStatus implements BaseErrorCode {
     EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."),
 
     //리뷰 이미지 에러
-    IMAGE_UPLOAD_FAILED(HttpStatus.NOT_FOUND,"REVIEW_4001", "존재하지 않는 리뷰이미지 입니다"),
+    IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"REVIEW_4001", "리뷰 이미지 업로드에 실패했습니다"),
+    IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND,"REVIEW_4001", "존재하지 않는 리뷰이미지 입니다"),
     // 채팅 에러
     NO_SUCH_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5001", "존재하지 않는 채팅방 ID 입니다."),
     NO_MEMBER_IN_THE_ROOM(HttpStatus.NOT_FOUND, "CHATTING_5002", "해당 방에는 해당 사용자가 없습니다."),

From b781d55d896f7fd447cf8d976f5ce49f640dc4e8 Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Thu, 28 Aug 2025 14:43:15 +0900
Subject: [PATCH 103/270] =?UTF-8?q?[Feat/#14]=20=20-=20storeRepository=20?=
 =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/partnership/service/PartnershipServiceImpl.java      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 2d9dcf4..95c52ea 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
@@ -61,7 +61,7 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipAsPart
         Admin admin = adminRepository.findById(request.getAdminId())
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
-        Store store = storeRepository.findByPartner_Id(partner.getId())
+        Store store = storeRepository.findByPartner(partner)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
 
         return writePartnership(request, admin, partner, store);

From a03beea198b3d95ce1ab68f530326ab1e0b7a8a6 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Thu, 28 Aug 2025 20:05:08 +0900
Subject: [PATCH 104/270] =?UTF-8?q?=EA=B7=B8=EB=A3=B9=20=EC=9D=B8=EC=A6=9D?=
 =?UTF-8?q?=20=EC=8B=9C=20auth=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../config/CertifyWebSocketConfig.java        | 11 ++++++
 .../config/StompAuthChannelInterceptor.java   | 37 +++++++++++++++++++
 .../controller/CertificationController.java   |  6 +--
 3 files changed, 50 insertions(+), 4 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java

diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 71628c9..06e797f 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -1,14 +1,20 @@
 package com.assu.server.domain.certification.config;
 
 import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.ChannelRegistration;
 import org.springframework.messaging.simp.config.MessageBrokerRegistry;
 import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
 import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
 import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
 
+import lombok.RequiredArgsConstructor;
+
 @EnableWebSocketMessageBroker
 @Configuration
+@RequiredArgsConstructor
 public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+	private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
 	@Override
 	public void configureMessageBroker(MessageBrokerRegistry config) {
 		config.enableSimpleBroker("/certification/progress"); // 인증현황을 받아보기 위한 구독 주소
@@ -20,4 +26,9 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
 		registry.addEndpoint("/ws")           // 클라이언트 WebSocket 연결 주소
 			.setAllowedOriginPatterns("*").withSockJS(); // CORS 허용
 	}
+
+	@Override
+	public void configureClientInboundChannel(ChannelRegistration registration) {
+		registration.interceptors(stompAuthChannelInterceptor);
+	}
 }
diff --git a/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
new file mode 100644
index 0000000..d2eb1c0
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
@@ -0,0 +1,37 @@
+package com.assu.server.domain.certification.config;
+
+import com.assu.server.domain.auth.security.jwt.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.messaging.*;
+import org.springframework.messaging.simp.stomp.*;
+import org.springframework.messaging.support.ChannelInterceptor;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class StompAuthChannelInterceptor implements ChannelInterceptor {
+
+	private final JwtUtil jwtUtil;
+
+	@Override
+	public Message preSend(Message message, MessageChannel channel) {
+		StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
+
+		if (StompCommand.CONNECT.equals(accessor.getCommand())) {
+			// 프론트에서 connect 시 Authorization 헤더 넣어야 함
+			String authHeader = accessor.getFirstNativeHeader("Authorization");
+			if (authHeader != null && authHeader.startsWith("Bearer ")) {
+				String token = jwtUtil.getTokenFromHeader(authHeader);
+
+				// JwtUtil 이용해서 Authentication 복원
+				Authentication authentication = jwtUtil.getAuthentication(token);
+
+				// WebSocket 세션에 Authentication(UserPrincipal) 저장
+				accessor.setUser(authentication);
+			}
+		}
+
+		return message;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
index a9e3437..7cc301b 100644
--- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
+++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
@@ -50,12 +50,10 @@ public ResponseEntity> certifyGroup(
-		CertificationRequestDTO.groupSessionRequest dto  // 나중에 여기에 Security + WebSocket 설정 완료한 후
-		// @AuthenticationPrincipal 넣어주기
+		CertificationRequestDTO.groupSessionRequest dto  , PrincipalDetails pd
 
 	) {
-		Member member = memberRepository.findMemberById(4L).orElseThrow(
-			() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER));
+		Member member = pd.getMember();
 
 		certificationService.handleCertification(dto, member);
 

From 377ab2319380f16044d1e1de6b5e3d4e261f0b6f Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Fri, 29 Aug 2025 22:52:50 +0900
Subject: [PATCH 105/270] =?UTF-8?q?=EC=A0=9C=ED=9C=B4=EB=82=B4=EC=97=AD=20?=
 =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../partnership/controller/PartnershipController.java       | 3 ++-
 .../domain/partnership/service/PartnershipServiceImpl.java  | 1 +
 .../server/domain/user/controller/StudentController.java    | 6 +++++-
 .../server/global/apiPayload/code/status/SuccessStatus.java | 1 +
 .../java/com/assu/server/global/config/SecurityConfig.java  | 5 ++++-
 5 files changed, 13 insertions(+), 3 deletions(-)

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 1dad2f4..cf02703 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
@@ -28,12 +28,13 @@
 @RestController
 @Tag(name = "제휴 요청 api", description = "최종적으로 @@ 제휴를 요청할때 사용하는 api ")
 @RequiredArgsConstructor
+@RequestMapping("/partnership")
 public class PartnershipController {
 
 	private final PartnershipService partnershipService;
 
 
-	@PostMapping("/parntership/usage")
+	@PostMapping("/usage")
 	@Operation(summary= "유저의 인증 후 최종적으로 호출", description = "인증완료 화면 전에 바로 호출되어 유저의 제휴 내역에 데이터가 들어가게 됩니다. (개인 인증인 경우도 포함됩니다.)")
 	public ResponseEntity> finalPartnershipRequest(
 		@AuthenticationPrincipal PrincipalDetails userDetails,@RequestBody PartnershipRequestDTO.finalRequest dto
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 627dbca..67eeede 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
@@ -64,6 +64,7 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe
 
 		// 1) 요청한 member 본인
 		usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile()));
+        member.getStudentProfile().setStamp();
 
 		List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList());
 		// 2) dto의 userIds에 있는 다른 사용자들
diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
index 1ba8788..acd2714 100644
--- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java
+++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
@@ -6,6 +6,7 @@
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RestController;
 
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.user.dto.StudentResponseDTO;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.util.PrincipalDetails;
@@ -31,7 +32,10 @@ public class StudentController {
 	public ResponseEntity> getMyPartnership(
 		@PathVariable int year, @PathVariable int month, @AuthenticationPrincipal PrincipalDetails userDetails
 	){
-		return null;
+		Member member = userDetails.getMember();
+		StudentResponseDTO.myPartnership result = studentService.getMyPartnership(member.getId(), year, month);
+
+		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PARTNERSHIP_HISTORY_SUCCESS, result));
 	}
 
 
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
index 533e25a..4e185c4 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
@@ -36,6 +36,7 @@ public enum SuccessStatus implements BaseCode {
     PAPER_STORE_HISTORY_SUCCESS(HttpStatus.OK, "PAPER201", "가게 별 제휴 내용이 성공적으로 조회되었습니다."),
     USER_PAPER_REQUEST_SUCCESS(HttpStatus.OK, "PAPER202", "제휴 요청이 성공적으로 처리되었습니다."),
 
+    PARTNERSHIP_HISTORY_SUCCESS(HttpStatus.OK, "PARTNERSHIP202", "월 별 제휴 사용내역이 성공적으로 조회되었습니다."),
 
     // 그룹 인증
     GROUP_SESSION_CREATE(HttpStatus.OK, "GROUP201", "인증 세션 생성 및 대표자 구독이 완료되었습니다."),
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 e2e1054..db6339d 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -42,7 +42,10 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
 								"/sub/**",
 								"/auth/**",
 								"/certification/**",
-								"/store/**")
+								"/store/**",
+							"/proposal",
+							"/partnership/**",
+							"/user/**")
 						.permitAll()
 
                         // 로그아웃은 인증 필요

From 4041f0694b27c2ae50908399208c1b4717a0c832 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Fri, 29 Aug 2025 22:55:37 +0900
Subject: [PATCH 106/270] =?UTF-8?q?=EC=A0=9C=ED=9C=B4=EB=82=B4=EC=97=AD=20?=
 =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../partnership/service/PartnershipServiceImpl.java      | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

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 67eeede..88f1023 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
@@ -69,9 +69,12 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe
 		List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList());
 		// 2) dto의 userIds에 있는 다른 사용자들
 		for (Long userId : userIds) {
-			Student student = studentRepository.getReferenceById(userId);
-			usages.add(PartnershipConverter.toPartnershipUsage(dto, student));
-			student.setStamp();
+            if(userId != member.getId()){
+                Student student = studentRepository.getReferenceById(userId);
+                usages.add(PartnershipConverter.toPartnershipUsage(dto, student));
+                student.setStamp();
+            }
+
 		}
 
 		partnershipUsageRepository.saveAll(usages);

From 2d06d943dd8df7459784b711965a855eee37845d Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Sat, 30 Aug 2025 00:03:37 +0900
Subject: [PATCH 107/270] =?UTF-8?q?[MOD/#24]=20=20-=20student=20=ED=85=8C?=
 =?UTF-8?q?=EC=9D=B4=EB=B8=94=EC=97=90=EC=84=9C=20studentNumber=20?=
 =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/com/assu/server/domain/user/entity/Student.java | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java
index 3a76296..e63aa8f 100644
--- a/src/main/java/com/assu/server/domain/user/entity/Student.java
+++ b/src/main/java/com/assu/server/domain/user/entity/Student.java
@@ -41,8 +41,6 @@ public class Student {
 
     private int stamp;
 
-    private Long studentNumber;
-
     @Enumerated(EnumType.STRING)
     private Major major;
 

From 54d68d5ef4c140a04505df9763b3179145d2fadf Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 31 Aug 2025 22:39:31 +1000
Subject: [PATCH 108/270] =?UTF-8?q?[FEAT/#42]=20-=20controller=20PD=20?=
 =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95=20?=
 =?UTF-8?q?-=20SecurityConfig=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/DeviceTokenController.java     |  8 +++---
 .../controller/NotificationController.java    |  9 +++----
 .../server/global/config/SecurityConfig.java  | 27 +++++--------------
 3 files changed, 12 insertions(+), 32 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index 1a632d5..79475e5 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -25,11 +25,10 @@ public class DeviceTokenController {
     @PostMapping("/register")
     public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
                                          @Valid @RequestBody DeviceTokenRequest req) {
-        Long memberId = pd.getMember().getId();
-        service.register(req.getToken(), memberId);
+        service.register(req.getToken(), pd.getMemberId());
         return BaseResponse.onSuccess(
                 SuccessStatus._OK,
-                "Device token registered successfully. memberId=" + memberId
+                "Device token registered successfully. memberId=" + pd.getMemberId()
         );
     }
 
@@ -40,8 +39,7 @@ public BaseResponse register(@AuthenticationPrincipal PrincipalDetails p
     @DeleteMapping("/unregister/{tokenId}")
     public BaseResponse unregister(@AuthenticationPrincipal PrincipalDetails pd,
                                            @PathVariable Long tokenId) {
-        Long memberId = pd.getMember().getId();
-        service.unregister(tokenId, memberId); // 소유자 검증을 서비스에서 수행하도록 memberId 전달
+        service.unregister(tokenId, pd.getMemberId()); // 소유자 검증을 서비스에서 수행하도록 memberId 전달
         return BaseResponse.onSuccess(
                 SuccessStatus._OK,
                 "Device token unregistered successfully. tokenId=" + tokenId
diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 1f8f7b6..7e2bf0b 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -38,8 +38,7 @@ public BaseResponse> list(
             @RequestParam(defaultValue = "1") Integer page,
             @RequestParam(defaultValue = "20") Integer size
     ) {
-        Long memberId = pd.getMember().getId();
-        Map body = query.getNotifications(status, page, size, memberId);
+        Map body = query.getNotifications(status, page, size, pd.getMemberId());
         return BaseResponse.onSuccess(SuccessStatus._OK, body);
     }
 
@@ -50,8 +49,7 @@ public BaseResponse> list(
     @PostMapping("/{notificationId}/read")
     public BaseResponse markRead(@AuthenticationPrincipal PrincipalDetails pd,
                                          @PathVariable Long notificationId) throws AccessDeniedException {
-        Long memberId = pd.getMember().getId();
-        command.markRead(notificationId, memberId);
+        command.markRead(notificationId, pd.getMemberId());
         return BaseResponse.onSuccess(SuccessStatus._OK,
                 "The notification has been marked as read successfully. id=" + notificationId);
     }
@@ -71,8 +69,7 @@ public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest r
     @PutMapping("/{type}/toggle")
     public BaseResponse toggle(@AuthenticationPrincipal PrincipalDetails pd,
                                        @PathVariable NotificationType type) {
-        Long memberId = pd.getMember().getId();
-        boolean newValue = command.toggle(memberId, type);
+        boolean newValue = command.toggle(pd.getMemberId(), type);
         return BaseResponse.onSuccess(SuccessStatus._OK,
                 "Notification setting toggled: now enabled=" + newValue);
     }
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 063e941..e01f8e3 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -16,6 +16,8 @@ public class SecurityConfig {
     public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
         http
                 .csrf(csrf -> csrf.disable())
+                .cors(cors -> {}) // 기본 CORS 구성 사용(필요하면 CorsConfigurationSource 빈 추가)
+                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                 .authorizeHttpRequests(auth -> auth
                         .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
 
@@ -24,7 +26,6 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
                                 "/swagger-resources/**", "/webjars/**"
                         ).permitAll()
-
                         // 로그인/회원가입/재발급만 공개
                         .requestMatchers(
                                 "/auth/login/common",
@@ -33,32 +34,16 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/auth/refresh",
                                 "/auth/phone-numbers/**"
                         ).permitAll()
-						.requestMatchers(
-								"/chat/**",
-								"/suggestion/**",
-								"/review/**",
-								"/ws/**",
-								"/pub/**",
-								"/sub/**",
-								"/auth/**",
-								"/certification/**",
-								"/store/**",
-							"/proposal",
-							"/partnership/**",
-							"/user/**")
-						.permitAll()
-
-                        // 지도 API 공개
-                        .requestMatchers("/map/**").permitAll()
-
                         // 로그아웃은 인증 필요
                         .requestMatchers("/auth/logout").authenticated()
-
                         // 나머지는 인증 필요
                         .anyRequest().authenticated()
                 )
                 .formLogin(form -> form.disable())
-                .httpBasic(basic -> basic.disable());
+                .httpBasic(basic -> basic.disable())
+        		.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
+
+
         return http.build();
     }
 

From dd7c5e5840b6ca5be37896872210e33b97cd3394 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 31 Aug 2025 23:05:24 +1000
Subject: [PATCH 109/270] =?UTF-8?q?[FEAT]=20-=20pd=20=EC=82=AC=EC=9A=A9=20?=
 =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/admin/controller/AdminController.java |  3 +--
 .../controller/CertificationController.java      | 16 +++++-----------
 .../controller/DeviceTokenController.java        |  6 +++---
 .../inquiry/controller/InquiryController.java    |  9 +++------
 .../controller/StudentAdminController.java       | 15 +++++----------
 .../partner/controller/PartnerController.java    |  3 +--
 .../partnership/controller/PaperController.java  |  6 ++----
 .../converter/PartnershipConverter.java          |  6 +++---
 .../review/controller/ReviewController.java      |  9 +++------
 .../domain/store/controller/StoreController.java |  6 ++----
 .../controller/SuggestionController.java         |  6 ++----
 .../user/controller/StudentController.java       |  8 +++-----
 12 files changed, 33 insertions(+), 60 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java
index 817eaf0..0ea470c 100644
--- a/src/main/java/com/assu/server/domain/admin/controller/AdminController.java
+++ b/src/main/java/com/assu/server/domain/admin/controller/AdminController.java
@@ -27,7 +27,6 @@ public class AdminController {
     public BaseResponse randomPartnerRecommend(
             @AuthenticationPrincipal PrincipalDetails pd
             ) {
-        Long adminId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, adminService.suggestRandomPartner(adminId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, adminService.suggestRandomPartner(pd.getId()));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
index 7cc301b..6b2ddef 100644
--- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
+++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
@@ -36,13 +36,11 @@ public class CertificationController {
 	@PostMapping("/certification/session")
 	@Operation(summary = "세션 정보를 요청하는 api", description = "인원 수 기준이 요구되는 제휴일 때 세션을 만들고, 대표자 QR에 담을 정보를 요청하는 api 입니다.")
 	public ResponseEntity> getSessionId(
-		@AuthenticationPrincipal PrincipalDetails userDetails,
+		@AuthenticationPrincipal PrincipalDetails pd,
 		@RequestBody CertificationRequestDTO.groupRequest dto
 	) {
 
-		Member member = userDetails.getMember();
-
-		CertificationResponseDTO.getSessionIdResponse result = certificationService.getSessionId(dto, member);
+		CertificationResponseDTO.getSessionIdResponse result = certificationService.getSessionId(dto, pd.getMember());
 
 		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_SESSION_CREATE, result));
 	}
@@ -53,9 +51,7 @@ public ResponseEntity> certifyGroup(
 		CertificationRequestDTO.groupSessionRequest dto  , PrincipalDetails pd
 
 	) {
-		Member member = pd.getMember();
-
-		certificationService.handleCertification(dto, member);
+		certificationService.handleCertification(dto, pd.getMember());
 
 		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_CERTIFICATION_SUCCESS, null));
 	}
@@ -64,12 +60,10 @@ public ResponseEntity> certifyGroup(
 	@Operation(summary = "개인 인증 api", description = "사실 크게 필요없는데, 제휴 내역 통계를 위해 데이터를 post하는 api 입니다. "
 		+ "가게 별 제휴를 조회하고 people값이 null 인 제휴를 선택한 경우 그룹 인증 대신 요청하는 api 입니다.")
 	public ResponseEntity> personalCertification(
-		@AuthenticationPrincipal PrincipalDetails userDetails,
+		@AuthenticationPrincipal PrincipalDetails pd,
 		@RequestBody CertificationRequestDTO.personalRequest dto
 	) {
-
-		Member member = userDetails.getMember();
-		certificationService.certificatePersonal(dto, member);
+		certificationService.certificatePersonal(dto, pd.getMember());
 
 		return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.PERSONAL_CERTIFICATION_SUCCESS));
 	}
diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index 79475e5..0a2268f 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -25,10 +25,10 @@ public class DeviceTokenController {
     @PostMapping("/register")
     public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
                                          @Valid @RequestBody DeviceTokenRequest req) {
-        service.register(req.getToken(), pd.getMemberId());
+        service.register(req.getToken(), pd.getId());
         return BaseResponse.onSuccess(
                 SuccessStatus._OK,
-                "Device token registered successfully. memberId=" + pd.getMemberId()
+                "Device token registered successfully. memberId=" + pd.getId()
         );
     }
 
@@ -39,7 +39,7 @@ public BaseResponse register(@AuthenticationPrincipal PrincipalDetails p
     @DeleteMapping("/unregister/{tokenId}")
     public BaseResponse unregister(@AuthenticationPrincipal PrincipalDetails pd,
                                            @PathVariable Long tokenId) {
-        service.unregister(tokenId, pd.getMemberId()); // 소유자 검증을 서비스에서 수행하도록 memberId 전달
+        service.unregister(tokenId, pd.getId()); // 소유자 검증을 서비스에서 수행하도록 memberId 전달
         return BaseResponse.onSuccess(
                 SuccessStatus._OK,
                 "Device token unregistered successfully. tokenId=" + tokenId
diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index b2317b7..1505429 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -32,8 +32,7 @@ public BaseResponse create(
             @AuthenticationPrincipal PrincipalDetails pd,
             @RequestBody @Valid InquiryCreateRequestDTO req
     ) {
-        Long memberId = pd.getMember().getId();
-        Long id = inquiryService.create(req, memberId);
+        Long id = inquiryService.create(req, pd.getId());
         return BaseResponse.onSuccess(SuccessStatus._OK, id);
     }
 
@@ -48,8 +47,7 @@ public BaseResponse> list(
             @RequestParam(defaultValue = "1") Integer page,
             @RequestParam(defaultValue = "20") Integer size
     ) {
-        Long memberId = pd.getMember().getId();
-        Map response = inquiryService.getInquiries(status, page, size, memberId);
+        Map response = inquiryService.getInquiries(status, page, size, pd.getId());
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
@@ -63,8 +61,7 @@ public BaseResponse get(
             @AuthenticationPrincipal PrincipalDetails pd,
             @PathVariable("inquiryId") Long inquiryId
     ) {
-        Long memberId = pd.getMember().getId();
-        InquiryResponseDTO response = inquiryService.get(inquiryId, memberId);
+        InquiryResponseDTO response = inquiryService.get(inquiryId, pd.getMemberId());
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
index 5cc4f3c..4286805 100644
--- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
+++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
@@ -25,8 +25,7 @@ public class StudentAdminController {
     public BaseResponse getCountAdmin(
             @AuthenticationPrincipal PrincipalDetails pd
             ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth(memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountAdminAuth(pd.getId()));
     }
     @Operation(
             summary = "신규 한 달 가입자 수 조회 API",
@@ -36,8 +35,7 @@ public BaseResponse getCountA
     public BaseResponse getNewStudentCountAdmin(
             @AuthenticationPrincipal PrincipalDetails pd
     ){
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getNewStudentCountAdmin(memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getNewStudentCountAdmin(pd.getId()));
     }
 
     @Operation(
@@ -48,8 +46,7 @@ public BaseResponse getNewStud
     public BaseResponse getCountUser(
             @AuthenticationPrincipal PrincipalDetails pd
     ){
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsagePerson(memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsagePerson(pd.getId()));
     }
     @Operation(
             summary = "제휴업체 누적별 1위 업체 조회 API",
@@ -59,8 +56,7 @@ public BaseResponse getCoun
         public BaseResponse getTopUsage(
                 @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        Long memberId = pd.getMember().getId();
-            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsage(memberId));
+            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsage(pd.getId()));
         }
 
         /**
@@ -74,8 +70,7 @@ public BaseResponse getTopUsage(
         public BaseResponse getUsageList(
                 @AuthenticationPrincipal PrincipalDetails pd
         ) {
-            Long memberId = pd.getMember().getId();
-            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsageList(memberId));
+            return BaseResponse.onSuccess(SuccessStatus._OK, studentAdminService.getCountUsageList(pd.getId()));
         }
 
 }
diff --git a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java
index 16badf2..cfd7e55 100644
--- a/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java
+++ b/src/main/java/com/assu/server/domain/partner/controller/PartnerController.java
@@ -25,7 +25,6 @@ public class PartnerController {
     public BaseResponse randomAdminRecommend(
             @AuthenticationPrincipal PrincipalDetails pd
             ){
-        Long partnerId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnerService.getRandomAdmin(partnerId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnerService.getRandomAdmin(pd.getId()));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java
index 8ef2954..b3c3523 100644
--- a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java
+++ b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java
@@ -34,11 +34,9 @@ public class PaperController {
 		@Parameter(name = "storeId", description = "QR에서 추출한 storeId를 입력해주세요")
 	})
 	public ResponseEntity> getStorePaperContent(@PathVariable Long storeId,
-		@AuthenticationPrincipal PrincipalDetails userDetails
+		@AuthenticationPrincipal PrincipalDetails pd
 	) {
-		Member member = userDetails.getMember();
-
-		PaperResponseDTO.partnershipContent result = paperQueryService.getStorePaperContent(storeId, member);
+		PaperResponseDTO.partnershipContent result = paperQueryService.getStorePaperContent(storeId, pd.getMember());
 
 		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PAPER_STORE_HISTORY_SUCCESS, result));
 	}
diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index 8aae7a4..fb1ae37 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -175,18 +175,18 @@ else if (content.getCriterionType() == CriterionType.HEADCOUNT &&
 		else if (content.getCriterionType() == CriterionType.PRICE &&
 			content.getOptionType() == OptionType.SERVICE &&
 			isGoodsMultiple) {
-			result = content.getCost() + " 이상 주문 시 " + content.getCategory() + " 제공";
+			result = content.getCost() + "원 이상 주문 시 " + content.getCategory() + " 제공";
 		}
 		// 5. PRICE + SERVICE + 단일 goods
 		else if (content.getCriterionType() == CriterionType.PRICE &&
 			content.getOptionType() == OptionType.SERVICE &&
 			isGoodsSingle) {
-			result = content.getCost() + " 이상 주문 시 " + goodsList.get(0) + " 제공";
+			result = content.getCost() + "원 이상 주문 시 " + goodsList.get(0) + " 제공";
 		}
 		// 6. PRICE + DISCOUNT
 		else if (content.getCriterionType() == CriterionType.PRICE &&
 			content.getOptionType() == OptionType.DISCOUNT) {
-			result = content.getCost() + " 이상 주문 시 " + content.getDiscount() + "% 할인";
+			result = content.getCost() + "원 이상 주문 시 " + content.getDiscount() + "% 할인";
 		}
 
 		return result;
diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
index c71f982..8347d6a 100644
--- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
+++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
@@ -35,8 +35,7 @@ public BaseResponse writeReview(
             @RequestPart("request") ReviewRequestDTO.WriteReviewRequestDTO request,
             @RequestPart(value = "reviewImages", required = false) List reviewImages
     ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(request, memberId, reviewImages));
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.writeReview(request, pd.getId(), reviewImages));
     }
 
     @Operation(
@@ -47,8 +46,7 @@ public BaseResponse writeReview(
     public BaseResponse> checkStudent(
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview(memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview(pd.getId()));
     }
 
     @Operation(
@@ -72,7 +70,6 @@ public BaseResponse deleteReview(
     public BaseResponse> checkPartnerReview(
             @AuthenticationPrincipal PrincipalDetails pd
     ){
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview(memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview(pd.getId()));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/store/controller/StoreController.java b/src/main/java/com/assu/server/domain/store/controller/StoreController.java
index 6bd7d80..51fd866 100644
--- a/src/main/java/com/assu/server/domain/store/controller/StoreController.java
+++ b/src/main/java/com/assu/server/domain/store/controller/StoreController.java
@@ -40,8 +40,7 @@ public ResponseEntity> getTodayBestStor
     @GetMapping("/ranking")
     public ResponseEntity> getWeeklyRank(
             @AuthenticationPrincipal PrincipalDetails pd) {
-        Long memberId = pd.getMember().getId();
-        return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, storeService.getWeeklyRank(memberId)));
+        return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, storeService.getWeeklyRank(pd.getId())));
     }
 
     @Operation(
@@ -52,8 +51,7 @@ public ResponseEntity> getW
     public BaseResponse> getWeeklyRankByPartnerId(
             @AuthenticationPrincipal PrincipalDetails pd
     ){
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getListWeeklyRank(memberId).getItems());
+        return BaseResponse.onSuccess(SuccessStatus._OK, storeService.getListWeeklyRank(pd.getId()).getItems());
     }
 
 
diff --git a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
index 7fd5b20..7b23742 100644
--- a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
+++ b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
@@ -29,8 +29,7 @@ public BaseResponse writeSugge
             @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO,
             @AuthenticationPrincipal PrincipalDetails pd
     ){
-        Long userId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO, userId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO, pd.getId()));
     }
 
     @Operation(
@@ -41,7 +40,6 @@ public BaseResponse writeSugge
     public BaseResponse> getSuggestions(
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        Long adminId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.getSuggestions(adminId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.getSuggestions(pd.getId()));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
index acd2714..ee8630f 100644
--- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java
+++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
@@ -30,10 +30,9 @@ public class StudentController {
 	@GetMapping("/partnership/{year}/{month}")
 	@Operation(summary = "유저의 제휴 내역을 조회", description = "건수 및 금액으로 조회")
 	public ResponseEntity> getMyPartnership(
-		@PathVariable int year, @PathVariable int month, @AuthenticationPrincipal PrincipalDetails userDetails
+		@PathVariable int year, @PathVariable int month, @AuthenticationPrincipal PrincipalDetails pd
 	){
-		Member member = userDetails.getMember();
-		StudentResponseDTO.myPartnership result = studentService.getMyPartnership(member.getId(), year, month);
+		StudentResponseDTO.myPartnership result = studentService.getMyPartnership(pd.getId(), year, month);
 
 		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PARTNERSHIP_HISTORY_SUCCESS, result));
 	}
@@ -47,7 +46,6 @@ public ResponseEntity> getMyPartn
     public BaseResponse getStamp(
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getStamp(memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getStamp(pd.getId()));
     }
 }

From a5cbc58bc7bed1b04fd53eae95dbd74567120221 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 31 Aug 2025 23:50:11 +1000
Subject: [PATCH 110/270] =?UTF-8?q?[MOD/#48]=20-=20student=20Number=20?=
 =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B3=A0=20=EC=9E=88=EB=8D=98=20?=
 =?UTF-8?q?=EA=B1=B0=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/suggestion/converter/SuggestionConverter.java     | 5 +++--
 .../server/domain/suggestion/dto/SuggestionResponseDTO.java  | 2 --
 .../domain/suggestion/service/SuggestionServiceImpl.java     | 4 ++++
 3 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
index 381f4df..0ef257d 100644
--- a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
+++ b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
@@ -1,11 +1,14 @@
 package com.assu.server.domain.suggestion.converter;
 
 import com.assu.server.domain.admin.entity.Admin;
+import com.assu.server.domain.auth.entity.SSUAuth;
+import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO;
 import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO;
 import com.assu.server.domain.suggestion.entity.Suggestion;
 import com.assu.server.domain.user.entity.Student;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 
 import java.util.List;
 import java.util.stream.Collectors;
@@ -16,7 +19,6 @@ public static SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestionRe
         return SuggestionResponseDTO.WriteSuggestionResponseDTO.builder()
                 .suggestionId(suggestion.getId())
                 .memberId(suggestion.getStudent().getId())
-                .studentNumber(suggestion.getStudent().getStudentNumber())
                 .suggestionSubjectId(suggestion.getAdmin().getId())
                 .suggestionStore(suggestion.getStoreName())
                 .suggestionBenefit(suggestion.getContent())
@@ -39,7 +41,6 @@ public static SuggestionResponseDTO.GetSuggestionResponseDTO GetSuggestionResult
                 .suggestionId(s.getId())
                 .createdAt(s.getCreatedAt())
                 .content(s.getContent())
-                .studentNumber(student.getStudentNumber())
                 .enrollmentStatus(student.getEnrollmentStatus())
                 .studentMajor(student.getMajor())
                 .build();
diff --git a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java
index 51de446..5ad52a3 100644
--- a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java
@@ -19,7 +19,6 @@ public class SuggestionResponseDTO {
         public static class WriteSuggestionResponseDTO {
             private Long suggestionId; // 제안 번호
             private Long memberId; // 제안인 아이디
-            private Long studentNumber; // 제안인 학번
             private Long suggestionSubjectId; // 건의 대상 아이디
             private String suggestionStore; // 희망 가게 이름
             private String suggestionBenefit; // 희망 혜택
@@ -34,7 +33,6 @@ public static class GetSuggestionResponseDTO {
             private LocalDateTime createdAt;
             private String content;
             private Major studentMajor;
-            private Long studentNumber;
             private EnrollmentStatus enrollmentStatus;
         }
     }
diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
index 1508dc5..c12864d 100644
--- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
@@ -2,6 +2,10 @@
 
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
+import com.assu.server.domain.auth.entity.SSUAuth;
+import com.assu.server.domain.auth.repository.SSUAuthRepository;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.partnership.converter.PartnershipConverter;
 import com.assu.server.domain.partnership.dto.PartnershipResponseDTO;
 import com.assu.server.domain.partnership.entity.Goods;

From 974116703db929ea89f9b03f3bcff8bf4a7cd840 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Mon, 1 Sep 2025 01:05:42 +0900
Subject: [PATCH 111/270] =?UTF-8?q?[HOTFIX]=20application-test.yml?=
 =?UTF-8?q?=EC=97=90=20dummy=EA=B0=92=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/test/resources/application-secret.yml | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 src/test/resources/application-secret.yml

diff --git a/src/test/resources/application-secret.yml b/src/test/resources/application-secret.yml
deleted file mode 100644
index e69de29..0000000

From 2a844181d564e506d35a62b97283046d75d82e26 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Tue, 2 Sep 2025 00:58:32 +0900
Subject: [PATCH 112/270] =?UTF-8?q?[FIX/#52]=20=EB=A6=AC=EB=B7=B0=20?=
 =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=A1=B0=ED=9A=8C=EB=A1=9C=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../review/controller/ReviewController.java   | 15 ++++++-----
 .../review/converter/ReviewConverter.java     | 27 +++++++++++++------
 .../domain/review/entity/ReviewPhoto.java     |  2 ++
 .../review/repository/ReviewRepository.java   |  7 +++--
 .../domain/review/service/ReviewService.java  |  7 +++--
 .../review/service/ReviewServiceImpl.java     | 11 +++++---
 6 files changed, 47 insertions(+), 22 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
index 8347d6a..a2be857 100644
--- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
+++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
@@ -12,6 +12,9 @@
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import lombok.RequiredArgsConstructor;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.http.MediaType;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
@@ -43,10 +46,10 @@ public BaseResponse writeReview(
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/student")
-    public BaseResponse> checkStudent(
-            @AuthenticationPrincipal PrincipalDetails pd
+    public BaseResponse> checkStudent(
+            @AuthenticationPrincipal PrincipalDetails pd, Pageable pageable
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview(pd.getId()));
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview(pd.getId(), pageable));
     }
 
     @Operation(
@@ -67,9 +70,9 @@ public BaseResponse deleteReview(
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/partner")
-    public BaseResponse> checkPartnerReview(
-            @AuthenticationPrincipal PrincipalDetails pd
+    public BaseResponse> checkPartnerReview(
+            @AuthenticationPrincipal PrincipalDetails pd, Pageable pageable
     ){
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview(pd.getId()));
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview(pd.getId(), pageable));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
index 99b8dfb..eb0d0d8 100644
--- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
+++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
@@ -11,6 +11,8 @@
 import java.util.List;
 import java.util.stream.Collectors;
 
+import org.springframework.data.domain.Page;
+
 public class ReviewConverter {
     public static ReviewResponseDTO.WriteReviewResponseDTO writeReviewResultDTO(Review review){
         //enti -> dto
@@ -49,11 +51,16 @@ public static ReviewResponseDTO.CheckStudentReviewResponseDTO checkStudentReview
                         .collect(Collectors.toList()))
                 .build();
     }
-    public static List checkStudentReviewResultDTO(List reviews){
-        return reviews.stream()
-                .map(ReviewConverter::checkStudentReviewResultDTO)
-                .collect(Collectors.toList());
+    // public static List checkStudentReviewResultDTO(List reviews){
+    //     return reviews.stream()
+    //             .map(ReviewConverter::checkStudentReviewResultDTO)
+    //             .collect(Collectors.toList());
+    // }
+
+    public static Page checkStudentReviewResultDTO(Page reviews){
+        return reviews.map(ReviewConverter::checkStudentReviewResultDTO);
     }
+
     public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReviewResultDTO(Review review){
         return ReviewResponseDTO.CheckPartnerReviewResponseDTO.builder()
                 .reviewId(review.getId())
@@ -68,10 +75,14 @@ public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReview
                 .build();
 
     }
-    public static List checkPartnerReviewResultDTO(List reviews){
-        return reviews.stream()
-                .map(ReviewConverter::checkPartnerReviewResultDTO)
-                .collect(Collectors.toList());
+    // public static List checkPartnerReviewResultDTO(List reviews){
+    //     return reviews.stream()
+    //             .map(ReviewConverter::checkPartnerReviewResultDTO)
+    //             .collect(Collectors.toList());
+    // }
+
+    public static Page checkPartnerReviewResultDTO(Page reviews){
+        return reviews.map(ReviewConverter::checkPartnerReviewResultDTO);
     }
     public static ReviewResponseDTO.DeleteReviewResponseDTO deleteReviewResultDTO(Long reviewId){
         return ReviewResponseDTO.DeleteReviewResponseDTO.builder()
diff --git a/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java b/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java
index ab49c15..892e70d 100644
--- a/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java
+++ b/src/main/java/com/assu/server/domain/review/entity/ReviewPhoto.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.review.entity;
 import com.assu.server.domain.common.entity.BaseEntity;
 
+import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.EnumType;
 import jakarta.persistence.Enumerated;
@@ -30,6 +31,7 @@ public class ReviewPhoto extends BaseEntity {
 	@JoinColumn(name = "review_id")
 	private Review review;
 
+	@Column(length =2000)
 	private String photoUrl;
 
 	@JoinColumn(name = "key_name") // S3 키 이름 저장 (조회 시 새 URL 생성용)
diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
index bfc1ecd..7b505a6 100644
--- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
+++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
@@ -1,6 +1,9 @@
 package com.assu.server.domain.review.repository;
 
 import com.assu.server.domain.review.entity.Review;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
@@ -14,8 +17,8 @@ public interface ReviewRepository extends JpaRepository {
     WHERE r.student.id = :memberId
     ORDER BY r.createdAt DESC
 """)
-    List findByMemberId(@Param("memberId") Long memberId);
+    Page findByMemberId(@Param("memberId") Long memberId, Pageable pageable);
     List findByStoreId(Long storeId);
 
-    List findByStoreIdOrderByCreatedAtDesc(Long id);//최신순 정렬
+    Page findByStoreIdOrderByCreatedAtDesc(Long id, Pageable pageable);//최신순 정렬
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
index 16c808d..72ef6c3 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
@@ -2,6 +2,9 @@
 
 import com.assu.server.domain.review.dto.ReviewRequestDTO;
 import com.assu.server.domain.review.dto.ReviewResponseDTO;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.multipart.MultipartFile;
@@ -10,7 +13,7 @@
 
 public interface ReviewService {
     ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages);
-    List checkStudentReview(Long memberId);
-    List checkPartnerReview(Long memberId);
+    Page checkStudentReview(Long memberId, Pageable pageable);
+    Page checkPartnerReview(Long memberId, Pageable pageable);
     ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(@PathVariable Long reviewId);
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 0fab833..14074ca 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -19,6 +19,9 @@
 import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
@@ -93,8 +96,8 @@ private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteR
     }
 
     @Override
-    public List checkStudentReview(Long memberId) {
-        List reviews = reviewRepository.findByMemberId(memberId);
+    public Page checkStudentReview(Long memberId, Pageable pageable) {
+        Page reviews = reviewRepository.findByMemberId(memberId, pageable);
 
         for (Review review : reviews) {
             updateReviewImageUrls(review);
@@ -105,13 +108,13 @@ public List checkStudentReview(
 
     @Override
     @Transactional
-    public List checkPartnerReview(Long memberId) {
+    public Page checkPartnerReview(Long memberId, Pageable pageable) {
         Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
         Store store = storeRepository.findByPartner(partner)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
 
-        List reviews = reviewRepository.findByStoreIdOrderByCreatedAtDesc(store.getId());
+        Page reviews = reviewRepository.findByStoreIdOrderByCreatedAtDesc(store.getId(), pageable);
 
         for (Review review : reviews) {
             updateReviewImageUrls(review);

From 931c31d9d4882b1e8a92b2d48208c7074556ad0f Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Tue, 2 Sep 2025 01:11:02 +0900
Subject: [PATCH 113/270] =?UTF-8?q?[FIX/#52]=20=EC=A0=9C=ED=9C=B4=EB=82=B4?=
 =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20id=EA=B9=8C=EC=A7=80?=
 =?UTF-8?q?=20=EA=B0=99=EC=9D=B4=20=EB=82=B4=EB=A0=A4=EC=A3=BC=EA=B8=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/PaperContentRepository.java    |  2 +
 .../domain/user/dto/StudentResponseDTO.java   |  3 ++
 .../user/service/StudentServiceImpl.java      | 42 +++++++++++++++----
 3 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
index 05b5d60..9c478c3 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
@@ -29,4 +29,6 @@ public interface PaperContentRepository extends JpaRepository findAllByOnePaperIdInFetchGoods(@Param("paperIds") Long paperIds);
+
+    Optional findById(Long id);
 }
diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
index 1b5cccb..ff18d75 100644
--- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
@@ -32,7 +32,10 @@ public static class myPartnership {
 	@AllArgsConstructor
 	@Builder
 	public static class UsageDetailDTO {
+		private Long partnershipUsageId;
 		private String storeName;
+		private Long partnerId;
+		private Long storeId;
 		private LocalDate usedAt;
 		private String benefitDescription;
 		private boolean isReviewed;
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
index e34f35a..e8e3945 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
@@ -2,7 +2,9 @@
 
 import java.util.List;
 
+import com.assu.server.domain.partnership.entity.PaperContent;
 import com.assu.server.domain.partnership.repository.PaperContentRepository;
+import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.user.converter.StudentConverter;
 import com.assu.server.domain.user.dto.StudentResponseDTO;
 import com.assu.server.domain.user.entity.PartnershipUsage;
@@ -36,16 +38,42 @@ public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) {
 	public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) {
 		List usages = partnershipUsageRepository.findByYearAndMonth(studentId, year, month);
 
+		// return StudentResponseDTO.myPartnership.builder()
+		// 	.serviceCount(usages.size())
+		// 	.details(usages.stream()
+		// 		.map(u ->
+		// 			StudentResponseDTO.UsageDetailDTO.builder()
+		// 			.partnershipUsageId(u.getId())
+		// 			.storeName(u.getPlace())
+		// 			.usedAt(u.getDate())
+		// 			.benefitDescription(u.getPartnershipContent())
+		// 			.isReviewed(u.getIsReviewed())
+		// 			.build()
+		// 		).toList()
+		// 	)
+		// 	.build();
 		return StudentResponseDTO.myPartnership.builder()
 			.serviceCount(usages.size())
 			.details(usages.stream()
-				.map(u -> StudentResponseDTO.UsageDetailDTO.builder()
-					.storeName(u.getPlace())
-					.usedAt(u.getDate())
-					.benefitDescription(u.getPartnershipContent())
-					.isReviewed(u.getIsReviewed())
-					.build()
-				).toList()
+				.map(u -> {
+					// 1. partnershipUsage의 paperContentId로 paperContent를 조회합니다.
+					// findById는 Optional을 반환하므로, orElse(null)로 처리합니다.
+					PaperContent paperContent = paperContentRepository.findById(u.getContentId())
+						.orElse(null);
+
+					// 2. PaperContent에서 storeId를 가져옵니다.
+					Store store = (paperContent != null) ? paperContent.getPaper().getStore() : null;
+
+					return StudentResponseDTO.UsageDetailDTO.builder()
+						.partnershipUsageId(u.getId())
+						.storeName(u.getPlace())
+						.usedAt(u.getDate())
+						.benefitDescription(u.getPartnershipContent())
+						.isReviewed(u.getIsReviewed())
+						.storeId(store.getId()) // 3. storeId를 DTO에 매핑합니다.
+						.partnerId(store.getPartner().getId())
+						.build();
+				}).toList()
 			)
 			.build();
 	}

From 7662c29797cf34e2c40be118066d2c3113256a6c Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 2 Sep 2025 18:12:26 +0900
Subject: [PATCH 114/270] =?UTF-8?q?refactor/#51:=20application-test.yml?=
 =?UTF-8?q?=EC=97=90=20dummy=EA=B0=92=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20g?=
 =?UTF-8?q?itignore=20=EC=A0=9C=EC=99=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitignore                              |  2 --
 src/test/resources/application-test.yml | 39 +++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 2 deletions(-)
 create mode 100644 src/test/resources/application-test.yml

diff --git a/.gitignore b/.gitignore
index a0e3821..4c53f8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,8 +44,6 @@ out/
 
 ### Secret ###
 src/main/resources/application-secret.yml
-src/test/resources/application-test.yml
-src/test/resources/application-secret.yml
 
 ### Firebase ###
 src/main/resources/firebase/
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
new file mode 100644
index 0000000..1bb046d
--- /dev/null
+++ b/src/test/resources/application-test.yml
@@ -0,0 +1,39 @@
+spring:
+  datasource:
+    url: jdbc:h2:mem:testdb
+    driver-class-name: org.h2.Driver
+    username: sa
+    password:
+
+jwt:
+  header: Authorization
+  prefix: Bearer
+  secret: dummy-secret
+  access-valid-seconds: 3600
+  refresh-valid-seconds: 1209600
+
+assu:
+  security:
+    school-crypto:
+      base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="
+
+cloud:
+  aws:
+    s3:
+      bucket: test-bucket
+    region:
+      static: ap-northeast-2
+    stack:
+      auto: false
+    credentials:
+      accessKey: dummy-access
+      secretKey: dummy-secret
+
+firebase:
+  project-id: dummy-firebase
+  credentials:
+    path: classpath:firebase/service-account.json
+
+kakao:
+  base-url: https://dapi.kakao.com
+  rest-api-key: dummy-kakao-key
\ No newline at end of file

From 139dfebceeec8e0b7462c9a19e6c55feff8b9015 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 2 Sep 2025 18:18:36 +0900
Subject: [PATCH 115/270] =?UTF-8?q?refactor/#51:=20firebase=20service-acco?=
 =?UTF-8?q?unt.json=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/cd.yml                         | 15 +++++++++++++++
 docker-compose.yml                               |  6 ++++--
 src/test/resources/firebase/service-account.json | 13 +++++++++++++
 3 files changed, 32 insertions(+), 2 deletions(-)
 create mode 100644 src/test/resources/firebase/service-account.json

diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index b81ca45..d948a46 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -17,6 +17,12 @@ jobs:
           echo "${{ secrets.APPLICATION_SECRET }}" > ./temp_secret/application-secret.yml
         shell: bash
 
+      - name: Create
+        run: |
+          mkdir -p ./temp_secret
+          echo "${{ secrets.SERVICE_ACCOUNT }}" > ./temp_secret/service-account.json
+        shell: bash
+
       - name: Copy application-secret.yml to EC2
         uses: appleboy/scp-action@v0.1.3
         with:
@@ -26,6 +32,15 @@ jobs:
           source: ./temp_secret/application-secret.yml
           target: /home/ubuntu/secret/
 
+      - name: Copy service-account.json to EC2
+        uses: appleboy/scp-action@v0.1.3
+        with:
+          username: ubuntu
+          host: ${{ secrets.EC2_HOST }}
+          key: ${{ secrets.EC2_SSH_KEY }}
+          source: ./temp_secret/service-account.json
+          target: /home/ubuntu/secret/
+
       - name: Copy docker-compose.yml
         uses: appleboy/scp-action@v0.1.3
         with:
diff --git a/docker-compose.yml b/docker-compose.yml
index 1ece20c..59c3991 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,7 +11,8 @@ services:
       - SPRING_PROFILES_ACTIVE=blue
       - SPRING_CONFIG_ADDITIONAL_LOCATION=file:/app/config/
     volumes:
-      - /home/ubuntu/app/config/application-secret.yml:/app/config/application-secret.yml:ro
+      - /home/ubuntu/secret/application-secret.yml:/app/config/application-secret.yml:ro
+      - /home/ubuntu/secret/service-account.json:/app/config/service-account.json:ro
     networks:
       - assu-network
 
@@ -27,7 +28,8 @@ services:
       - SPRING_PROFILES_ACTIVE=green
       - SPRING_CONFIG_ADDITIONAL_LOCATION=file:/app/config/
     volumes:
-      - /home/ubuntu/app/config/application-secret.yml:/app/config/application-secret.yml:ro
+      - /home/ubuntu/secret/application-secret.yml:/app/config/application-secret.yml:ro
+      - /home/ubuntu/secret/service-account.json:/app/config/service-account.json:ro
     networks:
       - assu-network
 
diff --git a/src/test/resources/firebase/service-account.json b/src/test/resources/firebase/service-account.json
new file mode 100644
index 0000000..c16d5f8
--- /dev/null
+++ b/src/test/resources/firebase/service-account.json
@@ -0,0 +1,13 @@
+{
+  "type": "service_account",
+  "project_id": "dummy-project-id",
+  "private_key_id": "dummy-private-key-id",
+  "private_key": "-----BEGIN PRIVATE KEY-----\nDUMMY_PRIVATE_KEY_CONTENT\n-----END PRIVATE KEY-----\n",
+  "client_email": "dummy-service-account@dummy-project.iam.gserviceaccount.com",
+  "client_id": "123456789012345678901",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://oauth2.googleapis.com/token",
+  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dummy-service-account%40dummy-project.iam.gserviceaccount.com",
+  "universe_domain": "googleapis.com"
+}

From 95ea5c4af088e14a56c8d83187c96b58b8e1d90b Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 2 Sep 2025 18:21:35 +0900
Subject: [PATCH 116/270] =?UTF-8?q?refactor/#51:=20dev=EC=97=90=20pr?=
 =?UTF-8?q?=EC=8B=9C=20ci=20=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3c6e1a0..214323f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,6 +3,8 @@ name: CI Pipeline
 on:
   push:
     branches: [ develop ]
+  pull_request:
+    branches: [ dev ]
 
 jobs:
   ci:

From c4f8ca3296513b64c84de018c80030461eeb119c Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 2 Sep 2025 18:22:03 +0900
Subject: [PATCH 117/270] =?UTF-8?q?refactor/#51:=20dev=EC=97=90=20pr?=
 =?UTF-8?q?=EC=8B=9C=20ci=20=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 214323f..d793980 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches: [ develop ]
   pull_request:
-    branches: [ dev ]
+    branches: [ develop ]
 
 jobs:
   ci:

From 41a4ce18eeedf797bdfc3a695142408096fa23f3 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Tue, 2 Sep 2025 22:16:49 +1000
Subject: [PATCH 118/270] =?UTF-8?q?[REFACTOR/#48]=20-=20device=20Controlle?=
 =?UTF-8?q?r=20=EB=A6=AC=ED=8C=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/DeviceTokenController.java     | 30 +++++++++----------
 .../deviceToken/dto/DeviceTokenRequest.java   |  8 -----
 .../service/DeviceTokenService.java           |  2 +-
 .../service/DeviceTokenServiceImpl.java       |  4 ++-
 .../inquiry/controller/InquiryController.java |  6 ++--
 .../controller/NotificationController.java    | 10 ++++---
 6 files changed, 28 insertions(+), 32 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java

diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index 0a2268f..d8ae7f0 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -1,18 +1,18 @@
 package com.assu.server.domain.deviceToken.controller;
 
-import com.assu.server.domain.deviceToken.dto.DeviceTokenRequest;
 import com.assu.server.domain.deviceToken.service.DeviceTokenService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
-import jakarta.validation.Valid;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
+@Tag(name = "Device Token", description = "디바이스 토큰 등록/해제 API")
 @RestController
-@RequestMapping("deviceTokens")
+@RequestMapping("/device-tokens")
 @RequiredArgsConstructor
 public class DeviceTokenController {
 
@@ -20,26 +20,24 @@ public class DeviceTokenController {
 
     @Operation(
             summary = "Device Token 등록 API",
-            description = "로그인 사용자 기준으로 FCM Device Token을 등록합니다."
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed8092864ac5a1ddc88d07?source=copy_link) device Token을 등록하고 등록된 Token의 ID를 반환합니다.\n"+
+                    "- token: Request Param, String\n"
     )
     @PostMapping("/register")
-    public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
-                                         @Valid @RequestBody DeviceTokenRequest req) {
-        service.register(req.getToken(), pd.getId());
-        return BaseResponse.onSuccess(
-                SuccessStatus._OK,
-                "Device token registered successfully. memberId=" + pd.getId()
-        );
+    public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
+                                       @RequestParam String token) {
+        Long tokenId = service.register(token, pd.getId());
+        return BaseResponse.onSuccess(SuccessStatus._OK, tokenId);
     }
-
     @Operation(
             summary = "Device Token 등록 해제 API",
-            description = "로그아웃/탈퇴 시 호출합니다. 자신의 토큰만 해제됩니다."
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed80b8b26be9e01d24c929?source=copy_link) 로그아웃/탈퇴 시 호출해 device Token 등록을 해제합니다. 자신의 토큰만 해제가 가능합니다.\n"+
+                    "- token-id: Path Variavle, Long\n"
     )
-    @DeleteMapping("/unregister/{tokenId}")
+    @DeleteMapping("/unregister/{token-id}")
     public BaseResponse unregister(@AuthenticationPrincipal PrincipalDetails pd,
-                                           @PathVariable Long tokenId) {
-        service.unregister(tokenId, pd.getId()); // 소유자 검증을 서비스에서 수행하도록 memberId 전달
+                                           @PathVariable("token-id") Long tokenId) {
+        service.unregister(tokenId, pd.getId());
         return BaseResponse.onSuccess(
                 SuccessStatus._OK,
                 "Device token unregistered successfully. tokenId=" + tokenId
diff --git a/src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java b/src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java
deleted file mode 100644
index 77df7f4..0000000
--- a/src/main/java/com/assu/server/domain/deviceToken/dto/DeviceTokenRequest.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.assu.server.domain.deviceToken.dto;
-
-import lombok.Data;
-
-@Data
-public class DeviceTokenRequest {
-    private String token;
-}
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
index 9808ab6..db41525 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenService.java
@@ -1,6 +1,6 @@
 package com.assu.server.domain.deviceToken.service;
 
 public interface DeviceTokenService {
-    void register(String tokenId, Long memberId);
+    Long register(String tokenId, Long memberId);
     void unregister(Long tokenId, Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
index 20044bd..9d4cabb 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java
@@ -20,7 +20,7 @@ public class DeviceTokenServiceImpl implements DeviceTokenService {
 
     @Transactional
     @Override
-    public void register(String tokenId, Long memberId) {
+    public Long register(String tokenId, Long memberId) {
         Member member = memberRepository.findMemberById(memberId).orElseThrow(
             () -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)
         );
@@ -32,6 +32,8 @@ public void register(String tokenId, Long memberId) {
                 .map(deviceToken -> { deviceToken.setActive(true); return deviceToken; })
                 .orElse(DeviceToken.builder().member(member).token(tokenId).active(true).build());
         deviceTokenRepository.save(dt);
+
+        return dt.getId();
     }
 
     @Transactional
diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index 1505429..b08b6ea 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -9,6 +9,7 @@
 
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -16,6 +17,7 @@
 
 import java.util.Map;
 
+@Tag(name = "Inquiry", description = "문의 API")
 @RestController
 @RequestMapping("/member/inquiries")
 @RequiredArgsConstructor
@@ -70,9 +72,9 @@ public BaseResponse get(
             summary = "운영자 답변 API",
             description = "문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다."
     )
-    @PatchMapping("/{inquiryId}/answer")
+    @PatchMapping("/{inquiry-id}/answer")
     public BaseResponse answer(
-            @PathVariable Long inquiryId,
+            @PathVariable("inquiry-id") Long inquiryId,
             @RequestBody @Valid InquiryAnswerRequestDTO req
     ) {
         inquiryService.answer(inquiryId, req.getAnswer());
diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 7e2bf0b..0865719 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -8,6 +8,7 @@
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
@@ -20,8 +21,9 @@
 import java.util.HashMap;
 import java.util.Map;
 
+@Tag(name = "Notification", description = "알림 API")
 @RestController
-@RequestMapping("notifications")
+@RequestMapping("/notifications")
 @RequiredArgsConstructor
 public class NotificationController {
     private final NotificationQueryService query;
@@ -46,9 +48,9 @@ public BaseResponse> list(
             summary = "알림 읽음 처리 API",
             description = "알림 아이디를 보내주세요"
     )
-    @PostMapping("/{notificationId}/read")
+    @PostMapping("/{notification-id}/read")
     public BaseResponse markRead(@AuthenticationPrincipal PrincipalDetails pd,
-                                         @PathVariable Long notificationId) throws AccessDeniedException {
+                                         @PathVariable("notification-id") Long notificationId) throws AccessDeniedException {
         command.markRead(notificationId, pd.getMemberId());
         return BaseResponse.onSuccess(SuccessStatus._OK,
                 "The notification has been marked as read successfully. id=" + notificationId);
@@ -66,7 +68,7 @@ public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest r
     }
 
     @Operation(summary = "알림 유형별 ON/OFF 토글 API")
-    @PutMapping("/{type}/toggle")
+    @PutMapping("/{type}")
     public BaseResponse toggle(@AuthenticationPrincipal PrincipalDetails pd,
                                        @PathVariable NotificationType type) {
         boolean newValue = command.toggle(pd.getMemberId(), type);

From d8de26a0803cff2b91cebc3152669553f325092b Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Tue, 2 Sep 2025 23:13:18 +1000
Subject: [PATCH 119/270] =?UTF-8?q?[REFACTOR/#48]=20-=20notification=20Con?=
 =?UTF-8?q?troller=20=EB=A6=AC=ED=8C=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/NotificationController.java    | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 0865719..884b79d 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -31,7 +31,10 @@ public class NotificationController {
 
     @Operation(
             summary = "알림 목록 조회 API",
-            description = "page는 1 이상이어야 합니다."
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/2491197c19ed8091b349ef0ef4bb0f60?source=copy_link) 본인의 알림 목록을 상태별로 조회합니다.\n"+
+                    "- status: Request Param, String, all/unread\n" +
+                    "- page: Request Param, Integer, 1 이상\n" +
+                    "- size: Request Param, Integer, default = 20"
     )
     @GetMapping
     public BaseResponse> list(
@@ -46,7 +49,8 @@ public BaseResponse> list(
 
     @Operation(
             summary = "알림 읽음 처리 API",
-            description = "알림 아이디를 보내주세요"
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/2491197c19ed80a89ff0c03bc150460f?source=copy_link) 알림 아이디에 해당하는 알림을 읽음 처리합니다.\n"+
+                    "- notification-id: Path Variable, Long\n"
     )
     @PostMapping("/{notification-id}/read")
     public BaseResponse markRead(@AuthenticationPrincipal PrincipalDetails pd,
@@ -58,8 +62,7 @@ public BaseResponse markRead(@AuthenticationPrincipal PrincipalDetails p
 
     @Operation(
             summary = "알림 전송 테스트 API",
-            description = "API 명세서의 [notification > 알림 보내기 테스트] 페이지의 예시 request를 참고해서 테스트 해주세요!"+
-                    "deviceToken을 등록하신 이후에 확인 가능합니다."
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/2511197c19ed8051bc93d95f0b216543?source=copy_link) deviceToken을 등록한 이후에 사용 가능합니다."
     )
     @PostMapping("/queue")
     public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest req) {
@@ -67,12 +70,14 @@ public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest r
         return BaseResponse.onSuccess(SuccessStatus._OK, "Notification delivery succeeded.");
     }
 
-    @Operation(summary = "알림 유형별 ON/OFF 토글 API")
+    @Operation(summary = "알림 유형별 ON/OFF 토글 API",
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/on-off-2511197c19ed80aeb4eed3c502691361?source=copy_link) 토글 형식으로 유형별 알림을 ON/OFF 합니다.\n"+
+                    "- type: Path Variable, NotificationType [CHAT / PARTNER_SUGGESTION / PARTNER_PROPOSAL / ORDER]\n")
     @PutMapping("/{type}")
     public BaseResponse toggle(@AuthenticationPrincipal PrincipalDetails pd,
-                                       @PathVariable NotificationType type) {
+                                       @PathVariable("type") NotificationType type) {
         boolean newValue = command.toggle(pd.getMemberId(), type);
         return BaseResponse.onSuccess(SuccessStatus._OK,
                 "Notification setting toggled: now enabled=" + newValue);
     }
-}
+}
\ No newline at end of file

From 360b791877dbad9811bc5e85b271e2631dd1f73a Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Tue, 2 Sep 2025 23:46:26 +1000
Subject: [PATCH 120/270] =?UTF-8?q?[REFACTOR/#48]=20-=20inquiry=20Controll?=
 =?UTF-8?q?er=20=EB=A6=AC=ED=8C=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../inquiry/controller/InquiryController.java | 26 ++++++++++++-------
 .../controller/NotificationController.java    |  2 +-
 2 files changed, 17 insertions(+), 11 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index b08b6ea..399a08f 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -26,8 +26,9 @@ public class InquiryController {
     private final InquiryService inquiryService;
 
     @Operation(
-            summary = "문의를 생성하는 API",
-            description = "생성 성공시 생성된 문의의 ID를 반환합니다."
+            summary = "문의 생성 API",
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/2441197c19ed800688f0cfb304dead63?source=copy_link) 문의를 생성하고 해당 문의의 id를 반환합니다.\n"+
+                    "- InquiryCreateRequestDTO: title, content, email\n"
     )
     @PostMapping
     public BaseResponse create(
@@ -40,12 +41,15 @@ public BaseResponse create(
 
     @Operation(
             summary = "문의 목록을 조회하는 API",
-            description = "page는 1 이상이어야 합니다."
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/2441197c19ed803eba4af9598484e5c5?source=copy_link) 본인의 문의 목록을 상태별로 조회합니다.\n"+
+                    "- status: Request Param, String, [all/waiting/answered]\n" +
+                    "- page: Request Param, Integer, 1 이상\n" +
+                    "- size: Request Param, Integer, default = 20"
     )
     @GetMapping
     public BaseResponse> list(
             @AuthenticationPrincipal PrincipalDetails pd,
-            @RequestParam(defaultValue = "all") String status,
+            @RequestParam(defaultValue = "all") String status, // all | waiting | answered
             @RequestParam(defaultValue = "1") Integer page,
             @RequestParam(defaultValue = "20") Integer size
     ) {
@@ -56,12 +60,13 @@ public BaseResponse> list(
     /** 단건 상세 조회 */
     @Operation(
             summary = "문의 단건 상세 조회 API",
-            description = "문의 ID를 보내주세요."
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed800f8a1fffc5a101f3c0?source=copy_link) 본인의 단건 문의를 상세 조회합니다.\n"+
+                    "- inquiry-id: Path Variable, Long\n"
     )
-    @GetMapping("/{inquiryId}")
+    @GetMapping("/{inquiry-id}")
     public BaseResponse get(
             @AuthenticationPrincipal PrincipalDetails pd,
-            @PathVariable("inquiryId") Long inquiryId
+            @PathVariable("inquiry-id") Long inquiryId
     ) {
         InquiryResponseDTO response = inquiryService.get(inquiryId, pd.getMemberId());
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
@@ -70,14 +75,15 @@ public BaseResponse get(
     /** 문의 답변 (운영자) */
     @Operation(
             summary = "운영자 답변 API",
-            description = "문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다."
+            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed8064808fcca568b8912a?source=copy_link) 문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다.\n"+
+                    "- inquiry-id: Path Variable, Long\n"
     )
     @PatchMapping("/{inquiry-id}/answer")
-    public BaseResponse answer(
+    public BaseResponse answer(
             @PathVariable("inquiry-id") Long inquiryId,
             @RequestBody @Valid InquiryAnswerRequestDTO req
     ) {
         inquiryService.answer(inquiryId, req.getAnswer());
-        return BaseResponse.onSuccess(SuccessStatus._OK, null);
+        return BaseResponse.onSuccess(SuccessStatus._OK, "The inquiry answered successfully. id=" + inquiryId);
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 884b79d..6d79447 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -32,7 +32,7 @@ public class NotificationController {
     @Operation(
             summary = "알림 목록 조회 API",
             description = "[v1.0 (2025-09-02)](https://www.notion.so/2491197c19ed8091b349ef0ef4bb0f60?source=copy_link) 본인의 알림 목록을 상태별로 조회합니다.\n"+
-                    "- status: Request Param, String, all/unread\n" +
+                    "- status: Request Param, String, [all/unread]\n" +
                     "- page: Request Param, Integer, 1 이상\n" +
                     "- size: Request Param, Integer, default = 20"
     )

From 71f63f1ceb05c6bcea962f1cfdd53d889e404359 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 3 Sep 2025 00:12:02 +1000
Subject: [PATCH 121/270] =?UTF-8?q?[REFACTOR/#48]=20-=20DeviceToken=20Cont?=
 =?UTF-8?q?roller=20URI=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/deviceToken/controller/DeviceTokenController.java  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index d8ae7f0..e7c9644 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -23,7 +23,7 @@ public class DeviceTokenController {
             description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed8092864ac5a1ddc88d07?source=copy_link) device Token을 등록하고 등록된 Token의 ID를 반환합니다.\n"+
                     "- token: Request Param, String\n"
     )
-    @PostMapping("/register")
+    @PostMapping
     public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
                                        @RequestParam String token) {
         Long tokenId = service.register(token, pd.getId());
@@ -34,7 +34,7 @@ public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
             description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed80b8b26be9e01d24c929?source=copy_link) 로그아웃/탈퇴 시 호출해 device Token 등록을 해제합니다. 자신의 토큰만 해제가 가능합니다.\n"+
                     "- token-id: Path Variavle, Long\n"
     )
-    @DeleteMapping("/unregister/{token-id}")
+    @DeleteMapping("/{token-id}")
     public BaseResponse unregister(@AuthenticationPrincipal PrincipalDetails pd,
                                            @PathVariable("token-id") Long tokenId) {
         service.unregister(tokenId, pd.getId());

From 6b4e04059b0c6514d234e759455e4edd4e73f1b6 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 01:37:36 +0900
Subject: [PATCH 122/270] =?UTF-8?q?refactor/#51:=20firebase=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9A=A9=20Mock=20bean=20=EC=A3=BC=EC=9E=85?=
 =?UTF-8?q?=20=EB=B0=8F=20firebase=20configuaration=EC=9D=80=20test?=
 =?UTF-8?q?=EC=9D=B4=EC=99=B8=EC=9D=98=20=ED=99=98=EA=B2=BD=EC=97=90?=
 =?UTF-8?q?=EC=84=9C=20=EC=A3=BC=EC=9E=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/global/config/FirebaseConfig.java    |  2 ++
 .../com/assu/server/ServerApplicationTests.java | 17 +++++++++++++++++
 src/test/resources/application-test.yml         |  8 +++-----
 .../resources/firebase/service-account.json     | 13 -------------
 4 files changed, 22 insertions(+), 18 deletions(-)
 delete mode 100644 src/test/resources/firebase/service-account.json

diff --git a/src/main/java/com/assu/server/global/config/FirebaseConfig.java b/src/main/java/com/assu/server/global/config/FirebaseConfig.java
index fc2e0a7..2279867 100644
--- a/src/main/java/com/assu/server/global/config/FirebaseConfig.java
+++ b/src/main/java/com/assu/server/global/config/FirebaseConfig.java
@@ -7,11 +7,13 @@
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
 import org.springframework.core.io.Resource;
 
 import java.io.InputStream;
 
 @Configuration
+@Profile("!test")
 public class FirebaseConfig {
 
     @Value("${firebase.project-id}")
diff --git a/src/test/java/com/assu/server/ServerApplicationTests.java b/src/test/java/com/assu/server/ServerApplicationTests.java
index b685821..0324e67 100644
--- a/src/test/java/com/assu/server/ServerApplicationTests.java
+++ b/src/test/java/com/assu/server/ServerApplicationTests.java
@@ -1,13 +1,30 @@
 package com.assu.server;
 
+import com.google.firebase.messaging.FirebaseMessaging;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
 import org.springframework.test.context.ActiveProfiles;
 
 @SpringBootTest
 @ActiveProfiles("test")
 class ServerApplicationTests {
 
+	@Mock
+	private FirebaseMessaging firebaseMessaging;
+
+	@TestConfiguration
+	static class MockConfig {
+		@Bean
+		FirebaseMessaging firebaseMessaging() {
+			return Mockito.mock(FirebaseMessaging.class);
+		}
+	}
+
+
 	@Test
 	void contextLoads() {
 	}
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 1bb046d..d14264d 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -8,14 +8,14 @@ spring:
 jwt:
   header: Authorization
   prefix: Bearer
-  secret: dummy-secret
+  secret: dummy-secret-key-for-testing
   access-valid-seconds: 3600
   refresh-valid-seconds: 1209600
 
 assu:
   security:
     school-crypto:
-      base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="
+      base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=" #"dummy-base64-key"를 Base64로 인코딩한 값
 
 cloud:
   aws:
@@ -30,9 +30,7 @@ cloud:
       secretKey: dummy-secret
 
 firebase:
-  project-id: dummy-firebase
-  credentials:
-    path: classpath:firebase/service-account.json
+  enabled: false
 
 kakao:
   base-url: https://dapi.kakao.com
diff --git a/src/test/resources/firebase/service-account.json b/src/test/resources/firebase/service-account.json
deleted file mode 100644
index c16d5f8..0000000
--- a/src/test/resources/firebase/service-account.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "type": "service_account",
-  "project_id": "dummy-project-id",
-  "private_key_id": "dummy-private-key-id",
-  "private_key": "-----BEGIN PRIVATE KEY-----\nDUMMY_PRIVATE_KEY_CONTENT\n-----END PRIVATE KEY-----\n",
-  "client_email": "dummy-service-account@dummy-project.iam.gserviceaccount.com",
-  "client_id": "123456789012345678901",
-  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
-  "token_uri": "https://oauth2.googleapis.com/token",
-  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
-  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dummy-service-account%40dummy-project.iam.gserviceaccount.com",
-  "universe_domain": "googleapis.com"
-}

From 0fd8159422bfaddc6122e2715b889b17a602734a Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 01:57:19 +0900
Subject: [PATCH 123/270] =?UTF-8?q?refactor/#51:=20cicd=20=ED=94=8C?=
 =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=20local?=
 =?UTF-8?q?=20profile=20active=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/cd.yml           | 42 +++++++++++++++++++++++++++++-
 .github/workflows/ci.yml           | 40 ----------------------------
 src/main/resources/application.yml |  8 +-----
 3 files changed, 42 insertions(+), 48 deletions(-)

diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index d948a46..20252a1 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -11,13 +11,53 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          install: true
+
+      - name: Create buildx builder
+        run: |
+          docker buildx create --use --name mybuilder
+          docker buildx inspect --bootstrap
+
+      - name: Login to DockerHub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+      - name: Build & Push Dependency Cache
+        run: |
+          docker buildx build \
+          --builder mybuilder \
+          --platform linux/amd64 \
+          --push \
+          --file Dockerfile \
+          --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
+          --target dependencies \
+          --cache-to type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache,mode=max \
+          .
+
+      - name: Build & Push Final App Image
+        run: |
+          docker buildx build \
+            --builder mybuilder \
+            --platform linux/amd64 \
+            --push \
+            --file Dockerfile \
+            --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:latest \
+            --build-arg DEPENDENCY_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
+            --cache-from type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
+            .
+
       - name: Create application-secret.yml
         run: |
           mkdir -p ./temp_secret
           echo "${{ secrets.APPLICATION_SECRET }}" > ./temp_secret/application-secret.yml
         shell: bash
 
-      - name: Create
+      - name: Create service-account.json
         run: |
           mkdir -p ./temp_secret
           echo "${{ secrets.SERVICE_ACCOUNT }}" > ./temp_secret/service-account.json
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d793980..c8bfd86 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,43 +35,3 @@ jobs:
 
       - name: Build and Test
         run: ./gradlew clean build test
-
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
-        with:
-          install: true
-
-      - name: Create buildx builder
-        run: |
-          docker buildx create --use --name mybuilder
-          docker buildx inspect --bootstrap
-
-      - name: Login to DockerHub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKERHUB_USERNAME }}
-          password: ${{ secrets.DOCKERHUB_TOKEN }}
-
-      - name: Build & Push Dependency Cache
-        run: |
-          docker buildx build \
-          --builder mybuilder \
-          --platform linux/amd64 \
-          --push \
-          --file Dockerfile \
-          --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
-          --target dependencies \
-          --cache-to type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache,mode=max \
-          .
-
-      - name: Build & Push Final App Image
-        run: |
-          docker buildx build \
-            --builder mybuilder \
-            --platform linux/amd64 \
-            --push \
-            --file Dockerfile \
-            --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:latest \
-            --build-arg DEPENDENCY_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
-            --cache-from type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
-            .
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 25d23c9..c76f75b 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -4,8 +4,6 @@ spring:
       initialize-schema: never
     job:
       enabled: false
-  profiles:
-    active: local # 여기에 local, blue, green 셋중 하나로 입력
   config:
     import:
       - optional:classpath:application-secret.yml
@@ -23,8 +21,4 @@ spring:
 logging:
   level:
     org.springframework.web: DEBUG
-    org.springframework.web.client.DefaultRestClient: OFF
-
-kakao:
-  base-url: https://dapi.kakao.com
-  rest-api-key: ${KAKAO_REST_API_KEY}
\ No newline at end of file
+    org.springframework.web.client.DefaultRestClient: OFF
\ No newline at end of file

From a63451680b1a1b6ce349b68787a092a5b9057beb Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 02:51:17 +0900
Subject: [PATCH 124/270] =?UTF-8?q?refactor/#51:=20Rest=20URL=20=EB=84=A4?=
 =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20&=20=EB=B2=84=EC=A0=80=EB=8B=9D=20&=20desc?=
 =?UTF-8?q?ription=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../auth/controller/AuthController.java       | 132 ++++++++++++++----
 1 file changed, 102 insertions(+), 30 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 7346a4f..3e60979 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
@@ -28,7 +28,7 @@
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
-@Tag(name = "Auth", description = "인증/회원가입 API")
+@Tag(name = "Auth", description = "인증/인가 API")
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/auth")
@@ -41,12 +41,16 @@ public class AuthController {
     private final SSUAuthService ssuAuthService;
 
     @Operation(
-            summary = "휴대폰 인증번호 발송 API (추후 개발)",
-            description = "# v1.0\n" +
+            summary = "휴대폰 인증번호 발송 API",
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed801bbcd9f61c3e5f5457?source=copy_link)\n" +
                     "- 입력한 휴대폰 번호로 1회용 인증번호(OTP)를 발송합니다.\n" +
-                    "- 유효시간/재요청 제한 정책은 서버 설정에 따릅니다."
+                    "- 유효시간/재요청 제한 정책은 서버 설정에 따릅니다.\n" +
+                    "\n**Request Body:**\n" +
+                    "  - `phoneNumber` (String, required): 인증번호를 받을 휴대폰 번호\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 성공 메시지 반환"
     )
-    @PostMapping("/phone-numbers/send")
+    @PostMapping("/phone-verification/send")
     public BaseResponse sendAuthNumber(
             @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthSendRequest request
     ) {
@@ -55,12 +59,17 @@ public BaseResponse sendAuthNumber(
     }
 
     @Operation(
-            summary = "휴대폰 인증번호 검증 API (추후 개발)",
-            description = "# v1.0\n" +
+            summary = "휴대폰 인증번호 검증 API",
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed81bb8c05d9061c0306c0?source=copy_link)\n" +
                     "- 발송된 인증번호(OTP)를 검증합니다.\n" +
-                    "- 성공 시 서버에 휴대폰 인증 상태가 기록됩니다."
+                    "- 성공 시 서버에 휴대폰 인증 상태가 기록됩니다.\n" +
+                    "\n**Request Body:**\n" +
+                    "  - `phoneNumber` (String, required): 인증받을 휴대폰 번호\n" +
+                    "  - `authNumber` (String, required): 발송받은 인증번호(OTP)\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 성공 메시지 반환"
     )
-    @PostMapping("/phone-numbers/verify")
+    @PostMapping("/phone-verification/verify")
     public BaseResponse checkAuthNumber(
             @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthVerifyRequest request
     ) {
@@ -73,12 +82,24 @@ public BaseResponse checkAuthNumber(
 
     @Operation(
             summary = "학생 회원가입 API",
-            description = "# v1.0 (2025-08-15)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971?source=copy_link)\n" +
                     "- `application/json` 요청 바디를 사용합니다.\n" +
                     "- 처리: users + ssu_auth 등 가입 레코드 생성, 휴대폰 인증 여부 확인.\n" +
-                    "- 성공 시 201(Created)과 생성된 memberId 반환."
+                    "- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+                    "\n**Request Body:**\n" +
+                    "  - `StudentSignUpRequest` 객체 (JSON, required): 학생 가입 정보\n" +
+                    "  - `email` (String, required): 이메일 주소\n" +
+                    "  - `password` (String, required): 비밀번호\n" +
+                    "  - `phoneNumber` (String, required): 휴대폰 번호\n" +
+                    "  - `studentNumber` (String, required): 학번\n" +
+                    "  - `name` (String, required): 학생 이름\n" +
+                    "  - `major` (Major enum, required): 전공\n" +
+                    "  - `grade` (Integer, required): 학년\n" +
+                    "  - `semester` (Integer, required): 학기\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환"
     )
-    @PostMapping(value = "/signup/student", consumes = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/students/signup", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse signupStudent(
             @Valid @RequestBody StudentSignUpRequest request) {
         return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupStudent(request));
@@ -86,13 +107,25 @@ public BaseResponse signupStudent(
 
     @Operation(
             summary = "제휴업체 회원가입 API",
-            description = "# v1.0 (2025-08-15)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80d7a8f2c3a6fcd8b537?source=copy_link)\n" +
                     "- `multipart/form-data`로 호출합니다.\n" +
                     "- 파트: `payload`(JSON, PartnerSignUpRequest) + `licenseImage`(파일, 사업자등록증).\n" +
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
-                    "- 성공 시 201(Created)과 생성된 memberId 반환."
+                    "- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+                    "\n**Request Parts:**\n" +
+                    "  - `request` (JSON, required): `PartnerSignUpRequest` 객체\n" +
+                    "  - `email` (String, required): 이메일 주소\n" +
+                    "  - `password` (String, required): 비밀번호\n" +
+                    "  - `phoneNumber` (String, required): 휴대폰 번호\n" +
+                    "  - `companyName` (String, required): 회사명\n" +
+                    "  - `businessNumber` (String, required): 사업자등록번호\n" +
+                    "  - `representativeName` (String, required): 대표자명\n" +
+                    "  - `address` (String, required): 회사 주소\n" +
+                    "  - `licenseImage` (MultipartFile, required): 사업자등록증 이미지 파일\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환"
     )
-    @PostMapping(value = "/signup/partner", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    @PostMapping(value = "/partners/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse signupPartner(
             @Valid @RequestPart("request")
             @Parameter(
@@ -117,13 +150,24 @@ public BaseResponse signupPartner(
 
     @Operation(
             summary = "관리자 회원가입 API",
-            description = "# v1.0 (2025-08-15)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48?source=copy_link)\n" +
                     "- `multipart/form-data`로 호출합니다.\n" +
                     "- 파트: `payload`(JSON, AdminSignUpRequest) + `signImage`(파일, 신분증).\n" +
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
-                    "- 성공 시 201(Created)과 생성된 memberId 반환."
+                    "- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+                    "\n**Request Parts:**\n" +
+                    "  - `request` (JSON, required): `AdminSignUpRequest` 객체\n" +
+                    "  - `email` (String, required): 이메일 주소\n" +
+                    "  - `password` (String, required): 비밀번호\n" +
+                    "  - `phoneNumber` (String, required): 휴대폰 번호\n" +
+                    "  - `name` (String, required): 관리자 이름\n" +
+                    "  - `department` (String, required): 소속 부서\n" +
+                    "  - `position` (String, required): 직책\n" +
+                    "  - `signImage` (MultipartFile, required): 인감 이미지 파일\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환"
     )
-    @PostMapping(value = "/signup/admin", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    @PostMapping(value = "/admins/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse signupAdmin(
             @Valid @RequestPart("request")
             @Parameter(
@@ -148,17 +192,26 @@ public BaseResponse signupAdmin(
     // 로그인 (파트너/관리자 공통)
     @Operation(
             summary = "공통 로그인 API",
-            description = "# v1.0 (2025-08-15)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50?source=copy_link)\n" +
                     "- `application/json`로 호출합니다.\n" +
                     "- 바디: `LoginRequest(email, password)`.\n" +
                     "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
-                    "- 성공 시 200(OK)과 토큰/만료시각 반환."
+                    "- 성공 시 200(OK)과 토큰/만료시각 반환.\n" +
+                    "\n**Request Body:**\n" +
+                    "  - `CommonLoginRequest` 객체 (JSON, required): 로그인 정보\n" +
+                    "  - `email` (String, required): 이메일 주소\n" +
+                    "  - `password` (String, required): 비밀번호\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 `LoginResponse` 객체 반환\n" +
+                    "  - `accessToken` (String): 액세스 토큰\n" +
+                    "  - `refreshToken` (String): 리프레시 토큰\n" +
+                    "  - `expiresAt` (LocalDateTime): 토큰 만료 시각"
     )
     @io.swagger.v3.oas.annotations.parameters.RequestBody(
             required = true,
             content = @Content(schema = @Schema(implementation = CommonLoginRequest.class))
     )
-    @PostMapping(value = "/login/common", consumes = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/commons/login", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse loginCommon(
             @RequestBody @Valid CommonLoginRequest request
     ) {
@@ -169,17 +222,27 @@ public BaseResponse loginCommon(
     // 학생 로그인
     @Operation(
             summary = "학생 로그인 API",
-            description = "# v1.1 (2025-08-18)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8?source=copy_link)\n" +
                     "- `application/json`로 호출합니다.\n" +
                     "- 바디: `바디: `StudentLoginRequest(studentNumber, studentPassword, school)`.\n" +
                     "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
-                    "- 성공 시 200(OK)과 토큰/만료시각 반환."
+                    "- 성공 시 200(OK)과 토큰/만료시각 반환.\n" +
+                    "\n**Request Body:**\n" +
+                    "  - `StudentLoginRequest` 객체 (JSON, required): 학생 로그인 정보\n" +
+                    "  - `studentNumber` (String, required): 학번\n" +
+                    "  - `studentPassword` (String, required): 학생 포털 비밀번호\n" +
+                    "  - `school` (String, required): 학교명\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 `LoginResponse` 객체 반환\n" +
+                    "  - `accessToken` (String): 액세스 토큰\n" +
+                    "  - `refreshToken` (String): 리프레시 토큰\n" +
+                    "  - `expiresAt` (LocalDateTime): 토큰 만료 시각"
     )
     @io.swagger.v3.oas.annotations.parameters.RequestBody(
             required = true,
             content = @Content(schema = @Schema(implementation = StudentLoginRequest.class))
     )
-    @PostMapping(value = "/login/student", consumes = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/students/login", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse loginStudent(
             @RequestBody @Valid StudentLoginRequest request
     ) {
@@ -190,11 +253,20 @@ public BaseResponse loginStudent(
     // 액세스 토큰 갱신
     @Operation(
             summary = "Access Token 갱신 API",
-            description = "# v1.0 (2025-08-15)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed806ea8cff29f9cd8695a?source=copy_link)\n" +
                     "- 헤더로 호출합니다.\n" +
                     "- 헤더: `Authorization: Bearer `(만료 허용), `RefreshToken: `.\n" +
                     "- 처리: Refresh 검증/회전 후 신규 Access/Refresh 발급 및 저장.\n" +
-                    "- 성공 시 200(OK)과 새 토큰/만료시각 반환."
+                    "- 성공 시 200(OK)과 새 토큰/만료시각 반환.\n" +
+                    "\n**Headers:**\n" +
+                    "  - `Authorization` (String, required): Bearer 토큰 형식의 액세스 토큰 (만료 허용)\n" +
+                    "  - `RefreshToken` (String, required): 리프레시 토큰\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 `RefreshResponse` 객체 반환\n" +
+                    "  - `accessToken` (String): 새로운 액세스 토큰\n" +
+                    "  - `refreshToken` (String): 새로운 리프레시 토큰\n" +
+                    "  - `expiresAt` (LocalDateTime): 새 토큰 만료 시각\n" +
+                    "  - 성공 시 200(OK)과 새 토큰/만료시각 반환."
     )
     @Parameters({
             @Parameter(name = "Authorization", description = "Access Token (만료 허용). 형식: `Bearer `", required = true,
@@ -202,7 +274,7 @@ public BaseResponse loginStudent(
             @Parameter(name = "RefreshToken", description = "Refresh Token", required = true,
                     in = ParameterIn.HEADER, schema = @Schema(type = "string"))
     })
-    @PostMapping("/refresh")
+    @PostMapping("/tokens/refresh")
     public BaseResponse refreshToken(
             @RequestHeader("RefreshToken") String refreshToken
     ) {
@@ -213,7 +285,7 @@ public BaseResponse refreshToken(
     // 로그아웃
     @Operation(
             summary = "로그아웃 API",
-            description = "# v1.0 (2025-08-15)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed809e9a09fcd741f554c8?source=copy_link)\n" +
                     "- 헤더로 호출합니다.\n" +
                     "- 헤더: `Authorization: Bearer `.\n" +
                     "- 처리: Refresh 무효화(선택), Access 블랙리스트 등록.\n" +
@@ -233,7 +305,7 @@ public BaseResponse logout(
     // 숭실대 인증 및 개인정보 조회
     @Operation(
             summary = "숭실대 유세인트 인증 API",
-            description = "# v1.0 (2025-08-20)\n" +
+            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed808d9266e641e5c4ea14?source=copy_link)\n" +
                     "- `application/json`으로 호출합니다.\n" +
                     "- 요청 바디: `USaintAuthRequest(sToken, sIdno)`.\n" +
                     "- 처리 순서:\n" +
@@ -248,7 +320,7 @@ public BaseResponse logout(
             required = true,
             content = @Content(schema = @Schema(implementation = USaintAuthRequest.class))
     )
-    @PostMapping(value = "/schools/ssu", consumes = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/students/ssu-verify", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse ssuAuth(
             @RequestBody @Valid USaintAuthRequest request
     ) {

From d6a1b41900d0be9a25cc1ffe818b02456be63e5f Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 02:55:07 +0900
Subject: [PATCH 125/270] refactor/#51: ./gradlew clean build test
 --no-build-cache

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c8bfd86..bb76cd0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,4 +34,4 @@ jobs:
         run: chmod +x ./gradlew
 
       - name: Build and Test
-        run: ./gradlew clean build test
+        run: ./gradlew clean build test --no-build-cache

From 256bdd2c95403365d1e604d281b72a7eec28dd73 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 03:05:19 +0900
Subject: [PATCH 126/270] =?UTF-8?q?refactor/#51:=20=ED=85=8C=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=20=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=20redis=20?=
 =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EB=B0=8F=20Mock=20bean=EC=9C=BC=EB=A1=9C?=
 =?UTF-8?q?=20=EB=93=B1=EB=A1=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/global/config/RedisConfig.java |  2 ++
 .../com/assu/server/ServerApplicationTests.java    | 14 +++++++++++++-
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/global/config/RedisConfig.java b/src/main/java/com/assu/server/global/config/RedisConfig.java
index b645501..35a9aba 100644
--- a/src/main/java/com/assu/server/global/config/RedisConfig.java
+++ b/src/main/java/com/assu/server/global/config/RedisConfig.java
@@ -8,6 +8,7 @@
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -17,6 +18,7 @@
 @Configuration
 @EnableConfigurationProperties(RedisProperties.class)
 @RequiredArgsConstructor
+@Profile("!test")
 public class RedisConfig {
 
     private final RedisProperties properties;
diff --git a/src/test/java/com/assu/server/ServerApplicationTests.java b/src/test/java/com/assu/server/ServerApplicationTests.java
index 0324e67..d4f5b19 100644
--- a/src/test/java/com/assu/server/ServerApplicationTests.java
+++ b/src/test/java/com/assu/server/ServerApplicationTests.java
@@ -7,6 +7,8 @@
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.context.TestConfiguration;
 import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.test.context.ActiveProfiles;
 
 @SpringBootTest
@@ -22,8 +24,18 @@ static class MockConfig {
 		FirebaseMessaging firebaseMessaging() {
 			return Mockito.mock(FirebaseMessaging.class);
 		}
-	}
 
+		@Bean
+		RedisConnectionFactory redisConnectionFactory() {
+			return Mockito.mock(RedisConnectionFactory.class);
+		}
+
+		@Bean
+        @SuppressWarnings("unchecked")
+		RedisTemplate redisTemplate() {
+			return Mockito.mock(RedisTemplate.class);
+		}
+	}
 
 	@Test
 	void contextLoads() {

From 9ae1c553591225b6dd673cee47a27d01e40d3f89 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 03:10:40 +0900
Subject: [PATCH 127/270] =?UTF-8?q?refactor/#51:=20jwt=EB=82=B4=EC=9D=98?=
 =?UTF-8?q?=20redis=20=EC=B4=88=EA=B8=B0=ED=99=94=20null-safe=20=EC=B2=98?=
 =?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/auth/security/jwt/JwtUtil.java     | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
index 84238bf..4306e5d 100644
--- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
@@ -49,7 +49,9 @@ public class JwtUtil {
 
     @PostConstruct
     public void clearRedisOnStartup() {
-        redisTemplate.getConnectionFactory().getConnection().flushAll();
+        if (redisTemplate != null && redisTemplate.getConnectionFactory() != null) {
+            redisTemplate.getConnectionFactory().getConnection().flushAll();
+        }
     }
 
     // ───────── 토큰 생성 공통 유틸 ─────────

From afcd97fd82aeec169438f398bc7df1d906463d2c Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 03:14:24 +0900
Subject: [PATCH 128/270] =?UTF-8?q?refactor/#51:=20jwtUtil=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=A0=9C=EC=99=B8,=20Mo?=
 =?UTF-8?q?ck=20Bean=20=EC=A3=BC=EC=9E=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/auth/security/jwt/JwtUtil.java   | 2 ++
 src/test/java/com/assu/server/ServerApplicationTests.java   | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
index 4306e5d..b3ae7f3 100644
--- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
@@ -16,6 +16,7 @@
 import jakarta.annotation.PostConstruct;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -33,6 +34,7 @@
  */
 @Component
 @RequiredArgsConstructor
+@Profile("!test")
 public class JwtUtil {
 
     @Value("${jwt.secret}")
diff --git a/src/test/java/com/assu/server/ServerApplicationTests.java b/src/test/java/com/assu/server/ServerApplicationTests.java
index d4f5b19..c081b76 100644
--- a/src/test/java/com/assu/server/ServerApplicationTests.java
+++ b/src/test/java/com/assu/server/ServerApplicationTests.java
@@ -1,5 +1,6 @@
 package com.assu.server;
 
+import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.google.firebase.messaging.FirebaseMessaging;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
@@ -35,6 +36,11 @@ RedisConnectionFactory redisConnectionFactory() {
 		RedisTemplate redisTemplate() {
 			return Mockito.mock(RedisTemplate.class);
 		}
+
+		@Bean
+		JwtUtil jwtUtil() {
+			return Mockito.mock(JwtUtil.class);
+		}
 	}
 
 	@Test

From 8ee7fe1d2bc0c5989eaa887acfe218bbe3a91e29 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 03:32:56 +0900
Subject: [PATCH 129/270] =?UTF-8?q?refactor/#51:=20string=20redis=20templa?=
 =?UTF-8?q?te=20=EC=A0=9C=EC=99=B8,=20redis=EC=9E=90=EC=B2=B4=EB=A5=BC=20t?=
 =?UTF-8?q?est=EC=97=90=EC=84=9C=20=EB=B0=B0=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/test/java/com/assu/server/ServerApplicationTests.java | 6 ++++++
 src/test/resources/application-test.yml                   | 4 ++++
 2 files changed, 10 insertions(+)

diff --git a/src/test/java/com/assu/server/ServerApplicationTests.java b/src/test/java/com/assu/server/ServerApplicationTests.java
index c081b76..5faba62 100644
--- a/src/test/java/com/assu/server/ServerApplicationTests.java
+++ b/src/test/java/com/assu/server/ServerApplicationTests.java
@@ -10,6 +10,7 @@
 import org.springframework.context.annotation.Bean;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.test.context.ActiveProfiles;
 
 @SpringBootTest
@@ -37,6 +38,11 @@ RedisTemplate redisTemplate() {
 			return Mockito.mock(RedisTemplate.class);
 		}
 
+		@Bean
+		StringRedisTemplate stringRedisTemplate() {
+			return Mockito.mock(StringRedisTemplate.class);
+		}
+
 		@Bean
 		JwtUtil jwtUtil() {
 			return Mockito.mock(JwtUtil.class);
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index d14264d..95151d4 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -1,4 +1,8 @@
 spring:
+  autoconfigure:
+    exclude:
+      - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
+      - org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
   datasource:
     url: jdbc:h2:mem:testdb
     driver-class-name: org.h2.Driver

From a875f64891368bb388c9870ce37b697be3362cb4 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Wed, 3 Sep 2025 11:40:13 +0900
Subject: [PATCH 130/270] =?UTF-8?q?[FEAT/#52]=20partnershipUsage=20id=20?=
 =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=84=9C=20=EB=A6=AC=EB=B7=B0=20=EC=83=81?=
 =?UTF-8?q?=ED=83=9C=20=EB=B0=94=EA=BF=94=EC=A3=BC=EA=B8=B0=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/review/converter/ReviewConverter.java     |  6 +-----
 .../server/domain/review/dto/ReviewRequestDTO.java   |  2 ++
 .../server/domain/review/dto/ReviewResponseDTO.java  |  2 +-
 .../domain/review/service/ReviewServiceImpl.java     | 12 ++++++++++++
 .../server/domain/user/entity/PartnershipUsage.java  |  3 +++
 .../user/repository/PartnershipUsageRepository.java  |  3 +++
 .../global/apiPayload/code/status/ErrorStatus.java   |  1 +
 7 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
index eb0d0d8..29983e2 100644
--- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
+++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
@@ -45,6 +45,7 @@ public static ReviewResponseDTO.CheckStudentReviewResponseDTO checkStudentReview
                 .rate(review.getRate())
                 .content(review.getContent())
                 .createdAt(review.getCreatedAt())
+                .storeName(review.getStore().getName())
                 .storeId(review.getStore().getId())
                 .reviewImageUrls(review.getImageList().stream()
                         .map(ReviewPhoto::getPhotoUrl)
@@ -75,11 +76,6 @@ public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReview
                 .build();
 
     }
-    // public static List checkPartnerReviewResultDTO(List reviews){
-    //     return reviews.stream()
-    //             .map(ReviewConverter::checkPartnerReviewResultDTO)
-    //             .collect(Collectors.toList());
-    // }
 
     public static Page checkPartnerReviewResultDTO(Page reviews){
         return reviews.map(ReviewConverter::checkPartnerReviewResultDTO);
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
index 5dfa6be..cf4c492 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
@@ -28,5 +28,7 @@ public static class WriteReviewRequestDTO {
 
         @Schema(description = "파트너 ID", example = "2")
         private Long partnerId;
+
+        private Long partnershipUsageId;
     }
 }
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
index 638b79e..c0b9be3 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
@@ -28,6 +28,7 @@ public static class WriteReviewResponseDTO {
     public static class CheckStudentReviewResponseDTO { //내가 작성한 리뷰
         private Long reviewId;
         private Long storeId;
+        private String storeName;
         private String content;
         private Integer rate;
         private LocalDateTime createdAt;
@@ -44,7 +45,6 @@ public static class CheckPartnerReviewResponseDTO {//partner의 리뷰 확인
         private String content;
         private Integer rate;
         private LocalDateTime createdAt;
-        private String sortBy; // 정렬기준 -> 최신, 별점, 오래된 순
         private List reviewImageUrls;
     }
 
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 14074ca..8906fe6 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -10,17 +10,21 @@
 import com.assu.server.domain.review.entity.ReviewPhoto;
 import com.assu.server.domain.review.repository.ReviewRepository;
 import com.assu.server.domain.store.entity.Store;
+import com.assu.server.domain.user.entity.PartnershipUsage;
 import com.assu.server.domain.user.entity.Student;
 import com.assu.server.domain.store.repository.StoreRepository;
+import com.assu.server.domain.user.repository.PartnershipUsageRepository;
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
+import com.assu.server.global.exception.GeneralException;
 import com.assu.server.global.util.PrincipalDetails;
 import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 
 import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.stereotype.Service;
@@ -41,12 +45,18 @@ public class ReviewServiceImpl implements ReviewService {
     private final PartnerRepository partnerRepository;
     private final StudentRepository studentRepository;
     private final AmazonS3Manager amazonS3Manager;
+    private final PartnershipUsageRepository partnershipUsageRepository;
 
 
     @Override
     public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages) {
         // createReview 메서드 호출로 통합
         Review review = createReview(memberId, request.getStoreId(), request, reviewImages);
+        PartnershipUsage pu = partnershipUsageRepository.findById(request.getPartnershipUsageId()).orElseThrow(
+            () -> new GeneralException(ErrorStatus.NO_SUCH_USAGE)
+        );
+        pu.setIsReviewed(true);
+        partnershipUsageRepository.save(pu);
         return ReviewConverter.writeReviewResultDTO(review);
     }
 
@@ -97,6 +107,7 @@ private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteR
 
     @Override
     public Page checkStudentReview(Long memberId, Pageable pageable) {
+        pageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort());
         Page reviews = reviewRepository.findByMemberId(memberId, pageable);
 
         for (Review review : reviews) {
@@ -109,6 +120,7 @@ public Page checkStudentReview(
     @Override
     @Transactional
     public Page checkPartnerReview(Long memberId, Pageable pageable) {
+        pageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort());
         Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
         Store store = storeRepository.findByPartner(partner)
diff --git a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java
index 09b87d2..d70e33a 100644
--- a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java
+++ b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java
@@ -38,5 +38,8 @@ public class PartnershipUsage extends BaseEntity {
 	private Long paperId;
 	private Long contentId;
 
+	public void setIsReviewed(Boolean isReviewed) {
+		this.isReviewed = isReviewed;
+	}
 
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
index bf82668..7553436 100644
--- a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
+++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.user.repository;
 
 import java.util.List;
+import java.util.Optional;
 
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
@@ -31,4 +32,6 @@ List findByYearAndMonth(
 		@Param("year") int year,
 		@Param("month") int month
 	);
+
+	Optional findById(Long id);
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index ee342da..22d6a9f 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -45,6 +45,7 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4003","존재하지 않는 partner ID 입니다."),
     NO_SUCH_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4004","존재하지 않는 student ID 입니다."),
     NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_4006", "존재하지 않는 스토어 ID입니다."),
+    NO_SUCH_USAGE(HttpStatus.NOT_FOUND, "USAGE4001", "존재하지 않는 제휴 사용 내역입니다."),
     NO_PAPER_FOR_STORE(HttpStatus.NOT_FOUND, "ADMIN_4005", "존재하지 않는 paper ID입니다."),
     EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4005","이미 존재하는 전화번호입니다."),
     EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."),

From e92245c1325f8c14b4c54d8f9cdc58f6516c916c Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 16:08:12 +0900
Subject: [PATCH 131/270] =?UTF-8?q?deploy/#56:=20secrets=20=EB=B3=B5?=
 =?UTF-8?q?=EC=82=AC=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/cd.yml | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 20252a1..9d66d6b 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -52,15 +52,11 @@ jobs:
             .
 
       - name: Create application-secret.yml
-        run: |
-          mkdir -p ./temp_secret
-          echo "${{ secrets.APPLICATION_SECRET }}" > ./temp_secret/application-secret.yml
+        run: echo "${{ secrets.APPLICATION_SECRET }}" > ./application-secret.yml
         shell: bash
 
       - name: Create service-account.json
-        run: |
-          mkdir -p ./temp_secret
-          echo "${{ secrets.SERVICE_ACCOUNT }}" > ./temp_secret/service-account.json
+        run: echo "${{ secrets.SERVICE_ACCOUNT }}" > ./service-account.json
         shell: bash
 
       - name: Copy application-secret.yml to EC2
@@ -69,7 +65,7 @@ jobs:
           username: ubuntu
           host: ${{ secrets.EC2_HOST }}
           key: ${{ secrets.EC2_SSH_KEY }}
-          source: ./temp_secret/application-secret.yml
+          source: ./application-secret.yml
           target: /home/ubuntu/secret/
 
       - name: Copy service-account.json to EC2
@@ -78,7 +74,7 @@ jobs:
           username: ubuntu
           host: ${{ secrets.EC2_HOST }}
           key: ${{ secrets.EC2_SSH_KEY }}
-          source: ./temp_secret/service-account.json
+          source: ./service-account.json
           target: /home/ubuntu/secret/
 
       - name: Copy docker-compose.yml

From 93bdabbb0ca74ef496ececaf7135fb78595ce791 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 17:15:41 +0900
Subject: [PATCH 132/270] =?UTF-8?q?deploy/#59:=20deploy.sh=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 deploy.sh | 96 +++++++++++++++++++++++++++++++++----------------------
 1 file changed, 58 insertions(+), 38 deletions(-)

diff --git a/deploy.sh b/deploy.sh
index 90e61e3..6af033a 100644
--- a/deploy.sh
+++ b/deploy.sh
@@ -2,7 +2,10 @@
 
 cd /home/ubuntu/cicd
 
+# 환경변수 설정
 APP_NAME="assu"
+TARGET=""
+OLD=""
 
 # NGINX 설정 관련
 NGINX_CONF_PATH="/etc/nginx"
@@ -10,59 +13,62 @@ BLUE_CONF="blue.conf"
 GREEN_CONF="green.conf"
 DEFAULT_CONF="nginx.conf"
 MAX_RETRIES=3
+RETRY_SLEEP_SEC=5
+HEALTH_CHECK_PORT=""
 
 # 활성화된 서비스 확인 및 스위칭 대상 결정
 determine_target() {
   if docker compose -f docker-compose.yml ps | grep -q "app-blue.*Up"; then
     TARGET="green"
     OLD="blue"
+    HEALTH_CHECK_PORT="8081"
   elif docker compose -f docker-compose.yml ps | grep -q "app-green.*Up"; then
     TARGET="blue"
     OLD="green"
+    HEALTH_CHECK_PORT="8080"
   else
     TARGET="blue"  # 첫 실행 시 기본값
     OLD="none"
+    HEALTH_CHECK_PORT="8080"
   fi
 
   echo "TARGET: $TARGET"
   echo "OLD: $OLD"
 }
-# 헬스체크 실패 시 롤백 처리
+
+# docker ps 기반 헬스체크: 컨테이너 상태가 Up이면 성공으로 판단
 health_check() {
-  local URL=$1
   local RETRIES=0
-  local ORIGINAL_TARGET=$TARGET  # 원래 TARGET 값을 저장
-
-  while [ $RETRIES -lt $MAX_RETRIES ]; do
-    echo "Checking service at $URL... (attempt: $((RETRIES + 1)))"
-    sleep 3
+    local CONTAINER_NAME="app-$TARGET"
 
-    # 현재 실행 중인 컨테이너 확인
-    CONTAINER_RUNNING=$(docker ps --filter "name=app-$TARGET" --format '{{.Names}}')
+    echo "Starting docker-ps based health check for '$CONTAINER_NAME'..."
 
-    if [ "$CONTAINER_RUNNING" = "app-$TARGET" ]; then
-      echo "$TARGET container is running."
-      return 0  # 컨테이너가 실행 중이라면 헬스체크 성공
-    else
-      echo "$TARGET container is not running."
-    fi
+    # 초기 대기 (컨테이너 기동 시간 확보)
+    sleep 10
 
-    RETRIES=$((RETRIES + 1))
-  done
+    while [ $RETRIES -lt $MAX_RETRIES ]; do
+      echo "Attempt $((RETRIES + 1)) of $MAX_RETRIES: checking docker ps status..."
 
-  # 헬스체크 실패 시 롤백 처리
-  echo "Health check failed after $MAX_RETRIES attempts."
-  echo "Rolling back to the original target: $ORIGINAL_TARGET"
+      # docker ps에서 해당 컨테이너의 상태 문자열을 가져옴 (예: "Up 10 seconds")
+      local STATUS
+      STATUS=$(docker ps --filter "name=^${CONTAINER_NAME}$" --format '{{.Status}}' || true)
 
-  # TARGET을 원래 값으로 롤백
-  TARGET=$ORIGINAL_TARGET
-  echo "Rolled back TARGET: $TARGET"
+      if [ -n "$STATUS" ] && [[ "$STATUS" == Up* ]]; then
+        echo "Health check succeeded! '$CONTAINER_NAME' is Up. (status: $STATUS)"
+        return 0
+      else
+        # 상태 디버깅용 출력
+        docker compose -f docker-compose.yml ps || true
+        docker logs --tail=50 "$CONTAINER_NAME" 2>/dev/null || true
+        echo "Current status: '${STATUS:-N/A}'. Retrying in ${RETRY_SLEEP_SEC}s..."
+        sleep "$RETRY_SLEEP_SEC"
+      fi
 
-  # 로그 파일에 실패 기록
-  echo "Failed health check for $TARGET container" > /home/ubuntu/cicd/health_check_failure.log
+      RETRIES=$((RETRIES + 1))
+    done
 
-  # docker-compose down 대신 실패 기록 후 종료
-  exit 1
+    echo "Health check failed after $MAX_RETRIES attempts."
+    return 1
 }
 
 # NGINX 설정 스위칭 함수
@@ -74,32 +80,46 @@ switch_nginx_conf() {
   fi
 
   echo "Reloading NGINX configuration..."
-  nginx -s reload
+  sudo nginx -s reload
 }
 
 # 이전 컨테이너 종료 함수
 down_old_container() {
   if [ "$OLD" != "none" ]; then
     echo "Stopping old container: $OLD"
-    sudo docker stop "app-$OLD"
-
+    docker compose -f docker-compose.yml stop "app-$OLD" || true
+    docker compose -f docker-compose.yml rm -f "app-$OLD" || true
   fi
 }
 
 # 메인 실행 로직
 main() {
+  # 대상 컨테이너 결정
   determine_target
 
+  local TARGET_SERVICE="app-$TARGET"
 
-  # 대상 컨테이너 실행
-  echo "Starting $TARGET container..."
-  docker compose -f docker-compose.yml up -d "app-$TARGET"
+  # 컨테이너 충돌 방지: compose/비compose 둘 다 제거 시도
+  echo "Removing any existing container with the name '$TARGET_SERVICE'..."
+  docker compose -f docker-compose.yml rm -f "$TARGET_SERVICE" 2>/dev/null || true
+  docker rm -f "$TARGET_SERVICE" 2>/dev/null || true
 
-  # 헬스체크
-  if [ "$TARGET" = "blue" ]; then
-    health_check "http://127.0.0.1:8080/actuator/health"
-  else
-    health_check "http://127.0.0.1:8081/actuator/health"
+  # 새 컨테이너 실행
+  echo "Starting new container: $TARGET_SERVICE..."
+  docker compose -f docker-compose.yml up -d "$TARGET_SERVICE"
+
+  # docker-ps 기반 헬스 체크 및 롤백
+  if ! health_check; then
+    echo "Health check failed. Initiating rollback..."
+
+    # 실패한 컨테이너를 중지하고 제거
+    echo "Removing failed container: '$TARGET_SERVICE'..."
+    docker compose -f docker-compose.yml stop "$TARGET_SERVICE" || true
+    docker compose -f docker-compose.yml rm -f "$TARGET_SERVICE" || true
+    docker rm -f "$TARGET_SERVICE" 2>/dev/null || true
+
+    echo "Rollback complete. The previous version remains active."
+    exit 1
   fi
 
   # NGINX 설정 스위칭

From d87bf7eb2c9dc6850b4737b493db3dc1473f4c85 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Wed, 3 Sep 2025 18:51:39 +0900
Subject: [PATCH 133/270] =?UTF-8?q?[FEAT/#52]=20=EB=A6=AC=EB=B7=B0=20?=
 =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=98=ED=99=98=20dto=20=EC=82=AD?=
 =?UTF-8?q?=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/review/controller/ReviewController.java |  8 ++++----
 .../domain/review/converter/ReviewConverter.java   | 10 +++++-----
 .../domain/review/dto/ReviewResponseDTO.java       | 14 +++++++-------
 .../domain/review/repository/ReviewRepository.java |  3 ++-
 .../domain/review/service/ReviewService.java       |  2 +-
 .../domain/review/service/ReviewServiceImpl.java   |  6 +++---
 .../domain/user/controller/StudentController.java  |  2 ++
 7 files changed, 24 insertions(+), 21 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
index a2be857..49abadb 100644
--- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
+++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
@@ -16,6 +16,7 @@
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
@@ -57,12 +58,11 @@ public BaseResponse> check
             description = "삭제할 리뷰 ID를 입력해주세요."
     )
     @DeleteMapping("/{reviewId}")
-    public BaseResponse deleteReview(
-            @AuthenticationPrincipal PrincipalDetails pd,
+    public ResponseEntity> deleteReview(
             @PathVariable Long reviewId) {
-        Long memberId = pd.getMember().getId();
+        reviewService.deleteReview(reviewId);
 
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.deleteReview(reviewId));
+        return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus._OK));
     }
 
     @Operation(
diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
index 29983e2..1282162 100644
--- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
+++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
@@ -80,9 +80,9 @@ public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReview
     public static Page checkPartnerReviewResultDTO(Page reviews){
         return reviews.map(ReviewConverter::checkPartnerReviewResultDTO);
     }
-    public static ReviewResponseDTO.DeleteReviewResponseDTO deleteReviewResultDTO(Long reviewId){
-        return ReviewResponseDTO.DeleteReviewResponseDTO.builder()
-                .reviewId(reviewId)
-                .build();
-    }
+    // public static ReviewResponseDTO.DeleteReviewResponseDTO deleteReviewResultDTO(Long reviewId){
+    //     return ReviewResponseDTO.DeleteReviewResponseDTO.builder()
+    //             .reviewId(reviewId)
+    //             .build();
+    // }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
index c0b9be3..5430dc0 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
@@ -49,12 +49,12 @@ public static class CheckPartnerReviewResponseDTO {//partner의 리뷰 확인
     }
 
 
-    @Getter
-    @NoArgsConstructor
-    @AllArgsConstructor
-    @Builder
-    public static class DeleteReviewResponseDTO {
-        private Long reviewId;
-    }
+    // @Getter
+    // @NoArgsConstructor
+    // @AllArgsConstructor
+    // @Builder
+    // public static class DeleteReviewResponseDTO {
+    //     private Long reviewId;
+    // }
 
 }
diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
index 7b505a6..0269477 100644
--- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
+++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
@@ -18,7 +18,8 @@ public interface ReviewRepository extends JpaRepository {
     ORDER BY r.createdAt DESC
 """)
     Page findByMemberId(@Param("memberId") Long memberId, Pageable pageable);
-    List findByStoreId(Long storeId);
+    // List findByStoreId(Long storeId);
 
     Page findByStoreIdOrderByCreatedAtDesc(Long id, Pageable pageable);//최신순 정렬
+    Page findByStoreId(Long id, Pageable pageable);
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
index 72ef6c3..240e87b 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
@@ -15,5 +15,5 @@ public interface ReviewService {
     ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages);
     Page checkStudentReview(Long memberId, Pageable pageable);
     Page checkPartnerReview(Long memberId, Pageable pageable);
-    ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(@PathVariable Long reviewId);
+    void deleteReview(@PathVariable Long reviewId);
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 8906fe6..26f0fe1 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -126,7 +126,7 @@ public Page checkPartnerReview(
         Store store = storeRepository.findByPartner(partner)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
 
-        Page reviews = reviewRepository.findByStoreIdOrderByCreatedAtDesc(store.getId(), pageable);
+        Page reviews = reviewRepository.findByStoreId(store.getId(), pageable);
 
         for (Review review : reviews) {
             updateReviewImageUrls(review);
@@ -136,9 +136,9 @@ public Page checkPartnerReview(
     }
     @Override
     @Transactional
-    public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) {
+    public void deleteReview(Long reviewId) {
         reviewRepository.deleteById(reviewId);
-        return ReviewConverter.deleteReviewResultDTO(reviewId);
+
     }
     private void updateReviewImageUrls(Review review) {
         for (ReviewPhoto reviewPhoto : review.getImageList()) {
diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
index ee8630f..4cb83ad 100644
--- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java
+++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
@@ -38,6 +38,8 @@ public ResponseEntity> getMyPartn
 	}
 
 
+
+
     @Operation(
             summary = "스탬프 조회 API",
             description = "Authorization 후에 사용해주세요."

From dcb43f01cff9c1be05f4c08d8d15dbaa8af309a6 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Wed, 3 Sep 2025 19:25:39 +0900
Subject: [PATCH 134/270] =?UTF-8?q?[FIX/#52]=20=EB=A6=AC=EB=B7=B0=20Entity?=
 =?UTF-8?q?=20=EC=99=80=20partnershipUsage=20Entity=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../converter/PartnershipConverter.java           |  1 +
 .../partnership/dto/PartnershipRequestDTO.java    |  1 +
 .../domain/review/dto/ReviewRequestDTO.java       |  1 +
 .../assu/server/domain/review/entity/Review.java  |  2 ++
 .../domain/review/service/ReviewServiceImpl.java  |  8 ++++++++
 .../domain/user/dto/StudentResponseDTO.java       |  1 +
 .../domain/user/entity/PartnershipUsage.java      |  2 ++
 .../domain/user/service/StudentServiceImpl.java   | 15 +--------------
 8 files changed, 17 insertions(+), 14 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index fb1ae37..7394ced 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -28,6 +28,7 @@ public class PartnershipConverter {
 
 	public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Student student) {
 		return PartnershipUsage.builder()
+			.adminName(dto.getAdminName())
 			.date(LocalDate.now())
 			.place(dto.getPlaceName())
 			.student(student)
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
index a305e35..1d9344c 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
@@ -14,6 +14,7 @@
 public class PartnershipRequestDTO {
     @Getter
     public static class finalRequest{
+        String adminName;
         String placeName;
         String partnershipContent;
         Long contentId;
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
index cf4c492..e8db66a 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewRequestDTO.java
@@ -30,5 +30,6 @@ public static class WriteReviewRequestDTO {
         private Long partnerId;
 
         private Long partnershipUsageId;
+        private String adminName;
     }
 }
diff --git a/src/main/java/com/assu/server/domain/review/entity/Review.java b/src/main/java/com/assu/server/domain/review/entity/Review.java
index 51a6c5f..e7e64f5 100644
--- a/src/main/java/com/assu/server/domain/review/entity/Review.java
+++ b/src/main/java/com/assu/server/domain/review/entity/Review.java
@@ -55,4 +55,6 @@ public List getImageList() {
 
 	private Integer rate;
 	private String content;
+
+	private String adminName;
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 26f0fe1..dcfc569 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -51,6 +51,7 @@ public class ReviewServiceImpl implements ReviewService {
     @Override
     public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages) {
         // createReview 메서드 호출로 통합
+        String affiliation = adminNameToAffliation(request.getAdminName());
         Review review = createReview(memberId, request.getStoreId(), request, reviewImages);
         PartnershipUsage pu = partnershipUsageRepository.findById(request.getPartnershipUsageId()).orElseThrow(
             () -> new GeneralException(ErrorStatus.NO_SUCH_USAGE)
@@ -60,6 +61,13 @@ public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.Wri
         return ReviewConverter.writeReviewResultDTO(review);
     }
 
+    private String adminNameToAffliation(String adminName) {
+        if(adminName.equals("총학생회")|| adminName.equals("숭실대학교 총학생회"))
+            return "숭실대학교 재학생";
+        else
+            return adminName.replace(" 학생회"," 재학생");
+    }
+
     private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteReviewRequestDTO request, List images) {
         // 존재여부 검증
         Store store = storeRepository.findById(storeId)
diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
index ff18d75..bda0e37 100644
--- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
@@ -32,6 +32,7 @@ public static class myPartnership {
 	@AllArgsConstructor
 	@Builder
 	public static class UsageDetailDTO {
+		private String adminName;
 		private Long partnershipUsageId;
 		private String storeName;
 		private Long partnerId;
diff --git a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java
index d70e33a..9ab3c00 100644
--- a/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java
+++ b/src/main/java/com/assu/server/domain/user/entity/PartnershipUsage.java
@@ -38,6 +38,8 @@ public class PartnershipUsage extends BaseEntity {
 	private Long paperId;
 	private Long contentId;
 
+	private String adminName;
+
 	public void setIsReviewed(Boolean isReviewed) {
 		this.isReviewed = isReviewed;
 	}
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
index e8e3945..fff02cf 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
@@ -38,20 +38,6 @@ public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) {
 	public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) {
 		List usages = partnershipUsageRepository.findByYearAndMonth(studentId, year, month);
 
-		// return StudentResponseDTO.myPartnership.builder()
-		// 	.serviceCount(usages.size())
-		// 	.details(usages.stream()
-		// 		.map(u ->
-		// 			StudentResponseDTO.UsageDetailDTO.builder()
-		// 			.partnershipUsageId(u.getId())
-		// 			.storeName(u.getPlace())
-		// 			.usedAt(u.getDate())
-		// 			.benefitDescription(u.getPartnershipContent())
-		// 			.isReviewed(u.getIsReviewed())
-		// 			.build()
-		// 		).toList()
-		// 	)
-		// 	.build();
 		return StudentResponseDTO.myPartnership.builder()
 			.serviceCount(usages.size())
 			.details(usages.stream()
@@ -66,6 +52,7 @@ public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int yea
 
 					return StudentResponseDTO.UsageDetailDTO.builder()
 						.partnershipUsageId(u.getId())
+						.adminName(u.getAdminName())
 						.storeName(u.getPlace())
 						.usedAt(u.getDate())
 						.benefitDescription(u.getPartnershipContent())

From 39a5e3e1325b4913a4edadba3ed6c268f1ba0786 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Wed, 3 Sep 2025 20:04:13 +0900
Subject: [PATCH 135/270] =?UTF-8?q?[FIX/#52]=20=EB=A6=AC=EB=B7=B0=20?=
 =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=8B=9C=20affiliation=20=EC=B1=84?=
 =?UTF-8?q?=EC=9A=B0=EA=B8=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/domain/review/converter/ReviewConverter.java     | 3 ++-
 .../java/com/assu/server/domain/review/entity/Review.java   | 2 +-
 .../server/domain/review/service/ReviewServiceImpl.java     | 6 +++---
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
index 1282162..9cb19db 100644
--- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
+++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
@@ -28,12 +28,13 @@ public static ReviewResponseDTO.WriteReviewResponseDTO writeReviewResultDTO(Revi
                 //한 리뷰 여러개 사진 but 하나로 묶임 추가 고려해보기 --추후에 !!
                 .build(); //리스폰스 리턴
     }
-    public static Review toReviewEntity(ReviewRequestDTO.WriteReviewRequestDTO  request, Store store, Partner partner, Student student){
+    public static Review toReviewEntity(ReviewRequestDTO.WriteReviewRequestDTO  request, Store store, Partner partner, Student student, String affiliation) {
         //request
         return Review.builder()
                 .rate(request.getRate())
                 .content(request.getContent())
                 .store(store)
+            .affiliation(affiliation)
                 .partner(partner)
                 .student(student)
                 //    .imageList(request.getReviewImage())
diff --git a/src/main/java/com/assu/server/domain/review/entity/Review.java b/src/main/java/com/assu/server/domain/review/entity/Review.java
index e7e64f5..a4c7a1b 100644
--- a/src/main/java/com/assu/server/domain/review/entity/Review.java
+++ b/src/main/java/com/assu/server/domain/review/entity/Review.java
@@ -56,5 +56,5 @@ public List getImageList() {
 	private Integer rate;
 	private String content;
 
-	private String adminName;
+	private String affiliation;
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index dcfc569..16bb17e 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -52,7 +52,7 @@ public class ReviewServiceImpl implements ReviewService {
     public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages) {
         // createReview 메서드 호출로 통합
         String affiliation = adminNameToAffliation(request.getAdminName());
-        Review review = createReview(memberId, request.getStoreId(), request, reviewImages);
+        Review review = createReview(memberId, request.getStoreId(), request, reviewImages, affiliation);
         PartnershipUsage pu = partnershipUsageRepository.findById(request.getPartnershipUsageId()).orElseThrow(
             () -> new GeneralException(ErrorStatus.NO_SUCH_USAGE)
         );
@@ -68,7 +68,7 @@ private String adminNameToAffliation(String adminName) {
             return adminName.replace(" 학생회"," 재학생");
     }
 
-    private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteReviewRequestDTO request, List images) {
+    private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteReviewRequestDTO request, List images, String affiliation) {
         // 존재여부 검증
         Store store = storeRepository.findById(storeId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
@@ -78,7 +78,7 @@ private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteR
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
 
         // 리뷰 엔티티 생성 및 저장
-        Review review = ReviewConverter.toReviewEntity(request, store, partner, student);
+        Review review = ReviewConverter.toReviewEntity(request, store, partner, student, affiliation);
         reviewRepository.save(review); // ID 생성을 위해 먼저 저장
 
         // 이미지 처리

From 8a085ceeac23ca4fd9dedbeb3cf2318737f0bb90 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 22:57:41 +0900
Subject: [PATCH 136/270] =?UTF-8?q?deploy/#62:=20deploy.sh=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 deploy.sh | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/deploy.sh b/deploy.sh
index 6af033a..55d6ada 100644
--- a/deploy.sh
+++ b/deploy.sh
@@ -14,22 +14,18 @@ GREEN_CONF="green.conf"
 DEFAULT_CONF="nginx.conf"
 MAX_RETRIES=3
 RETRY_SLEEP_SEC=5
-HEALTH_CHECK_PORT=""
 
 # 활성화된 서비스 확인 및 스위칭 대상 결정
 determine_target() {
   if docker compose -f docker-compose.yml ps | grep -q "app-blue.*Up"; then
     TARGET="green"
     OLD="blue"
-    HEALTH_CHECK_PORT="8081"
   elif docker compose -f docker-compose.yml ps | grep -q "app-green.*Up"; then
     TARGET="blue"
     OLD="green"
-    HEALTH_CHECK_PORT="8080"
   else
     TARGET="blue"  # 첫 실행 시 기본값
     OLD="none"
-    HEALTH_CHECK_PORT="8080"
   fi
 
   echo "TARGET: $TARGET"
@@ -44,7 +40,7 @@ health_check() {
     echo "Starting docker-ps based health check for '$CONTAINER_NAME'..."
 
     # 초기 대기 (컨테이너 기동 시간 확보)
-    sleep 10
+    sleep 30
 
     while [ $RETRIES -lt $MAX_RETRIES ]; do
       echo "Attempt $((RETRIES + 1)) of $MAX_RETRIES: checking docker ps status..."
@@ -104,9 +100,9 @@ main() {
   docker compose -f docker-compose.yml rm -f "$TARGET_SERVICE" 2>/dev/null || true
   docker rm -f "$TARGET_SERVICE" 2>/dev/null || true
 
-  # 새 컨테이너 실행
+  # 새 컨테이너 실행 (강제 재생성)
   echo "Starting new container: $TARGET_SERVICE..."
-  docker compose -f docker-compose.yml up -d "$TARGET_SERVICE"
+  docker compose -f docker-compose.yml up -d --force-recreate "$TARGET_SERVICE"
 
   # docker-ps 기반 헬스 체크 및 롤백
   if ! health_check; then

From 76214858565b416b9ef898095b49d8ee64304474 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 3 Sep 2025 22:57:54 +0900
Subject: [PATCH 137/270] =?UTF-8?q?deploy/#62:=20redis=20config=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/global/config/RedisConfig.java     | 19 ++++---------------
 1 file changed, 4 insertions(+), 15 deletions(-)

diff --git a/src/main/java/com/assu/server/global/config/RedisConfig.java b/src/main/java/com/assu/server/global/config/RedisConfig.java
index 35a9aba..135c0c8 100644
--- a/src/main/java/com/assu/server/global/config/RedisConfig.java
+++ b/src/main/java/com/assu/server/global/config/RedisConfig.java
@@ -4,36 +4,26 @@
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import lombok.RequiredArgsConstructor;
-import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
-import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
 @Configuration
-@EnableConfigurationProperties(RedisProperties.class)
 @RequiredArgsConstructor
 @Profile("!test")
 public class RedisConfig {
 
-    private final RedisProperties properties;
-
-    @Bean
-    public RedisConnectionFactory redisConnectionFactory() {
-        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(properties.getHost(), properties.getPort());
-        return lettuceConnectionFactory;
-    }
-
     @Bean
-    public RedisTemplate redisTemplate() {
+    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
         RedisTemplate redisTemplate = new RedisTemplate<>();
-        redisTemplate.setConnectionFactory(redisConnectionFactory());
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+
         redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
 
         ObjectMapper objectMapper = new ObjectMapper()
                 .registerModule(new JavaTimeModule())
@@ -42,7 +32,6 @@ public RedisTemplate redisTemplate() {
         GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
 
         redisTemplate.setValueSerializer(serializer);
-        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
         redisTemplate.setHashValueSerializer(serializer);
 
         redisTemplate.afterPropertiesSet();

From 9d10157a37c27bb58700847ba8e2397909f7390e Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 4 Sep 2025 00:29:17 +0900
Subject: [PATCH 138/270] =?UTF-8?q?deploy/#65:=20service=20account=20decod?=
 =?UTF-8?q?e=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/cd.yml | 214 +++++++++++++++++++--------------------
 1 file changed, 107 insertions(+), 107 deletions(-)

diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 9d66d6b..72acabc 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -1,108 +1,108 @@
-name: CD Pipeline
-
-on:
-  push:
-    branches: [ main ]
-
-jobs:
-  cd:
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v4
-
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
-        with:
-          install: true
-
-      - name: Create buildx builder
-        run: |
-          docker buildx create --use --name mybuilder
-          docker buildx inspect --bootstrap
-
-      - name: Login to DockerHub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKERHUB_USERNAME }}
-          password: ${{ secrets.DOCKERHUB_TOKEN }}
-
-      - name: Build & Push Dependency Cache
-        run: |
-          docker buildx build \
-          --builder mybuilder \
-          --platform linux/amd64 \
-          --push \
-          --file Dockerfile \
-          --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
-          --target dependencies \
-          --cache-to type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache,mode=max \
-          .
-
-      - name: Build & Push Final App Image
-        run: |
-          docker buildx build \
-            --builder mybuilder \
-            --platform linux/amd64 \
-            --push \
-            --file Dockerfile \
-            --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:latest \
-            --build-arg DEPENDENCY_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
-            --cache-from type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
-            .
-
-      - name: Create application-secret.yml
-        run: echo "${{ secrets.APPLICATION_SECRET }}" > ./application-secret.yml
-        shell: bash
-
-      - name: Create service-account.json
-        run: echo "${{ secrets.SERVICE_ACCOUNT }}" > ./service-account.json
-        shell: bash
-
-      - name: Copy application-secret.yml to EC2
-        uses: appleboy/scp-action@v0.1.3
-        with:
-          username: ubuntu
-          host: ${{ secrets.EC2_HOST }}
-          key: ${{ secrets.EC2_SSH_KEY }}
-          source: ./application-secret.yml
-          target: /home/ubuntu/secret/
-
-      - name: Copy service-account.json to EC2
-        uses: appleboy/scp-action@v0.1.3
-        with:
-          username: ubuntu
-          host: ${{ secrets.EC2_HOST }}
-          key: ${{ secrets.EC2_SSH_KEY }}
-          source: ./service-account.json
-          target: /home/ubuntu/secret/
-
-      - name: Copy docker-compose.yml
-        uses: appleboy/scp-action@v0.1.3
-        with:
-          username: ubuntu
-          host: ${{ secrets.EC2_HOST }}
-          key: ${{ secrets.EC2_SSH_KEY }}
-          source: ./docker-compose.yml
-          target: /home/ubuntu/cicd/
-
-      - name: Upload deploy.sh to EC2
-        uses: appleboy/scp-action@v0.1.3
-        with:
-          host: ${{ secrets.EC2_HOST }}
-          username: ${{ secrets.EC2_USER }}
-          key: ${{ secrets.EC2_SSH_KEY }}
-          source: ./deploy.sh
-          target: /home/ubuntu/cicd/
-
-      - name: SSH and Deploy
-        uses: appleboy/ssh-action@v1.0.0
-        with:
-          host: ${{ secrets.EC2_HOST }}
-          username: ${{ secrets.EC2_USER }}
-          key: ${{ secrets.EC2_SSH_KEY }}
-          script: |
-            echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
-            docker pull ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:latest
-            sudo chmod +x /home/ubuntu/cicd/deploy.sh
+name: CD Pipeline
+
+on:
+  push:
+    branches: [ main ]
+
+jobs:
+  cd:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          install: true
+
+      - name: Create buildx builder
+        run: |
+          docker buildx create --use --name mybuilder
+          docker buildx inspect --bootstrap
+
+      - name: Login to DockerHub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+      - name: Build & Push Dependency Cache
+        run: |
+          docker buildx build \
+          --builder mybuilder \
+          --platform linux/amd64 \
+          --push \
+          --file Dockerfile \
+          --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
+          --target dependencies \
+          --cache-to type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache,mode=max \
+          .
+
+      - name: Build & Push Final App Image
+        run: |
+          docker buildx build \
+            --builder mybuilder \
+            --platform linux/amd64 \
+            --push \
+            --file Dockerfile \
+            --tag ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:latest \
+            --build-arg DEPENDENCY_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
+            --cache-from type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/assu-app:dependency-cache \
+            .
+
+      - name: Create application-secret.yml
+        run: echo "${{ secrets.APPLICATION_SECRET }}" > ./application-secret.yml
+        shell: bash
+
+      - name: Create service-account.json
+        run: echo "${{ secrets.SERVICE_ACCOUNT_B64 }}" | base64 -d > ./service-account.json
+        shell: bash
+
+      - name: Copy application-secret.yml to EC2
+        uses: appleboy/scp-action@v0.1.3
+        with:
+          username: ubuntu
+          host: ${{ secrets.EC2_HOST }}
+          key: ${{ secrets.EC2_SSH_KEY }}
+          source: ./application-secret.yml
+          target: /home/ubuntu/secret/
+
+      - name: Copy service-account.json to EC2
+        uses: appleboy/scp-action@v0.1.3
+        with:
+          username: ubuntu
+          host: ${{ secrets.EC2_HOST }}
+          key: ${{ secrets.EC2_SSH_KEY }}
+          source: ./service-account.json
+          target: /home/ubuntu/secret/
+
+      - name: Copy docker-compose.yml
+        uses: appleboy/scp-action@v0.1.3
+        with:
+          username: ubuntu
+          host: ${{ secrets.EC2_HOST }}
+          key: ${{ secrets.EC2_SSH_KEY }}
+          source: ./docker-compose.yml
+          target: /home/ubuntu/cicd/
+
+      - name: Upload deploy.sh to EC2
+        uses: appleboy/scp-action@v0.1.3
+        with:
+          host: ${{ secrets.EC2_HOST }}
+          username: ${{ secrets.EC2_USER }}
+          key: ${{ secrets.EC2_SSH_KEY }}
+          source: ./deploy.sh
+          target: /home/ubuntu/cicd/
+
+      - name: SSH and Deploy
+        uses: appleboy/ssh-action@v1.0.0
+        with:
+          host: ${{ secrets.EC2_HOST }}
+          username: ${{ secrets.EC2_USER }}
+          key: ${{ secrets.EC2_SSH_KEY }}
+          script: |
+            echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
+            docker pull ${{ secrets.DOCKERHUB_USERNAME }}/assu-app:latest
+            sudo chmod +x /home/ubuntu/cicd/deploy.sh
             sudo /home/ubuntu/cicd/deploy.sh
\ No newline at end of file

From 5665cf2b37886e9a44e5784d4ee1bcb71ec3cc2e Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 4 Sep 2025 04:05:50 +0900
Subject: [PATCH 139/270] =?UTF-8?q?[FEAT/#65]=20infra=20=ED=8C=A8=ED=82=A4?=
 =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=9C=84=ED=95=B4=EC=84=9C=20?=
 =?UTF-8?q?firebase=20=EA=B4=80=EB=A0=A8=20=ED=8C=8C=EC=9D=BC=20infra/fire?=
 =?UTF-8?q?base=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20=EC=9D=B4?=
 =?UTF-8?q?=EB=8F=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../notification/service/NotificationCommandServiceImpl.java    | 2 +-
 .../domain/notification/service/NotificationDispatcher.java     | 2 +-
 .../java/com/assu/server/infra/{ => firebase}/FcmClient.java    | 2 +-
 .../assu/server/infra/{ => firebase}/NotificationFactory.java   | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)
 rename src/main/java/com/assu/server/infra/{ => firebase}/FcmClient.java (98%)
 rename src/main/java/com/assu/server/infra/{ => firebase}/NotificationFactory.java (98%)

diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index 6c9cc2f..81a6af1 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -14,7 +14,7 @@
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
 import com.assu.server.global.exception.GeneralException;
-import com.assu.server.infra.NotificationFactory;
+import com.assu.server.infra.firebase.NotificationFactory;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java b/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
index 56b9d85..a76b2ef 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
@@ -3,7 +3,7 @@
 import com.assu.server.domain.notification.entity.Notification;
 import com.assu.server.domain.notification.entity.NotificationOutbox;
 import com.assu.server.domain.notification.repository.NotificationOutboxRepository;
-import com.assu.server.infra.FcmClient;
+import com.assu.server.infra.firebase.FcmClient;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.scheduling.annotation.Scheduled;
diff --git a/src/main/java/com/assu/server/infra/FcmClient.java b/src/main/java/com/assu/server/infra/firebase/FcmClient.java
similarity index 98%
rename from src/main/java/com/assu/server/infra/FcmClient.java
rename to src/main/java/com/assu/server/infra/firebase/FcmClient.java
index c189db8..6a94865 100644
--- a/src/main/java/com/assu/server/infra/FcmClient.java
+++ b/src/main/java/com/assu/server/infra/firebase/FcmClient.java
@@ -1,4 +1,4 @@
-package com.assu.server.infra;
+package com.assu.server.infra.firebase;
 
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
 import com.assu.server.domain.member.entity.Member;
diff --git a/src/main/java/com/assu/server/infra/NotificationFactory.java b/src/main/java/com/assu/server/infra/firebase/NotificationFactory.java
similarity index 98%
rename from src/main/java/com/assu/server/infra/NotificationFactory.java
rename to src/main/java/com/assu/server/infra/firebase/NotificationFactory.java
index bfeefa6..16e78cc 100644
--- a/src/main/java/com/assu/server/infra/NotificationFactory.java
+++ b/src/main/java/com/assu/server/infra/firebase/NotificationFactory.java
@@ -1,4 +1,4 @@
-package com.assu.server.infra;
+package com.assu.server.infra.firebase;
 
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.notification.entity.Notification;

From a47499a2d1737e11d80b89639fa81f050749ddcc Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 4 Sep 2025 04:06:21 +0900
Subject: [PATCH 140/270] =?UTF-8?q?[FEAT/#47]=20auth=20=EC=8B=9C=ED=81=90?=
 =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=ED=95=84=ED=84=B0=20=EC=A0=9C=EC=99=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assu/server/global/config/SecurityConfig.java    | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

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 e01f8e3..8b22b93 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -26,16 +26,12 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
                                 "/swagger-resources/**", "/webjars/**"
                         ).permitAll()
-                        // 로그인/회원가입/재발급만 공개
-                        .requestMatchers(
-                                "/auth/login/common",
-                                "/auth/login/student",
-                                "/auth/signup/**",
-                                "/auth/refresh",
-                                "/auth/phone-numbers/**"
-                        ).permitAll()
                         // 로그아웃은 인증 필요
                         .requestMatchers("/auth/logout").authenticated()
+                        // 그 외 Auth 전체 공개
+                        .requestMatchers(
+                                "/auth/**"
+                        ).permitAll()
                         // 나머지는 인증 필요
                         .anyRequest().authenticated()
                 )

From 96b0460897356a4af63e8388e5b4dc157f43bfa0 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 4 Sep 2025 04:07:47 +0900
Subject: [PATCH 141/270] =?UTF-8?q?[FEAT/#47]=20phone-verification=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 알리고 SMS 클라이언트, DTO, Exception 구현
- PhoneAuthServiceImpl에서 알리고 클라이언트를 통해 SMS 전송 및 redis를 통해 인증 구현
---
 .../auth/controller/AuthController.java       | 20 +++----
 .../auth/service/PhoneAuthServiceImpl.java    | 18 +++---
 .../apiPayload/code/status/ErrorStatus.java   |  3 +
 .../infra/aligo/client/AligoSmsClient.java    | 60 +++++++++++++++++++
 .../infra/aligo/dto/AligoSendResponse.java    | 10 ++++
 .../infra/aligo/exception/AligoException.java | 11 ++++
 6 files changed, 105 insertions(+), 17 deletions(-)
 create mode 100644 src/main/java/com/assu/server/infra/aligo/client/AligoSmsClient.java
 create mode 100644 src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java
 create mode 100644 src/main/java/com/assu/server/infra/aligo/exception/AligoException.java

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 3e60979..896e432 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
@@ -42,7 +42,7 @@ public class AuthController {
 
     @Operation(
             summary = "휴대폰 인증번호 발송 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed801bbcd9f61c3e5f5457?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed801bbcd9f61c3e5f5457?source=copy_link)\n" +
                     "- 입력한 휴대폰 번호로 1회용 인증번호(OTP)를 발송합니다.\n" +
                     "- 유효시간/재요청 제한 정책은 서버 설정에 따릅니다.\n" +
                     "\n**Request Body:**\n" +
@@ -60,7 +60,7 @@ public BaseResponse sendAuthNumber(
 
     @Operation(
             summary = "휴대폰 인증번호 검증 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed81bb8c05d9061c0306c0?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed81bb8c05d9061c0306c0?source=copy_link)\n" +
                     "- 발송된 인증번호(OTP)를 검증합니다.\n" +
                     "- 성공 시 서버에 휴대폰 인증 상태가 기록됩니다.\n" +
                     "\n**Request Body:**\n" +
@@ -82,7 +82,7 @@ public BaseResponse checkAuthNumber(
 
     @Operation(
             summary = "학생 회원가입 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971?source=copy_link)\n" +
                     "- `application/json` 요청 바디를 사용합니다.\n" +
                     "- 처리: users + ssu_auth 등 가입 레코드 생성, 휴대폰 인증 여부 확인.\n" +
                     "- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
@@ -107,7 +107,7 @@ public BaseResponse signupStudent(
 
     @Operation(
             summary = "제휴업체 회원가입 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80d7a8f2c3a6fcd8b537?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80d7a8f2c3a6fcd8b537?source=copy_link)\n" +
                     "- `multipart/form-data`로 호출합니다.\n" +
                     "- 파트: `payload`(JSON, PartnerSignUpRequest) + `licenseImage`(파일, 사업자등록증).\n" +
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
@@ -150,7 +150,7 @@ public BaseResponse signupPartner(
 
     @Operation(
             summary = "관리자 회원가입 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48?source=copy_link)\n" +
                     "- `multipart/form-data`로 호출합니다.\n" +
                     "- 파트: `payload`(JSON, AdminSignUpRequest) + `signImage`(파일, 신분증).\n" +
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
@@ -192,7 +192,7 @@ public BaseResponse signupAdmin(
     // 로그인 (파트너/관리자 공통)
     @Operation(
             summary = "공통 로그인 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50?source=copy_link)\n" +
                     "- `application/json`로 호출합니다.\n" +
                     "- 바디: `LoginRequest(email, password)`.\n" +
                     "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
@@ -222,7 +222,7 @@ public BaseResponse loginCommon(
     // 학생 로그인
     @Operation(
             summary = "학생 로그인 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8?source=copy_link)\n" +
                     "- `application/json`로 호출합니다.\n" +
                     "- 바디: `바디: `StudentLoginRequest(studentNumber, studentPassword, school)`.\n" +
                     "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
@@ -253,7 +253,7 @@ public BaseResponse loginStudent(
     // 액세스 토큰 갱신
     @Operation(
             summary = "Access Token 갱신 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed806ea8cff29f9cd8695a?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed806ea8cff29f9cd8695a?source=copy_link)\n" +
                     "- 헤더로 호출합니다.\n" +
                     "- 헤더: `Authorization: Bearer `(만료 허용), `RefreshToken: `.\n" +
                     "- 처리: Refresh 검증/회전 후 신규 Access/Refresh 발급 및 저장.\n" +
@@ -285,7 +285,7 @@ public BaseResponse refreshToken(
     // 로그아웃
     @Operation(
             summary = "로그아웃 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed809e9a09fcd741f554c8?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed809e9a09fcd741f554c8?source=copy_link)\n" +
                     "- 헤더로 호출합니다.\n" +
                     "- 헤더: `Authorization: Bearer `.\n" +
                     "- 처리: Refresh 무효화(선택), Access 블랙리스트 등록.\n" +
@@ -305,7 +305,7 @@ public BaseResponse logout(
     // 숭실대 인증 및 개인정보 조회
     @Operation(
             summary = "숭실대 유세인트 인증 API",
-            description = "#[v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed808d9266e641e5c4ea14?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed808d9266e641e5c4ea14?source=copy_link)\n" +
                     "- `application/json`으로 호출합니다.\n" +
                     "- 요청 바디: `USaintAuthRequest(sToken, sIdno)`.\n" +
                     "- 처리 순서:\n" +
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 bd73185..6db58c4 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
@@ -3,10 +3,11 @@
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.util.RandomNumberUtil;
 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 lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.core.ValueOperations;
-import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.time.Duration;
@@ -16,20 +17,23 @@
 public class PhoneAuthServiceImpl implements PhoneAuthService {
 
     private final StringRedisTemplate redisTemplate;
+    private final AligoSmsClient aligoSmsClient;
 
     private static final Duration AUTH_CODE_TTL = Duration.ofMinutes(5); // 인증번호 5분 유효
 
-    @Async
     @Override
     public void sendAuthNumber(String phoneNumber) {
         String authNumber = RandomNumberUtil.generateSixDigit();
+        redisTemplate.opsForValue().set(phoneNumber, authNumber, AUTH_CODE_TTL);
 
-        ValueOperations valueOps = redisTemplate.opsForValue();
-        valueOps.set(phoneNumber, authNumber, AUTH_CODE_TTL);
+        String message = "[ASSU] 인증번호: " + authNumber;
+
+        AligoSendResponse response = aligoSmsClient.sendSms(phoneNumber, message, "사용자");
 
-        // 알리고 API로 실제 문자 발송 처리 필요
-        // 예: aligoService.sendSms(phoneNumber, authNumber);
-        System.out.println("[SMS] 전송 대상: " + phoneNumber + ", 인증번호: " + authNumber);
+        // 실패 처리
+        if (!response.getResult_code().equals("1")) {
+            throw new CustomAuthException(ErrorStatus.FAILED_TO_SEND_SMS);
+        }
     }
 
     @Override
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index ee342da..9e26569 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -30,6 +30,9 @@ public enum ErrorStatus implements BaseErrorCode {
     SSU_SAINT_PARSE_FAILED(HttpStatus.UNAUTHORIZED, "SSU4002", "숭실대학교 유세인트 포털 크롤링 파싱에 실패했습니다."),
     SSU_SAINT_UNSUPPORTED_MAJOR(HttpStatus.UNAUTHORIZED, "SSU4003", "지원하는 학과가 아닙니다."),
 
+    // 알리고 SMS 전송 관련 에러
+    FAILED_TO_SEND_SMS(HttpStatus.INTERNAL_SERVER_ERROR, "ALIGO500", "알리고 SMS 전송에 실패했습니다."),
+
     // 인증 에러
     NOT_VERIFIED_PHONE_NUMBER(HttpStatus.BAD_REQUEST,"AUTH_4007","전화번호 인증에 실패했습니다."),
 
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
new file mode 100644
index 0000000..9578538
--- /dev/null
+++ b/src/main/java/com/assu/server/infra/aligo/client/AligoSmsClient.java
@@ -0,0 +1,60 @@
+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.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Component
+@RequiredArgsConstructor
+public class AligoSmsClient {
+
+    private final WebClient webClient;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Value("${aligo.key}")
+    private String apiKey;
+
+    @Value("${aligo.user-id}")
+    private String userId;
+
+    @Value("${aligo.sender}")
+    private String sender;
+
+    private static final String SEND_URL = "https://apis.aligo.in/send/";
+
+    public AligoSendResponse sendSms(String phoneNumber, String message, String name) {
+        MultiValueMap params = new LinkedMultiValueMap<>();
+        params.add("key", apiKey);
+        params.add("userid", userId);
+        params.add("sender", sender);
+        params.add("receiver", phoneNumber);
+        params.add("msg", message);
+        params.add("msg_type", "SMS");
+        params.add("destination", phoneNumber + "|" + name);
+
+        String body = webClient.post()
+                .uri(SEND_URL)
+                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+                .body(BodyInserters.fromFormData(params))
+                .retrieve()
+                .bodyToMono(String.class)
+                .block(); // 동기로 변환
+
+        try {
+            return objectMapper.readValue(body, AligoSendResponse.class);
+        } catch (Exception e) {
+            throw new AligoException(ErrorStatus.FAILED_TO_SEND_SMS);
+        }
+    }
+}
+
diff --git a/src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java b/src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java
new file mode 100644
index 0000000..2e77e7b
--- /dev/null
+++ b/src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java
@@ -0,0 +1,10 @@
+package com.assu.server.infra.aligo.dto;
+
+import lombok.Data;
+
+@Data
+public class AligoSendResponse {
+    private String result_code; // 성공 여부
+    private String message;     // 결과 메시지
+    private String msg_id;      // 메시지 ID
+}
diff --git a/src/main/java/com/assu/server/infra/aligo/exception/AligoException.java b/src/main/java/com/assu/server/infra/aligo/exception/AligoException.java
new file mode 100644
index 0000000..d8ee9c4
--- /dev/null
+++ b/src/main/java/com/assu/server/infra/aligo/exception/AligoException.java
@@ -0,0 +1,11 @@
+package com.assu.server.infra.aligo.exception;
+
+import com.assu.server.global.apiPayload.code.BaseErrorCode;
+import com.assu.server.global.exception.GeneralException;
+
+public class AligoException  extends GeneralException {
+
+    public AligoException(BaseErrorCode errorCode) {
+        super(errorCode);
+    }
+}

From 368f3af4a7ddf2644600306efb6c0f76e9076d67 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 4 Sep 2025 04:16:22 +0900
Subject: [PATCH 142/270] =?UTF-8?q?[FEAT/#47]=20phone-verification=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- test에 알리고 추가
---
 src/test/resources/application-test.yml | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 95151d4..26e940b 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -38,4 +38,9 @@ firebase:
 
 kakao:
   base-url: https://dapi.kakao.com
-  rest-api-key: dummy-kakao-key
\ No newline at end of file
+  rest-api-key: dummy-kakao-key
+
+aligo:
+  key: dummy-aligo-key
+  user-id: dummy-user-id
+  sender: 01012345678
\ No newline at end of file

From 3210777f42bc10ed624acc1146141e9272c3bc36 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Thu, 4 Sep 2025 20:40:36 +0900
Subject: [PATCH 143/270] =?UTF-8?q?[FEAT/#52]=20=EB=A6=AC=EB=B7=B0=20?=
 =?UTF-8?q?=ED=8F=89=EA=B7=A0=20=EC=A1=B0=ED=9A=8C=20api?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../review/controller/ReviewController.java   | 58 +++++++++++++----
 .../review/converter/ReviewConverter.java     | 50 +++++++--------
 .../domain/review/dto/ReviewResponseDTO.java  | 25 +++++---
 .../review/repository/ReviewRepository.java   |  6 ++
 .../domain/review/service/ReviewService.java  | 11 +++-
 .../review/service/ReviewServiceImpl.java     | 64 +++++++++++++++----
 6 files changed, 153 insertions(+), 61 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
index 49abadb..ca5b4fd 100644
--- a/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
+++ b/src/main/java/com/assu/server/domain/review/controller/ReviewController.java
@@ -7,10 +7,6 @@
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Encoding;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import lombok.RequiredArgsConstructor;
 
 import org.springframework.data.domain.Page;
@@ -47,32 +43,66 @@ public BaseResponse writeReview(
             description = "Authorization 후에 사용해주세요."
     )
     @GetMapping("/student")
-    public BaseResponse> checkStudent(
+    public BaseResponse> checkStudent(
             @AuthenticationPrincipal PrincipalDetails pd, Pageable pageable
     ) {
         return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStudentReview(pd.getId(), pageable));
     }
 
+    @Operation(
+        summary = "내 가게 리뷰 조회 API",
+        description = "Authorization 후에 사용해주세요."
+    )
+    @GetMapping("/partner")
+    public BaseResponse> checkPartnerReview(
+        @AuthenticationPrincipal PrincipalDetails pd, Pageable pageable
+    ){
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview(pd.getId(), pageable));
+    }
+
+    @Operation(
+        summary = "가게 리뷰 조회 API",
+        description = "storeId 기반으로 가게 리뷰를 조회하는 API 입니다."
+    )
+    @GetMapping("/store/{storeId}")
+    public BaseResponse> checkStoreReview(
+        Pageable pageable, @PathVariable Long storeId
+    ){
+        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkStoreReview(storeId, pageable));
+    }
+
     @Operation(
             summary = "내가 쓴 리뷰 삭제 API",
             description = "삭제할 리뷰 ID를 입력해주세요."
     )
     @DeleteMapping("/{reviewId}")
-    public ResponseEntity> deleteReview(
+    public ResponseEntity> deleteReview(
             @PathVariable Long reviewId) {
-        reviewService.deleteReview(reviewId);
 
-        return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus._OK));
+        return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, reviewService.deleteReview(reviewId)));
     }
 
     @Operation(
-            summary = "내 가게 리뷰 조회 API",
-            description = "Authorization 후에 사용해주세요."
+        summary = "store 리뷰 평균 조회 API",
+        description = "storeId 기반으로 조회하는 API 입니다."
     )
-    @GetMapping("/partner")
-    public BaseResponse> checkPartnerReview(
-            @AuthenticationPrincipal PrincipalDetails pd, Pageable pageable
+    @GetMapping("/average/{storeId}")
+    public ResponseEntity> getStandardScore(
+        @PathVariable Long storeId
     ){
-        return BaseResponse.onSuccess(SuccessStatus._OK, reviewService.checkPartnerReview(pd.getId(), pageable));
+        return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, reviewService.standardScore(storeId)));
     }
+
+    @Operation(
+        summary = "store 리뷰 평균 조회 API",
+        description = "partner 로그인 시 자신의 가게 평균을 조회하는 api 입니다."
+    )
+    @GetMapping("/average")
+    public ResponseEntity> getMyStoreAverage(
+        @AuthenticationPrincipal PrincipalDetails pd
+    ){
+        return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, reviewService.myStoreAverage(pd.getId())));
+    }
+
+
 }
diff --git a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
index 9cb19db..234247d 100644
--- a/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
+++ b/src/main/java/com/assu/server/domain/review/converter/ReviewConverter.java
@@ -8,7 +8,6 @@
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.user.entity.Student;
 
-import java.util.List;
 import java.util.stream.Collectors;
 
 import org.springframework.data.domain.Page;
@@ -40,47 +39,48 @@ public static Review toReviewEntity(ReviewRequestDTO.WriteReviewRequestDTO  requ
                 //    .imageList(request.getReviewImage())
                 .build();
     }
-    public static ReviewResponseDTO.CheckStudentReviewResponseDTO checkStudentReviewResultDTO(Review review){
-        return ReviewResponseDTO.CheckStudentReviewResponseDTO.builder()
+    public static ReviewResponseDTO.CheckReviewResponseDTO checkReviewResultDTO(Review review){
+        return ReviewResponseDTO.CheckReviewResponseDTO.builder()
                 .reviewId(review.getId())
                 .rate(review.getRate())
                 .content(review.getContent())
                 .createdAt(review.getCreatedAt())
                 .storeName(review.getStore().getName())
+                .affiliation(review.getAffiliation())
                 .storeId(review.getStore().getId())
                 .reviewImageUrls(review.getImageList().stream()
                         .map(ReviewPhoto::getPhotoUrl)
                         .collect(Collectors.toList()))
                 .build();
     }
-    // public static List checkStudentReviewResultDTO(List reviews){
+    // public static List checkStudentReviewResultDTO(List reviews){
     //     return reviews.stream()
     //             .map(ReviewConverter::checkStudentReviewResultDTO)
     //             .collect(Collectors.toList());
     // }
 
-    public static Page checkStudentReviewResultDTO(Page reviews){
-        return reviews.map(ReviewConverter::checkStudentReviewResultDTO);
-    }
-
-    public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReviewResultDTO(Review review){
-        return ReviewResponseDTO.CheckPartnerReviewResponseDTO.builder()
-                .reviewId(review.getId())
-                .storeId(review.getStore().getId())
-                .reviewerId(review.getStudent().getId())
-                .content(review.getContent())
-                .rate(review.getRate())
-                .createdAt(review.getCreatedAt())
-                .reviewImageUrls(review.getImageList().stream()
-                        .map(ReviewPhoto::getPhotoUrl)
-                        .collect(Collectors.toList()))
-                .build();
-
-    }
-
-    public static Page checkPartnerReviewResultDTO(Page reviews){
-        return reviews.map(ReviewConverter::checkPartnerReviewResultDTO);
+    public static Page checkReviewResultDTO(Page reviews){
+        return reviews.map(ReviewConverter::checkReviewResultDTO);
     }
+    //
+    // public static ReviewResponseDTO.CheckPartnerReviewResponseDTO checkPartnerReviewResultDTO(Review review){
+    //     return ReviewResponseDTO.CheckPartnerReviewResponseDTO.builder()
+    //             .reviewId(review.getId())
+    //             .storeId(review.getStore().getId())
+    //             .reviewerId(review.getStudent().getId())
+    //             .content(review.getContent())
+    //             .rate(review.getRate())
+    //             .createdAt(review.getCreatedAt())
+    //             .reviewImageUrls(review.getImageList().stream()
+    //                     .map(ReviewPhoto::getPhotoUrl)
+    //                     .collect(Collectors.toList()))
+    //             .build();
+    //
+    // }
+    //
+    // public static Page checkPartnerReviewResultDTO(Page reviews){
+    //     return reviews.map(ReviewConverter::checkPartnerReviewResultDTO);
+    // }
     // public static ReviewResponseDTO.DeleteReviewResponseDTO deleteReviewResultDTO(Long reviewId){
     //     return ReviewResponseDTO.DeleteReviewResponseDTO.builder()
     //             .reviewId(reviewId)
diff --git a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
index 5430dc0..13e84b9 100644
--- a/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/review/dto/ReviewResponseDTO.java
@@ -25,9 +25,10 @@ public static class WriteReviewResponseDTO {
     @NoArgsConstructor
     @AllArgsConstructor
     @Builder
-    public static class CheckStudentReviewResponseDTO { //내가 작성한 리뷰
+    public static class CheckReviewResponseDTO { //내가 작성한 리뷰
         private Long reviewId;
         private Long storeId;
+        private String affiliation; // store 기준 조회시 필요...
         private String storeName;
         private String content;
         private Integer rate;
@@ -49,12 +50,20 @@ public static class CheckPartnerReviewResponseDTO {//partner의 리뷰 확인
     }
 
 
-    // @Getter
-    // @NoArgsConstructor
-    // @AllArgsConstructor
-    // @Builder
-    // public static class DeleteReviewResponseDTO {
-    //     private Long reviewId;
-    // }
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class DeleteReviewResponseDTO {
+        private Long reviewId;
+    }
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class StandardScoreResponseDTO {
+        private Float score;
+    }
 
 }
diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
index 0269477..8648241 100644
--- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
+++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.review.repository;
 
 import com.assu.server.domain.review.entity.Review;
+import com.assu.server.domain.store.entity.Store;
 
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -22,4 +23,9 @@ public interface ReviewRepository extends JpaRepository {
 
     Page findByStoreIdOrderByCreatedAtDesc(Long id, Pageable pageable);//최신순 정렬
     Page findByStoreId(Long id, Pageable pageable);
+
+    @Query("SELECT AVG(r.rate) FROM Review r WHERE r.store.id = :storeId")
+    Float standardScore(Long storeId);
+
+    Long store(Store store);
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewService.java b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
index 240e87b..699d3bc 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewService.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewService.java
@@ -13,7 +13,12 @@
 
 public interface ReviewService {
     ReviewResponseDTO.WriteReviewResponseDTO writeReview(@RequestBody ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages);
-    Page checkStudentReview(Long memberId, Pageable pageable);
-    Page checkPartnerReview(Long memberId, Pageable pageable);
-    void deleteReview(@PathVariable Long reviewId);
+    Page checkStudentReview(Long memberId, Pageable pageable);
+    Page checkPartnerReview(Long memberId, Pageable pageable);
+    Page checkStoreReview(Long storeId, Pageable pageable);
+
+    ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(@PathVariable Long reviewId);
+
+    ReviewResponseDTO.StandardScoreResponseDTO standardScore(@PathVariable Long storeId);
+    ReviewResponseDTO.StandardScoreResponseDTO myStoreAverage(@PathVariable Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
index 16bb17e..df6694a 100644
--- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java
@@ -1,6 +1,5 @@
 package com.assu.server.domain.review.service;
 
-import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.domain.review.converter.ReviewConverter;
@@ -18,7 +17,6 @@
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
 import com.assu.server.global.exception.GeneralException;
-import com.assu.server.global.util.PrincipalDetails;
 import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
@@ -26,16 +24,12 @@
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
-import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.sql.SQLOutput;
 import java.time.LocalDateTime;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
-import java.util.stream.Collectors;
 
 @Service
 @RequiredArgsConstructor
@@ -114,7 +108,7 @@ private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteR
     }
 
     @Override
-    public Page checkStudentReview(Long memberId, Pageable pageable) {
+    public Page checkStudentReview(Long memberId, Pageable pageable) {
         pageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort());
         Page reviews = reviewRepository.findByMemberId(memberId, pageable);
 
@@ -122,12 +116,12 @@ public Page checkStudentReview(
             updateReviewImageUrls(review);
         }
 
-        return ReviewConverter.checkStudentReviewResultDTO(reviews);
+        return ReviewConverter.checkReviewResultDTO(reviews);
     }
 
     @Override
     @Transactional
-    public Page checkPartnerReview(Long memberId, Pageable pageable) {
+    public Page checkPartnerReview(Long memberId, Pageable pageable) {
         pageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort());
         Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
@@ -140,12 +134,16 @@ public Page checkPartnerReview(
             updateReviewImageUrls(review);
         }
 
-        return ReviewConverter.checkPartnerReviewResultDTO(reviews);
+        return ReviewConverter.checkReviewResultDTO(reviews);
     }
+
     @Override
     @Transactional
-    public void deleteReview(Long reviewId) {
+    public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) {
         reviewRepository.deleteById(reviewId);
+        return ReviewResponseDTO.DeleteReviewResponseDTO.builder()
+            .reviewId(reviewId)
+            .build();
 
     }
     private void updateReviewImageUrls(Review review) {
@@ -157,4 +155,48 @@ private void updateReviewImageUrls(Review review) {
             }
         }
     }
+
+    @Override
+    @Transactional
+    public Page checkStoreReview(Long storeId, Pageable pageable) {
+        pageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort());
+        Store store = storeRepository.findById(storeId).orElseThrow(
+            () -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+        Page reviews = reviewRepository.findByStoreId(store.getId(), pageable);
+
+        for (Review review : reviews) {
+            updateReviewImageUrls(review);
+        }
+
+        return ReviewConverter.checkReviewResultDTO(reviews);
+    }
+
+    @Override
+    @Transactional
+    public ReviewResponseDTO.StandardScoreResponseDTO standardScore(Long storeId) {
+        Float score = reviewRepository.standardScore(storeId);
+        if(score == null){
+            score = 0f;
+        }
+        return ReviewResponseDTO.StandardScoreResponseDTO.builder()
+            .score(score)
+            .build();
+    }
+
+    @Override
+    @Transactional
+    public ReviewResponseDTO.StandardScoreResponseDTO myStoreAverage(Long memberId) {
+        Partner partner = partnerRepository.findById(memberId)
+            .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+        Store store = storeRepository.findByPartner(partner)
+            .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+
+        Float score = reviewRepository.standardScore(store.getId());
+        System.out.println(store.getId());
+        return ReviewResponseDTO.StandardScoreResponseDTO
+            .builder()
+            .score(score)
+            .build();
+
+    }
 }

From 4d172a4986046588b6df651b03245b5999fcc3cd Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Thu, 4 Sep 2025 21:44:21 +0900
Subject: [PATCH 144/270] =?UTF-8?q?refactor/#38=20=20-=20git.ignore?=
 =?UTF-8?q?=EC=97=90=20firebase/service-account.json=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitignore | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 9503f1d..1c6856b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,5 @@ out/
 ### Secret ###
 src/main/resources/application-secret.yml
 src/test/resources/application-test.yml
-src/test/resources/application-secret.yml
\ No newline at end of file
+src/test/resources/application-secret.yml
+src/main/resources/firebase/service-account.json
\ No newline at end of file

From f6b41d4168cc649c94e613609d69d8a325b519a6 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Thu, 4 Sep 2025 21:55:06 +0900
Subject: [PATCH 145/270] =?UTF-8?q?refactor/#38=20-=20chatting=20PD=20-=20?=
 =?UTF-8?q?develop=20pull=20&=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/auth/security/JwtAuthFilter.java   | 120 ++++++++++++++++++
 .../chat/controller/ChatController.java       |  45 +++++--
 .../domain/chat/dto/ChatRequestDTO.java       |   2 +-
 .../domain/chat/service/ChatService.java      |  10 +-
 .../domain/chat/service/ChatServiceImpl.java  |  43 +++----
 .../server/domain/store/entity/Store.java     |   2 +-
 .../apiPayload/code/status/ErrorStatus.java   |   8 +-
 .../server/global/util/PrincipalDetails.java  |   2 +
 8 files changed, 184 insertions(+), 48 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java

diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
new file mode 100644
index 0000000..a33dac9
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
@@ -0,0 +1,120 @@
+package com.assu.server.domain.auth.security;
+
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+    @Value("${jwt.header}")
+    private String jwtHeader;
+    private final JwtUtil jwtUtil;
+    private final RedisTemplate redisTemplate;
+
+    private static final AntPathMatcher PATH = new AntPathMatcher();
+    private static final String[] WHITELIST = {
+            "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
+            "/swagger-resources/**", "/webjars/**",
+            "/auth/**",           // ← 로그인/회원가입/인증 등은 토큰 없이 접근
+            "/chat/**", "/suggestion/**", "/review/**",
+            "/ws/**", "/pub/**", "/sub/**"
+    };
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; // CORS preflight 우회
+        if (PATH.match("/auth/refresh", uri)) return false;               // 토큰 재발급은 필터 적용
+        for (String p : WHITELIST) if (PATH.match(p, uri)) return true;   // 나머지 공개 경로 우회
+        return false;                                                     // 보호 자원은 필터 적용
+    }
+
+    private static void checkAuthorizationHeader(String header) {
+        log.info("-------------------#@@@@@------------------");
+        if(header == null) {
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+        } else if (!header.startsWith("Bearer ")) {
+            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
+        }
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+
+        final String authHeader = request.getHeader(jwtHeader);
+
+        log.debug("Auth header={}", request.getHeader("Authorization"));
+
+        // Refresh 전용 처리
+        if (PATH.match("/auth/refresh", request.getRequestURI())) {
+            final String refreshToken = request.getHeader("refreshToken");
+            try {
+                // 둘 다 필수
+                checkAuthorizationHeader(authHeader);
+                if (refreshToken == null) throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
+
+                String accessToken = JwtUtil.getTokenFromHeader(authHeader);
+                Claims claims = jwtUtil.validateTokenOnlySignature(accessToken); // 서명만 검증(만료 허용)
+                Authentication authentication = jwtUtil.getAuthenticationFromExpiredAccessToken(accessToken);
+                SecurityContextHolder.getContext().setAuthentication(authentication);
+
+                jwtUtil.validateRefreshToken(refreshToken); // RT는 만료 허용 X
+                chain.doFilter(request, response);
+                return;
+            } catch (Exception e) {
+                // EntryPoint로 넘겨 통일 처리
+                if (e instanceof CustomAuthException ce) {
+                    request.setAttribute("exceptionCode", ce.getCode());
+                    request.setAttribute("exceptionMessage", ce.getMessage());
+                    request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
+                }
+                throw new InsufficientAuthenticationException(e.getMessage(), e);
+            }
+        }
+
+        // 그 외(보호 자원): Authorization 헤더가 없으면 그냥 통과
+        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            String accessToken = JwtUtil.getTokenFromHeader(authHeader);
+            jwtUtil.validateToken(accessToken);
+            jwtUtil.isTokenBlacklisted(accessToken); // accessToken 전달
+
+            Authentication authentication = jwtUtil.getAuthentication(accessToken);
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+
+            chain.doFilter(request, response);
+        } catch (Exception e) {
+            if (e instanceof CustomAuthException ce) {
+                request.setAttribute("exceptionCode", ce.getCode());
+                request.setAttribute("exceptionMessage", ce.getMessage());
+                request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
+            }
+            throw new InsufficientAuthenticationException(e.getMessage(), e);
+        }
+    }
+}
+
diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
index 52b677d..3014b6e 100644
--- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
+++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
@@ -4,10 +4,12 @@
 import com.assu.server.domain.chat.dto.ChatResponseDTO;
 import com.assu.server.domain.chat.service.ChatService;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
+import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
 import org.springframework.messaging.handler.annotation.MessageMapping;
 import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 import com.assu.server.global.apiPayload.BaseResponse;
 import org.springframework.messaging.simp.SimpMessagingTemplate;
@@ -22,25 +24,31 @@ public class ChatController {
     private final SimpMessagingTemplate simpMessagingTemplate;
 
     @Operation(
-            summary = "채팅방 목록 조회 API",
+            summary = "채팅방 목록을 조회하는 API",
             description = "Request Header에 User id를 입력해 주세요."
     )
     @GetMapping("/rooms")
-    public BaseResponse> getChatRoomList() {
-        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList());
+    public BaseResponse> getChatRoomList(
+            @AuthenticationPrincipal PrincipalDetails pd
+            ) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList(memberId));
     }
 
     @Operation(
-            summary = "채팅방 생성 API",
+            summary = "채팅방을 생성하는 API",
             description = "상대방의 id를 request body에 입력해 주세요"
     )
     @PostMapping("/create/rooms")
-    public BaseResponse createChatRoom(@RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request));
+    public BaseResponse createChatRoom(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.createChatRoom(request, memberId));
     }
 
     @Operation(
-            summary = "채팅 API",
+            summary = "채팅 API.",
             description = "roomId, senderId, message를 입력해 주세요"
     )
     @MessageMapping("/send")
@@ -56,8 +64,11 @@ public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request)
     )
     @PatchMapping("rooms/{roomId}/read")
     public BaseResponse readMessage(
-            @PathVariable Long roomId) {
-        ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId);
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @PathVariable Long roomId
+    ) {
+        Long memberId = pd.getMember().getId();
+        ChatResponseDTO.ReadMessageResponseDTO response = chatService.readMessage(roomId, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
@@ -66,20 +77,26 @@ public BaseResponse readMessage(
             description = "roomId를 입력해 주세요."
     )
     @GetMapping("rooms/{roomId}/messages")
-    public BaseResponse getChatHistory(@PathVariable Long roomId) {
-        ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId);
+    public BaseResponse getChatHistory(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @PathVariable Long roomId
+    ) {
+        Long memberId = pd.getMember().getId();
+        ChatResponseDTO.ChatHistoryResponseDTO response = chatService.readHistory(roomId, memberId);
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
     @Operation(
-            summary = "채팅방 나가기 API" +
-                    " (참여자가 2명이면 채팅방이 살아있지만, 이미 한 명이 나갔다면 채팅방이 삭제됩니다.)",
+            summary = "채팅방을 나가는 API" +
+                    "참여자가 2명이면 채팅방이 살아있지만, 이미 한 명이 나갔다면 채팅방이 삭제됩니다.",
             description = "roomId를 입력해 주세요."
     )
     @DeleteMapping("rooms/{roomId}/leave")
     public BaseResponse leaveChattingRoom(
+            @AuthenticationPrincipal PrincipalDetails pd,
             @PathVariable Long roomId
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId));
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId, memberId));
     }
 }
diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
index 90798fc..2123afc 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
@@ -5,7 +5,7 @@
 public class ChatRequestDTO {
     @Getter
     public static class CreateChatRoomRequestDTO {
-        private Long adminId;
+        private Long storeId;
         private Long partnerId;
     }
 
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java
index bc44c56..e11b3c1 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java
@@ -6,10 +6,10 @@
 import java.util.List;
 
 public interface ChatService {
-    List getChatRoomList();
-    ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request);
+    List getChatRoomList(Long memberId);
+    ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId);
     ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request);
-    ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId);
-    ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId);
-    ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId);
+    ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId);
+    ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId);
+    ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index 8b4e1e3..379fec6 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -16,6 +16,8 @@
 import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.store.entity.Store;
+import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
@@ -31,29 +33,33 @@ public class ChatServiceImpl implements ChatService {
     private final PartnerRepository partnerRepository;
     private final AdminRepository adminRepository;
     private final MessageRepository messageRepository;
+    private final StoreRepository storeRepository;
 
 
     @Override
-    public List getChatRoomList() {
-//        Long memberId = SecurityUtil.getCurrentUserId;
-        Long memberId = 1L;
+    public List getChatRoomList(Long memberId) {
 
         List chatRoomList = chatRepository.findChattingRoomsByMemberId(memberId);
         return ChatConverter.toChatRoomListResultDTO(chatRoomList);
     }
 
     @Override
-    public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request) {
-//        Long memberId = SecurityUtil.getCurrentUserId;
-//        Long opponentId = request.getOpponentId();
+    public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId) {
 
-        Long adminId = request.getAdminId();
+        Long storeId = request.getStoreId();
         Long partnerId = request.getPartnerId();
 
-        Admin admin = adminRepository.findById(adminId)
+        Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
         Partner partner = partnerRepository.findById(partnerId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+        Store store = storeRepository.findById(storeId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+
+
+        if (!store.getPartner().getMember().getId().equals(partner.getMember().getId())) {
+            throw new DatabaseException(ErrorStatus.NO_SUCH_STORE_WITH_THAT_PARTNER);
+        }
 
         ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner);
 
@@ -66,9 +72,6 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C
                 admin.getName()
         );
         ChattingRoom savedRoom = chatRepository.save(room);
-
-
-
         return ChatConverter.toCreateChatRoomIdDTO(savedRoom);
     }
 
@@ -90,9 +93,7 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM
 
     @Transactional
     @Override
-    public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) {
-//        Long memberId = SecurityUtil.getCurrentUserId();
-        Long memberId = 2L;
+    public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId) {
 
         List unreadMessages = messageRepository.findUnreadMessagesByRoomAndReceiver(roomId, memberId);
 
@@ -102,24 +103,18 @@ public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId) {
     }
 
     @Override
-    public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId) {
-//        Long memberId = SecurityUtil.getCurrentUserId();
-        Long memberId = 1L;
+    public ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId) {
 
         ChattingRoom room = chatRepository.findById(roomId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM));
 
-        List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(roomId, memberId);
+        List allMessages = messageRepository.findAllMessagesByRoomAndMemberId(room.getId(), memberId);
 
-        return ChatConverter.toChatHistoryDTO(roomId, allMessages);
+        return ChatConverter.toChatHistoryDTO(room.getId(), allMessages);
     }
 
     @Override
-    public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId) {
-//        Long memberId = SecurityUtil.getCurrentUserId();
-
-        Long memberId = 2L;
-
+    public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId) {
         // 멤버 조회
         Member member = memberRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java
index e756de0..717bf26 100644
--- a/src/main/java/com/assu/server/domain/store/entity/Store.java
+++ b/src/main/java/com/assu/server/domain/store/entity/Store.java
@@ -28,7 +28,7 @@ public class Store extends BaseEntity {
 	@GeneratedValue(strategy = GenerationType.IDENTITY)
 	private Long id;
 
-	@ManyToOne(fetch = FetchType.LAZY)
+	@OneToOne(fetch = FetchType.LAZY)
 	@JoinColumn(name = "partner_id")
 	private Partner partner;
 
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index 9e26569..18b4b6e 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -49,10 +49,12 @@ public enum ErrorStatus implements BaseErrorCode {
     NO_SUCH_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4004","존재하지 않는 student ID 입니다."),
     NO_SUCH_STORE(HttpStatus.NOT_FOUND, "STORE_4006", "존재하지 않는 스토어 ID입니다."),
     NO_PAPER_FOR_STORE(HttpStatus.NOT_FOUND, "ADMIN_4005", "존재하지 않는 paper ID입니다."),
-    EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4005","이미 존재하는 전화번호입니다."),
-    EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."),
-    EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."),
     NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "MEMBER_4009", "제휴업체를 찾을 수 없습니다."),
+    NO_SUCH_STORE_WITH_THAT_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4006","해당 store ID에 해당하는 partner ID가 존재하지 않습니다."),
+    EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 전화번호입니다."),
+    EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4008","이미 존재하는 이메일입니다."),
+    EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4009","이미 존재하는 학번입니다."),
+
 
     // 제휴 에러
     NO_SUCH_PAPER(HttpStatus.NOT_FOUND, "PAPER_9001", "제휴를 찾을 수 없습니다."),
diff --git a/src/main/java/com/assu/server/global/util/PrincipalDetails.java b/src/main/java/com/assu/server/global/util/PrincipalDetails.java
index 1e21ab1..dc7582b 100644
--- a/src/main/java/com/assu/server/global/util/PrincipalDetails.java
+++ b/src/main/java/com/assu/server/global/util/PrincipalDetails.java
@@ -13,9 +13,11 @@
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
+import java.util.ArrayList;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.stream.Collectors;
 
 @Getter
 @Builder

From 1428268cad8b4bf323759145f51e1f52762d859d Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Mon, 25 Aug 2025 15:35:17 +0900
Subject: [PATCH 146/270] =?UTF-8?q?refactor/#38=20=20-=20JwtAuthFilter.jav?=
 =?UTF-8?q?a=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/auth/security/JwtAuthFilter.java   | 120 ------------------
 1 file changed, 120 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java

diff --git a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java b/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
deleted file mode 100644
index a33dac9..0000000
--- a/src/main/java/com/assu/server/domain/auth/security/JwtAuthFilter.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.assu.server.domain.auth.security;
-
-import com.assu.server.domain.auth.exception.CustomAuthException;
-import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import io.jsonwebtoken.Claims;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.security.authentication.InsufficientAuthenticationException;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.stereotype.Component;
-import org.springframework.util.AntPathMatcher;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import java.io.IOException;
-
-@RequiredArgsConstructor
-@Component
-@Slf4j
-public class JwtAuthFilter extends OncePerRequestFilter {
-
-    @Value("${jwt.header}")
-    private String jwtHeader;
-    private final JwtUtil jwtUtil;
-    private final RedisTemplate redisTemplate;
-
-    private static final AntPathMatcher PATH = new AntPathMatcher();
-    private static final String[] WHITELIST = {
-            "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
-            "/swagger-resources/**", "/webjars/**",
-            "/auth/**",           // ← 로그인/회원가입/인증 등은 토큰 없이 접근
-            "/chat/**", "/suggestion/**", "/review/**",
-            "/ws/**", "/pub/**", "/sub/**"
-    };
-
-    @Override
-    protected boolean shouldNotFilter(HttpServletRequest request) {
-        String uri = request.getRequestURI();
-        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; // CORS preflight 우회
-        if (PATH.match("/auth/refresh", uri)) return false;               // 토큰 재발급은 필터 적용
-        for (String p : WHITELIST) if (PATH.match(p, uri)) return true;   // 나머지 공개 경로 우회
-        return false;                                                     // 보호 자원은 필터 적용
-    }
-
-    private static void checkAuthorizationHeader(String header) {
-        log.info("-------------------#@@@@@------------------");
-        if(header == null) {
-            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
-        } else if (!header.startsWith("Bearer ")) {
-            throw new CustomAuthException(ErrorStatus.JWT_TOKEN_OUT_OF_FORM);
-        }
-    }
-
-    @Override
-    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
-            throws ServletException, IOException {
-
-        final String authHeader = request.getHeader(jwtHeader);
-
-        log.debug("Auth header={}", request.getHeader("Authorization"));
-
-        // Refresh 전용 처리
-        if (PATH.match("/auth/refresh", request.getRequestURI())) {
-            final String refreshToken = request.getHeader("refreshToken");
-            try {
-                // 둘 다 필수
-                checkAuthorizationHeader(authHeader);
-                if (refreshToken == null) throw new CustomAuthException(ErrorStatus.JWT_TOKEN_NOT_RECEIVED);
-
-                String accessToken = JwtUtil.getTokenFromHeader(authHeader);
-                Claims claims = jwtUtil.validateTokenOnlySignature(accessToken); // 서명만 검증(만료 허용)
-                Authentication authentication = jwtUtil.getAuthenticationFromExpiredAccessToken(accessToken);
-                SecurityContextHolder.getContext().setAuthentication(authentication);
-
-                jwtUtil.validateRefreshToken(refreshToken); // RT는 만료 허용 X
-                chain.doFilter(request, response);
-                return;
-            } catch (Exception e) {
-                // EntryPoint로 넘겨 통일 처리
-                if (e instanceof CustomAuthException ce) {
-                    request.setAttribute("exceptionCode", ce.getCode());
-                    request.setAttribute("exceptionMessage", ce.getMessage());
-                    request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
-                }
-                throw new InsufficientAuthenticationException(e.getMessage(), e);
-            }
-        }
-
-        // 그 외(보호 자원): Authorization 헤더가 없으면 그냥 통과
-        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
-            chain.doFilter(request, response);
-            return;
-        }
-
-        try {
-            String accessToken = JwtUtil.getTokenFromHeader(authHeader);
-            jwtUtil.validateToken(accessToken);
-            jwtUtil.isTokenBlacklisted(accessToken); // accessToken 전달
-
-            Authentication authentication = jwtUtil.getAuthentication(accessToken);
-            SecurityContextHolder.getContext().setAuthentication(authentication);
-
-            chain.doFilter(request, response);
-        } catch (Exception e) {
-            if (e instanceof CustomAuthException ce) {
-                request.setAttribute("exceptionCode", ce.getCode());
-                request.setAttribute("exceptionMessage", ce.getMessage());
-                request.setAttribute("exceptionHttpStatus", ce.getHttpStatus());
-            }
-            throw new InsufficientAuthenticationException(e.getMessage(), e);
-        }
-    }
-}
-

From 6cab7ed2007a1d754889aa1b0f8d85c5da7ba497 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Thu, 4 Sep 2025 23:34:42 +1000
Subject: [PATCH 147/270] =?UTF-8?q?[FIX/#71]=20-=20RabbitMQ=20=EC=82=AC?=
 =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?=
 =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |  4 ++
 .../com/assu/server/ServerApplication.java    |  2 +
 .../dto/NotificationMessageDTO.java           | 18 +++++
 .../entity/NotificationOutbox.java            | 11 ++--
 .../entity/OutboxCreatedEvent.java            | 10 +++
 .../NotificationOutboxRepository.java         | 21 ++++++
 .../NotificationCommandServiceImpl.java       | 24 ++++---
 .../service/NotificationDispatcher.java       | 37 -----------
 .../service/NotificationListener.java         | 65 +++++++++++++++++++
 .../service/NotificationQueryServiceImpl.java |  2 +-
 .../service/OutboxAfterCommitPublisher.java   | 45 +++++++++++++
 .../service/OutboxStatusService.java          | 27 ++++++++
 .../server/infra/firebase/AmqpConfig.java     | 51 +++++++++++++++
 .../assu/server/infra/firebase/FcmClient.java | 62 ++++++++++++------
 14 files changed, 308 insertions(+), 71 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/notification/dto/NotificationMessageDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/notification/entity/OutboxCreatedEvent.java
 delete mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
 create mode 100644 src/main/java/com/assu/server/domain/notification/service/NotificationListener.java
 create mode 100644 src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java
 create mode 100644 src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java
 create mode 100644 src/main/java/com/assu/server/infra/firebase/AmqpConfig.java

diff --git a/build.gradle b/build.gradle
index 79e1a33..cac628a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -99,6 +99,10 @@ dependencies {
 	implementation 'com.fasterxml.jackson.core:jackson-databind'
 	implementation 'com.google.auth:google-auth-library-oauth2-http:1.18.0'
 	implementation 'com.google.firebase:firebase-admin:9.2.0'
+
+	// amqp
+	implementation("org.springframework.boot:spring-boot-starter-amqp")
+	implementation("com.fasterxml.jackson.core:jackson-databind")
 }
 
 tasks.named('test') {
diff --git a/src/main/java/com/assu/server/ServerApplication.java b/src/main/java/com/assu/server/ServerApplication.java
index 27c7b81..9cb6b66 100644
--- a/src/main/java/com/assu/server/ServerApplication.java
+++ b/src/main/java/com/assu/server/ServerApplication.java
@@ -1,5 +1,6 @@
 package com.assu.server;
 
+import org.springframework.amqp.rabbit.annotation.EnableRabbit;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@@ -8,6 +9,7 @@
 @SpringBootApplication
 @EnableJpaAuditing
 @EnableScheduling
+@EnableRabbit
 public class ServerApplication {
 
 	public static void main(String[] args) {
diff --git a/src/main/java/com/assu/server/domain/notification/dto/NotificationMessageDTO.java b/src/main/java/com/assu/server/domain/notification/dto/NotificationMessageDTO.java
new file mode 100644
index 0000000..e8bfd20
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/dto/NotificationMessageDTO.java
@@ -0,0 +1,18 @@
+package com.assu.server.domain.notification.dto;
+
+import lombok.*;
+
+import java.util.Map;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class NotificationMessageDTO {
+    private String idempotencyKey;
+    private Long receiverId;
+    private String title;
+    private String body;
+    private Map data;
+}
diff --git a/src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java b/src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java
index 6f18073..ae0e26b 100644
--- a/src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java
+++ b/src/main/java/com/assu/server/domain/notification/entity/NotificationOutbox.java
@@ -27,8 +27,11 @@ public class NotificationOutbox {
 
     @Column(nullable=false) private int retryCount;
 
-    public enum Status { PENDING, SENT, FAILED }
-    public void markSent(){ this.status = Status.SENT; }
-    public void markFailed(){ this.status = Status.FAILED; }
-    public void incRetry(){ this.retryCount++; }
+    public enum Status { PENDING, SENDING, DISPATCHED, SENT, FAILED }
+
+    public void markSending()    { this.status = Status.SENDING; }
+    public void markDispatched() { this.status = Status.DISPATCHED; }
+    public void markSent()       { this.status = Status.SENT; }
+    public void markFailed()     { this.status = Status.FAILED; }
+    public void incRetry()       { this.retryCount++; }
 }
diff --git a/src/main/java/com/assu/server/domain/notification/entity/OutboxCreatedEvent.java b/src/main/java/com/assu/server/domain/notification/entity/OutboxCreatedEvent.java
new file mode 100644
index 0000000..0a37f3d
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/entity/OutboxCreatedEvent.java
@@ -0,0 +1,10 @@
+package com.assu.server.domain.notification.entity;
+
+import com.assu.server.domain.notification.entity.Notification;
+import lombok.Value;
+
+@Value
+public class OutboxCreatedEvent {
+    Long outboxId;
+    Notification notification;
+}
diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java
index 2ce6a27..9894859 100644
--- a/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java
+++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java
@@ -2,9 +2,30 @@
 
 import com.assu.server.domain.notification.entity.NotificationOutbox;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
 import java.util.List;
 
 public interface NotificationOutboxRepository extends JpaRepository {
+    @Modifying(clearAutomatically = true, flushAutomatically = true)
+    @Query("""
+          update NotificationOutbox o
+             set o.status = com.assu.server.domain.notification.entity.NotificationOutbox.Status.DISPATCHED
+           where o.id = :id
+             and o.status = com.assu.server.domain.notification.entity.NotificationOutbox.Status.PENDING
+        """)
+    int markDispatchedById(@Param("id") Long id);
+
+    @Modifying(clearAutomatically = true, flushAutomatically = true)
+    @Query("""
+ update NotificationOutbox o
+    set o.status = com.assu.server.domain.notification.entity.NotificationOutbox.Status.SENT
+  where o.id = :id
+    and o.status <> com.assu.server.domain.notification.entity.NotificationOutbox.Status.SENT
+""")
+    int markSentById(@Param("id") Long id);
+
     List findTop50ByStatusOrderByIdAsc(NotificationOutbox.Status status);
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index 81a6af1..27b5962 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -4,10 +4,7 @@
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.notification.dto.QueueNotificationRequest;
-import com.assu.server.domain.notification.entity.Notification;
-import com.assu.server.domain.notification.entity.NotificationOutbox;
-import com.assu.server.domain.notification.entity.NotificationSetting;
-import com.assu.server.domain.notification.entity.NotificationType;
+import com.assu.server.domain.notification.entity.*;
 import com.assu.server.domain.notification.repository.NotificationOutboxRepository;
 import com.assu.server.domain.notification.repository.NotificationRepository;
 import com.assu.server.domain.notification.repository.NotificationSettingRepository;
@@ -15,8 +12,9 @@
 import com.assu.server.global.exception.DatabaseException;
 import com.assu.server.global.exception.GeneralException;
 import com.assu.server.infra.firebase.NotificationFactory;
-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;
 import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Service;
 
 import java.util.HashMap;
@@ -31,6 +29,8 @@ public class NotificationCommandServiceImpl implements NotificationCommandServic
     private final NotificationSettingRepository notificationSettingRepository;
     private final NotificationFactory notificationFactory;
     private final MemberRepository memberRepository;
+    private final ApplicationEventPublisher events;
+
 
     @Transactional
     @Override
@@ -45,11 +45,15 @@ public Notification createAndQueue(Long receiverId, NotificationType type, Long
         Notification notification = notificationFactory.create(member, type, refId, ctx);
 
         notificationRepository.save(notification);
-        outboxRepository.save(NotificationOutbox.builder()
-                .notification(notification)
-                .status(NotificationOutbox.Status.PENDING)
-                .retryCount(0)
-                .build());
+        NotificationOutbox outbox = outboxRepository.save(
+                NotificationOutbox.builder()
+                        .notification(notification)
+                        .status(NotificationOutbox.Status.PENDING)
+                        .retryCount(0)
+                        .build()
+        );
+
+        events.publishEvent(new OutboxCreatedEvent(outbox.getId(), notification));
         return notification;
     }
 
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java b/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
deleted file mode 100644
index a76b2ef..0000000
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationDispatcher.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.assu.server.domain.notification.service;
-
-import com.assu.server.domain.notification.entity.Notification;
-import com.assu.server.domain.notification.entity.NotificationOutbox;
-import com.assu.server.domain.notification.repository.NotificationOutboxRepository;
-import com.assu.server.infra.firebase.FcmClient;
-import jakarta.transaction.Transactional;
-import lombok.RequiredArgsConstructor;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-
-@Component
-@RequiredArgsConstructor
-public class NotificationDispatcher {
-    private final NotificationOutboxRepository outboxRepo;
-    private final FcmClient fcmClient;
-
-    @Scheduled(fixedDelay = 1000) // 1초 간격 배치
-    @Transactional
-    public void dispatch() {
-        List batch =
-                outboxRepo.findTop50ByStatusOrderByIdAsc(NotificationOutbox.Status.PENDING);
-
-        for (NotificationOutbox o : batch) {
-            try {
-                Notification notification = o.getNotification();
-                fcmClient.sendToMember(notification.getReceiver(), notification);
-                o.markSent();
-            } catch (Exception e) {
-                o.incRetry();
-                if (o.getRetryCount() >= 5) o.markFailed(); // 과도한 재시도 방지
-            }
-        }
-    }
-}
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java
new file mode 100644
index 0000000..07e1973
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java
@@ -0,0 +1,65 @@
+package com.assu.server.domain.notification.service;
+
+import com.assu.server.infra.firebase.AmqpConfig;
+import com.assu.server.infra.firebase.FcmClient;
+import com.assu.server.domain.notification.dto.NotificationMessageDTO;
+
+import com.rabbitmq.client.Channel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.amqp.support.AmqpHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.stereotype.Component;
+
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class NotificationListener {
+
+    private final FcmClient fcmClient;
+    private final OutboxStatusService outboxStatus; // ← 주입
+
+    @RabbitListener(queues = AmqpConfig.QUEUE)
+    public void onMessage(@Payload NotificationMessageDTO payload,
+                          Channel ch,
+                          @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
+        try {
+            fcmClient.sendToMemberId(
+                    payload.getReceiverId(),
+                    payload.getTitle(),
+                    payload.getBody(),
+                    payload.getData()
+            );
+            ch.basicAck(tag, false);
+
+            // idempotencyKey = outboxId 로 보냈으니 그대로 사용
+            Long outboxId = Long.valueOf(payload.getIdempotencyKey());
+            outboxStatus.markSent(outboxId); // 새 트랜잭션에서 SENT 전이
+        } catch (RuntimeException e) {
+            if (isTransient(e)) {
+                ch.basicNack(tag, false, true);
+            } else {
+                ch.basicNack(tag, false, false);
+            }
+        } catch (Exception e) {
+            ch.basicNack(tag, false, false);
+        }
+    }
+
+    private boolean isTransient(Throwable t) {
+        while (t != null) {
+            if (t instanceof java.util.concurrent.TimeoutException
+                    || t instanceof java.net.SocketTimeoutException
+                    || t instanceof java.io.IOException) {
+                return true;
+            }
+            t = t.getCause();
+        }
+        return false;
+    }
+}
+
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
index f8d9187..0cfa082 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
@@ -7,7 +7,7 @@
 import com.assu.server.domain.notification.repository.NotificationRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
diff --git a/src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java b/src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java
new file mode 100644
index 0000000..c719419
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/service/OutboxAfterCommitPublisher.java
@@ -0,0 +1,45 @@
+package com.assu.server.domain.notification.service;
+
+
+import com.assu.server.domain.notification.dto.NotificationMessageDTO;
+import com.assu.server.domain.notification.entity.OutboxCreatedEvent;
+import com.assu.server.infra.firebase.AmqpConfig;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+import java.util.Map;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OutboxAfterCommitPublisher {
+    private final RabbitTemplate rabbit;
+    private final OutboxStatusService outboxStatus; // ← 여기!
+
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    public void onOutboxCreated(OutboxCreatedEvent e) {
+        var n = e.getNotification();
+
+        var dto = NotificationMessageDTO.builder()
+                .idempotencyKey(String.valueOf(e.getOutboxId()))
+                .receiverId(n.getReceiver().getId())
+                .title(n.getTitle())
+                .body(n.getMessagePreview())
+                .data(Map.of(
+                        "type", n.getType().name(),
+                        "refId", String.valueOf(n.getRefId()),
+                        "deeplink", n.getDeeplink() == null ? "" : n.getDeeplink(),
+                        "notificationId", String.valueOf(n.getId())
+                ))
+                .build();
+
+        rabbit.convertAndSend(AmqpConfig.EXCHANGE, AmqpConfig.ROUTING_KEY, dto);
+
+        // ★ 새 트랜잭션에서 상태 전이
+        outboxStatus.markDispatched(e.getOutboxId());
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java b/src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java
new file mode 100644
index 0000000..1341220
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java
@@ -0,0 +1,27 @@
+package com.assu.server.domain.notification.service;
+
+import com.assu.server.domain.notification.repository.NotificationOutboxRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OutboxStatusService {
+    private final NotificationOutboxRepository repo;
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void markDispatched(Long id) {
+        int updated = repo.markDispatchedById(id);
+        log.info("[OutboxStatus] DISPATCHED updated={} outboxId={}", updated, id);
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void markSent(Long id) {
+        int updated = repo.markSentById(id);
+        log.info("[OutboxStatus] SENT updated={} outboxId={}", updated, id);
+    }
+}
diff --git a/src/main/java/com/assu/server/infra/firebase/AmqpConfig.java b/src/main/java/com/assu/server/infra/firebase/AmqpConfig.java
new file mode 100644
index 0000000..19f37c8
--- /dev/null
+++ b/src/main/java/com/assu/server/infra/firebase/AmqpConfig.java
@@ -0,0 +1,51 @@
+package com.assu.server.infra.firebase;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.amqp.core.*;
+import org.springframework.amqp.rabbit.connection.ConnectionFactory;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
+import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+// AmqpConfig.java (새 파일, 적절한 패키지: com.assu.server.infra.mq 등)
+@Configuration
+public class AmqpConfig {
+    public static final String EXCHANGE = "notif.ex";
+    public static final String ROUTING_KEY = "notif.send";
+    public static final String QUEUE = "notif.send.q";
+    public static final String DLX = "notif.dlx";
+    public static final String DLQ = "notif.send.dlq";
+
+    @Bean DirectExchange exchange() { return new DirectExchange(EXCHANGE, true, false); }
+    @Bean DirectExchange dlx()      { return new DirectExchange(DLX, true, false); }
+
+    @Bean
+    Queue queue() {
+        return QueueBuilder.durable(QUEUE)
+                .withArgument("x-dead-letter-exchange", DLX)
+                .withArgument("x-dead-letter-routing-key", ROUTING_KEY + ".dead")
+                .build();
+    }
+
+    @Bean Queue dlq() { return QueueBuilder.durable(DLQ).build(); }
+
+    @Bean Binding bind()    { return BindingBuilder.bind(queue()).to(exchange()).with(ROUTING_KEY); }
+    @Bean Binding bindDlq() { return BindingBuilder.bind(dlq()).to(dlx()).with(ROUTING_KEY + ".dead"); }
+
+    @Bean
+    RabbitTemplate rabbitTemplate(ConnectionFactory cf) {
+        RabbitTemplate rt = new RabbitTemplate(cf);
+        rt.setMessageConverter(new Jackson2JsonMessageConverter());
+        return rt;
+    }
+
+    @Bean
+    public Jackson2JsonMessageConverter jackson2JsonMessageConverter(ObjectMapper om) {
+        return new Jackson2JsonMessageConverter(om);
+    }
+}
diff --git a/src/main/java/com/assu/server/infra/firebase/FcmClient.java b/src/main/java/com/assu/server/infra/firebase/FcmClient.java
index 6a94865..bf4e8d1 100644
--- a/src/main/java/com/assu/server/infra/firebase/FcmClient.java
+++ b/src/main/java/com/assu/server/infra/firebase/FcmClient.java
@@ -3,47 +3,71 @@
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.notification.entity.Notification;
+import com.google.api.core.ApiFuture;
 import com.google.firebase.messaging.FirebaseMessaging;
 import com.google.firebase.messaging.FirebaseMessagingException;
 import com.google.firebase.messaging.Message;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.time.Duration;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
+@Slf4j
 @Component
 @RequiredArgsConstructor
 public class FcmClient {
     private final FirebaseMessaging messaging;
     private final DeviceTokenRepository tokenRepo;
 
-    public void sendToMember(Member receiver, Notification n) {
-        List tokens = tokenRepo.findActiveTokensByMemberId(receiver.getId());
-        if (tokens.isEmpty()) return; // 토큰 없으면 조용히 스킵
+    // 전송 타임아웃 (필요 시 2~5초로 조정)
+    private static final Duration SEND_TIMEOUT = Duration.ofSeconds(3);
 
+    public void sendToMemberId(Long memberId, String title, String body, Map data) {
+        if (memberId == null) {
+            throw new IllegalArgumentException("receiverId is null");
+        }
+
+        // 1) 토큰 조회
+        List tokens = tokenRepo.findActiveTokensByMemberId(memberId);
+        if (tokens.isEmpty()) return;
+
+        // 2) 데이터 안전하게 추출
+        final String _title = title == null ? "" : title;
+        final String _body  = body  == null ? "" : body;
+
+        String type  = data != null && data.get("type") != null ? data.get("type") : "";
+        String refId = data != null && data.get("refId") != null ? data.get("refId") : "";
+        String deeplink = data != null && data.get("deeplink") != null ? data.get("deeplink") : "";
+        String notificationId = data != null && data.get("notificationId") != null ? data.get("notificationId") : "";
+
+        // 3) 각 토큰에 FCM 전송
         for (String token : tokens) {
             Message msg = Message.builder()
                     .setToken(token)
-                    // notification 채널(시스템 트레이 자동 표시) + data(딥링크/타입/ID)
                     .setNotification(com.google.firebase.messaging.Notification.builder()
-                            .setTitle(n.getTitle())
-                            .setBody(n.getMessagePreview())
+                            .setTitle(_title)
+                            .setBody(_body)
                             .build())
-                    .putData("type", n.getType().name())
-                    .putData("refId", String.valueOf(n.getRefId()))
-                    .putData("deeplink", n.getDeeplink()==null? "" : n.getDeeplink())
-                    .putData("notificationId", String.valueOf(n.getId()))
+                    .putData("type", type)
+                    .putData("refId", refId)
+                    .putData("deeplink", deeplink)
+                    .putData("notificationId", notificationId)
                     .build();
+
             try {
-                messaging.send(msg);
-            } catch (FirebaseMessagingException e) {
-                // 실패 코드에 따라 토큰 비활성화 등 치료
-                String code = e.getMessagingErrorCode() == null ? "" : e.getMessagingErrorCode().name();
-                if ("UNREGISTERED".equals(code) || "INVALID_ARGUMENT".equals(code)) {
-                    tokenRepo.findByToken(token).ifPresent(t -> t.setActive(false));
-                }
-                // 로깅/모니터링
+                ApiFuture future = messaging.sendAsync(msg);
+                future.get(SEND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+            } catch (TimeoutException te) {
+                log.warn("[FCM] timeout ({} ms) memberId={}", SEND_TIMEOUT.toMillis(), memberId);
+                throw new RuntimeException("FCM timeout", te);
+            } catch (Exception e) {
+                throw new RuntimeException("FCM unexpected error", e);
             }
         }
     }
-}
+}
\ No newline at end of file

From a00cdf872491dd74a45458d4d7461c94b640189c Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Thu, 4 Sep 2025 23:47:08 +1000
Subject: [PATCH 148/270] =?UTF-8?q?[FIX/#71]=20-=20yml=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/resources/application.yml      | 15 ++++++++++++++-
 src/test/resources/application-test.yml | 11 +++++++++++
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index c76f75b..b6722ba 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -17,8 +17,21 @@ spring:
           time_zone: Asia/Seoul
         show_sql: true
         highlight_sql : true
+  lifecycle:
+    timeout-per-shutdown-phase: 30s
+  rabbitmq:
+    listener:
+      simple:
+        acknowledge-mode: manual
+        prefetch: 20
+        concurrency: 1
+        max-concurrency: 4
+        default-requeue-rejected: false
 
 logging:
   level:
     org.springframework.web: DEBUG
-    org.springframework.web.client.DefaultRestClient: OFF
\ No newline at end of file
+    org.springframework.web.client.DefaultRestClient: OFF
+
+server:
+  shutdown: graceful
\ No newline at end of file
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 26e940b..9858a9e 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -3,11 +3,16 @@ spring:
     exclude:
       - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
       - org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
+      - org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
   datasource:
     url: jdbc:h2:mem:testdb
     driver-class-name: org.h2.Driver
     username: sa
     password:
+  rabbitmq:
+    listener:
+      simple:
+        auto-startup: false
 
 jwt:
   header: Authorization
@@ -21,6 +26,12 @@ assu:
     school-crypto:
       base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=" #"dummy-base64-key"를 Base64로 인코딩한 값
 
+  messaging:
+    rabbit:
+      enabled: false
+    push:
+      enabled: false
+
 cloud:
   aws:
     s3:

From 70464da79b39cea6975c469cab6c1259ed89d919 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Thu, 4 Sep 2025 22:59:18 +0900
Subject: [PATCH 149/270] =?UTF-8?q?refactor/#38=20-=20chatting=20Controlle?=
 =?UTF-8?q?r=20=EC=88=98=EC=A0=95=20-=20=EA=B7=9C=EC=B9=99=EC=97=90=20?=
 =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20Controller=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../chat/controller/ChatController.java       | 45 +++++++++++--------
 .../server/domain/store/entity/Store.java     | 10 +----
 2 files changed, 28 insertions(+), 27 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
index 3014b6e..d84703d 100644
--- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
+++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
@@ -23,21 +23,11 @@ public class ChatController {
     private final ChatService chatService;
     private final SimpMessagingTemplate simpMessagingTemplate;
 
-    @Operation(
-            summary = "채팅방 목록을 조회하는 API",
-            description = "Request Header에 User id를 입력해 주세요."
-    )
-    @GetMapping("/rooms")
-    public BaseResponse> getChatRoomList(
-            @AuthenticationPrincipal PrincipalDetails pd
-            ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList(memberId));
-    }
-
     @Operation(
             summary = "채팅방을 생성하는 API",
-            description = "상대방의 id를 request body에 입력해 주세요"
+            description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed80c38871ec77deced713) 채팅방을 생성합니다.\n"+
+                    "- storeId: Request Body, Long\n" +
+                    "- partnerId: Request Body, Long\n"
     )
     @PostMapping("/create/rooms")
     public BaseResponse createChatRoom(
@@ -48,8 +38,24 @@ public BaseResponse createChatRoom(
     }
 
     @Operation(
-            summary = "채팅 API.",
-            description = "roomId, senderId, message를 입력해 주세요"
+            summary = "채팅방 목록을 조회하는 API",
+            description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/API-1d71197c19ed819f8f70fb437e9ce62b?p=2241197c19ed816993c3c5ae17d6f099&pm=s) 채팅방 목록을 조회합니다.\n"
+    )
+    @GetMapping("/rooms")
+    public BaseResponse> getChatRoomList(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        Long memberId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, chatService.getChatRoomList(memberId));
+    }
+
+    @Operation(
+            summary = "채팅 API",
+            description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2371197c19ed80968342e2bc8fe88cee&pm=s) 메시지를 전송합니다.\n"+
+                    "- roomId: Request Body, Long\n" +
+                    "- senderId: Request Body, Long\n"+
+                    "- receiverId: Request Body, Long\n" +
+                    "- message: Request Body, String\n"
     )
     @MessageMapping("/send")
     public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) {
@@ -60,7 +66,8 @@ public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request)
 
     @Operation(
             summary = "메시지 읽음 처리 API",
-            description = "roomId를 입력해 주세요."
+            description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2241197c19ed81ffa771cb18ab157b54&pm=s) 메시지를 읽음처리합니다.\n"+
+                    "- roomId: Path Variable, Long\n"
     )
     @PatchMapping("rooms/{roomId}/read")
     public BaseResponse readMessage(
@@ -74,7 +81,8 @@ public BaseResponse readMessage(
 
     @Operation(
             summary = "채팅방 상세 조회 API",
-            description = "roomId를 입력해 주세요."
+            description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2241197c19ed81399395fd66f73730af&pm=s) 채팅방을 클릭했을 때 메시지를 조회합니다.\n"+
+                    "- roomId: Path Variable, Long\n"
     )
     @GetMapping("rooms/{roomId}/messages")
     public BaseResponse getChatHistory(
@@ -89,7 +97,8 @@ public BaseResponse getChatHistory(
     @Operation(
             summary = "채팅방을 나가는 API" +
                     "참여자가 2명이면 채팅방이 살아있지만, 이미 한 명이 나갔다면 채팅방이 삭제됩니다.",
-            description = "roomId를 입력해 주세요."
+            description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2371197c19ed8079a6e1c2331cb4f534&pm=s) 채팅방을 나갑니다.\n"+
+                    "- roomId: Path Variable, Long\n"
     )
     @DeleteMapping("rooms/{roomId}/leave")
     public BaseResponse leaveChattingRoom(
diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java
index 717bf26..7c460ba 100644
--- a/src/main/java/com/assu/server/domain/store/entity/Store.java
+++ b/src/main/java/com/assu/server/domain/store/entity/Store.java
@@ -3,15 +3,7 @@
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partner.entity.Partner;
 
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
+import jakarta.persistence.*;
 import lombok.*;
 import org.hibernate.annotations.JdbcTypeCode;
 import org.hibernate.type.SqlTypes;

From d39d7803f9ab20954806fde0af2849473b652408 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 5 Sep 2025 00:16:52 +1000
Subject: [PATCH 150/270] =?UTF-8?q?[FIX/#71]=20-=20Controller=20=ED=86=B5?=
 =?UTF-8?q?=EC=9D=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/DeviceTokenController.java     | 10 ++++----
 .../inquiry/controller/InquiryController.java | 24 +++++++++++--------
 .../controller/NotificationController.java    | 22 ++++++++++-------
 3 files changed, 33 insertions(+), 23 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
index e7c9644..aed85d2 100644
--- a/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
+++ b/src/main/java/com/assu/server/domain/deviceToken/controller/DeviceTokenController.java
@@ -20,8 +20,9 @@ public class DeviceTokenController {
 
     @Operation(
             summary = "Device Token 등록 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed8092864ac5a1ddc88d07?source=copy_link) device Token을 등록하고 등록된 Token의 ID를 반환합니다.\n"+
-                    "- token: Request Param, String\n"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed8092864ac5a1ddc88d07?source=copy_link)\n" +
+                    "- device Token을 등록하고 등록된 Token의 ID를 반환합니다.\n" +
+                    "  - 'token': Request Param, String\n"
     )
     @PostMapping
     public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
@@ -31,8 +32,9 @@ public BaseResponse register(@AuthenticationPrincipal PrincipalDetails pd,
     }
     @Operation(
             summary = "Device Token 등록 해제 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed80b8b26be9e01d24c929?source=copy_link) 로그아웃/탈퇴 시 호출해 device Token 등록을 해제합니다. 자신의 토큰만 해제가 가능합니다.\n"+
-                    "- token-id: Path Variavle, Long\n"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed80b8b26be9e01d24c929?source=copy_link)\n" +
+                    "- 로그아웃/탈퇴 시 호출해 device Token 등록을 해제합니다. 자신의 토큰만 해제가 가능합니다.\n"+
+                    "  - 'token-id': Path Variavle, Long\n"
     )
     @DeleteMapping("/{token-id}")
     public BaseResponse unregister(@AuthenticationPrincipal PrincipalDetails pd,
diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index 399a08f..00e9adf 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -27,8 +27,9 @@ public class InquiryController {
 
     @Operation(
             summary = "문의 생성 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/2441197c19ed800688f0cfb304dead63?source=copy_link) 문의를 생성하고 해당 문의의 id를 반환합니다.\n"+
-                    "- InquiryCreateRequestDTO: title, content, email\n"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/2441197c19ed800688f0cfb304dead63?source=copy_link)\n" +
+                    "- 문의를 생성하고 해당 문의의 id를 반환합니다.\n"+
+                    "  - InquiryCreateRequestDTO: title, content, email\n"
     )
     @PostMapping
     public BaseResponse create(
@@ -41,10 +42,11 @@ public BaseResponse create(
 
     @Operation(
             summary = "문의 목록을 조회하는 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/2441197c19ed803eba4af9598484e5c5?source=copy_link) 본인의 문의 목록을 상태별로 조회합니다.\n"+
-                    "- status: Request Param, String, [all/waiting/answered]\n" +
-                    "- page: Request Param, Integer, 1 이상\n" +
-                    "- size: Request Param, Integer, default = 20"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/2441197c19ed803eba4af9598484e5c5?source=copy_link)\n" +
+                    "- 본인의 문의 목록을 상태별로 조회합니다.\n"+
+                    "  - status: Request Param, String, [all/waiting/answered]\n" +
+                    "  - page: Request Param, Integer, 1 이상\n" +
+                    "  - size: Request Param, Integer, default = 20"
     )
     @GetMapping
     public BaseResponse> list(
@@ -60,8 +62,9 @@ public BaseResponse> list(
     /** 단건 상세 조회 */
     @Operation(
             summary = "문의 단건 상세 조회 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed800f8a1fffc5a101f3c0?source=copy_link) 본인의 단건 문의를 상세 조회합니다.\n"+
-                    "- inquiry-id: Path Variable, Long\n"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed800f8a1fffc5a101f3c0?source=copy_link)\n" +
+                    "- 본인의 단건 문의를 상세 조회합니다.\n"+
+                    "  - inquiry-id: Path Variable, Long\n"
     )
     @GetMapping("/{inquiry-id}")
     public BaseResponse get(
@@ -75,8 +78,9 @@ public BaseResponse get(
     /** 문의 답변 (운영자) */
     @Operation(
             summary = "운영자 답변 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed8064808fcca568b8912a?source=copy_link) 문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다.\n"+
-                    "- inquiry-id: Path Variable, Long\n"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/24e1197c19ed8064808fcca568b8912a?source=copy_link)\n" +
+                    "- 문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다.\n"+
+                    "  - inquiry-id: Path Variable, Long\n"
     )
     @PatchMapping("/{inquiry-id}/answer")
     public BaseResponse answer(
diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 6d79447..9077fb2 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -31,10 +31,11 @@ public class NotificationController {
 
     @Operation(
             summary = "알림 목록 조회 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/2491197c19ed8091b349ef0ef4bb0f60?source=copy_link) 본인의 알림 목록을 상태별로 조회합니다.\n"+
-                    "- status: Request Param, String, [all/unread]\n" +
-                    "- page: Request Param, Integer, 1 이상\n" +
-                    "- size: Request Param, Integer, default = 20"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/2491197c19ed8091b349ef0ef4bb0f60?source=copy_link)\n" +
+                    "- 본인의 알림 목록을 상태별로 조회합니다.\n"+
+                    "  - status: Request Param, String, [all/unread]\n" +
+                    "  - page: Request Param, Integer, 1 이상\n" +
+                    "  - size: Request Param, Integer, default = 20"
     )
     @GetMapping
     public BaseResponse> list(
@@ -49,8 +50,9 @@ public BaseResponse> list(
 
     @Operation(
             summary = "알림 읽음 처리 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/2491197c19ed80a89ff0c03bc150460f?source=copy_link) 알림 아이디에 해당하는 알림을 읽음 처리합니다.\n"+
-                    "- notification-id: Path Variable, Long\n"
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/2491197c19ed80a89ff0c03bc150460f?source=copy_link) \n" +
+                    "- 알림 아이디에 해당하는 알림을 읽음 처리합니다.\n"+
+                    "  - notification-id: Path Variable, Long\n"
     )
     @PostMapping("/{notification-id}/read")
     public BaseResponse markRead(@AuthenticationPrincipal PrincipalDetails pd,
@@ -62,7 +64,8 @@ public BaseResponse markRead(@AuthenticationPrincipal PrincipalDetails p
 
     @Operation(
             summary = "알림 전송 테스트 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/2511197c19ed8051bc93d95f0b216543?source=copy_link) deviceToken을 등록한 이후에 사용 가능합니다."
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/2511197c19ed8051bc93d95f0b216543?source=copy_link)\n" +
+                    "- deviceToken을 등록한 이후에 사용 가능합니다."
     )
     @PostMapping("/queue")
     public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest req) {
@@ -71,8 +74,9 @@ public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest r
     }
 
     @Operation(summary = "알림 유형별 ON/OFF 토글 API",
-            description = "[v1.0 (2025-09-02)](https://www.notion.so/on-off-2511197c19ed80aeb4eed3c502691361?source=copy_link) 토글 형식으로 유형별 알림을 ON/OFF 합니다.\n"+
-                    "- type: Path Variable, NotificationType [CHAT / PARTNER_SUGGESTION / PARTNER_PROPOSAL / ORDER]\n")
+            description = "# [v1.0 (2025-09-02)](https://www.notion.so/on-off-2511197c19ed80aeb4eed3c502691361?source=copy_link)\n" +
+                    "- 토글 형식으로 유형별 알림을 ON/OFF 합니다.\n"+
+                    "  - type: Path Variable, NotificationType [CHAT / PARTNER_SUGGESTION / PARTNER_PROPOSAL / ORDER]\n")
     @PutMapping("/{type}")
     public BaseResponse toggle(@AuthenticationPrincipal PrincipalDetails pd,
                                        @PathVariable("type") NotificationType type) {

From d22a186018df341776d0039b5498ead0d4926d05 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Sat, 6 Sep 2025 17:40:20 +0900
Subject: [PATCH 151/270] =?UTF-8?q?[FEAT/#52]=20=EC=82=AC=EC=9A=A9?=
 =?UTF-8?q?=EC=8B=9C=EA=B0=81=20String=EC=9C=BC=EB=A1=9C=20=EB=82=B4?=
 =?UTF-8?q?=EB=A0=A4=EC=A3=BC=EA=B8=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/user/dto/StudentResponseDTO.java | 2 +-
 .../assu/server/domain/user/service/StudentServiceImpl.java | 6 +++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
index bda0e37..92231a0 100644
--- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java
@@ -37,7 +37,7 @@ public static class UsageDetailDTO {
 		private String storeName;
 		private Long partnerId;
 		private Long storeId;
-		private LocalDate usedAt;
+		private String usedAt;
 		private String benefitDescription;
 		private boolean isReviewed;
 	}
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
index fff02cf..30232f3 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
@@ -1,5 +1,7 @@
 package com.assu.server.domain.user.service;
 
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
 
 import com.assu.server.domain.partnership.entity.PaperContent;
@@ -49,12 +51,14 @@ public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int yea
 
 					// 2. PaperContent에서 storeId를 가져옵니다.
 					Store store = (paperContent != null) ? paperContent.getPaper().getStore() : null;
+					LocalDateTime ld= u.getCreatedAt();
+					String formatDate =ld.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
 
 					return StudentResponseDTO.UsageDetailDTO.builder()
 						.partnershipUsageId(u.getId())
 						.adminName(u.getAdminName())
 						.storeName(u.getPlace())
-						.usedAt(u.getDate())
+						.usedAt(formatDate)
 						.benefitDescription(u.getPartnershipContent())
 						.isReviewed(u.getIsReviewed())
 						.storeId(store.getId()) // 3. storeId를 DTO에 매핑합니다.

From 68cf793b09b72ee6d65906da32b3208723adf2cb Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Sun, 7 Sep 2025 21:59:58 +0900
Subject: [PATCH 152/270] =?UTF-8?q?refactor/#38=20-=20chatting=20Controlle?=
 =?UTF-8?q?r=20=EC=88=98=EC=A0=95=20-=20=EA=B7=9C=EC=B9=99=EC=97=90=20?=
 =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20Controller=20=EC=88=98=EC=A0=95=20-=20?=
 =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=97=B0=EA=B2=B0=20=EC=A4=91=20?=
 =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EB=82=B4=EC=9A=A9=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assu/server/domain/chat/controller/ChatController.java | 2 +-
 .../assu/server/domain/chat/converter/ChatConverter.java   | 7 ++++++-
 .../com/assu/server/domain/chat/dto/ChatMessageDTO.java    | 4 ++++
 .../com/assu/server/domain/chat/dto/ChatResponseDTO.java   | 2 ++
 4 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
index d84703d..d09bcff 100644
--- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
+++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
@@ -29,7 +29,7 @@ public class ChatController {
                     "- storeId: Request Body, Long\n" +
                     "- partnerId: Request Body, Long\n"
     )
-    @PostMapping("/create/rooms")
+    @PostMapping("/rooms")
     public BaseResponse createChatRoom(
             @AuthenticationPrincipal PrincipalDetails pd,
             @RequestBody ChatRequestDTO.CreateChatRoomRequestDTO request) {
diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
index a84621f..0f35123 100644
--- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
+++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
@@ -44,7 +44,12 @@ public static ChattingRoom toCreateChattingRoom(Admin admin, Partner partner) {
     }
 
     public static ChatResponseDTO.CreateChatRoomResponseDTO toCreateChatRoomIdDTO(ChattingRoom room) {
-        return new ChatResponseDTO.CreateChatRoomResponseDTO(room.getId());
+        return ChatResponseDTO.CreateChatRoomResponseDTO.builder()
+                .roomId(room.getId())
+                .adminViewName(room.getPartner().getName())
+                .partnerViewName(room.getAdmin().getName())
+                .build();
+
     }
 
     public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender, Member receiver) {
diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java
index 628af1d..ad20a6f 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.chat.dto;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
@@ -21,6 +22,9 @@ public class ChatMessageDTO {
     private String message;
     private LocalDateTime sendTime;
 
+    @JsonProperty("isRead")
     private boolean isRead;
+
+    @JsonProperty("isMyMessage")
     private boolean isMyMessage;
 }
diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
index 8c4790a..1d5ac80 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
@@ -16,6 +16,8 @@ public class ChatResponseDTO {
     @Builder
     public static class CreateChatRoomResponseDTO {
         private Long roomId;
+        private String adminViewName;
+        private String partnerViewName;
     }
 
     // 메시지 전송

From fae5522e269d826929fe195793f5746ab959fc7c Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 7 Sep 2025 23:40:21 +1000
Subject: [PATCH 153/270] =?UTF-8?q?[FEAT/#71]=20-=20=EC=95=8C=EB=A6=BC=20?=
 =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=B6=84?=
 =?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/NotificationCommandService.java   |   5 +
 .../NotificationCommandServiceImpl.java       | 152 ++++++++++++------
 2 files changed, 109 insertions(+), 48 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
index 936cbb1..c02e0c1 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
@@ -13,4 +13,9 @@ public interface NotificationCommandService {
     void queue(QueueNotificationRequest req);
     boolean toggle(Long memberId, NotificationType type);
     boolean isEnabled(Long memberId, NotificationType type);
+
+    void sendChat(Long receiverId, Long roomId, String senderName, String message);
+    void sendPartnerSuggestion(Long receiverId, Long suggestionId);
+    void sendOrder(Long receiverId, Long orderId, String tableNum, String paperContent);
+    void sendPartnerProposal(Long receiverId, Long proposalId, String partnerName);
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index 27b5962..5b8557d 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -72,80 +72,59 @@ public void markRead(Long notificationId, Long currentMemberId) {
     @Transactional
     @Override
     public void queue(QueueNotificationRequest req) {
-        NotificationType type;
+        if (req.getType() == null) {
+            throw new DatabaseException(ErrorStatus.INVALID_NOTIFICATION_TYPE);
+        }
+        if (req.getReceiverId() == null) {
+            throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+        }
+
+        final NotificationType type;
         try {
             type = NotificationType.valueOf(req.getType().toUpperCase(Locale.ROOT));
         } catch (IllegalArgumentException e) {
             throw new DatabaseException(ErrorStatus.INVALID_NOTIFICATION_TYPE);
         }
 
-        Map ctx = new HashMap<>();
-        if (req.getContent()  != null) ctx.put("content",  req.getContent());
-        if (req.getTitle()    != null) ctx.put("title",    req.getTitle());
-        if (req.getDeeplink() != null) ctx.put("deeplink", req.getDeeplink());
-
-        Long refId = req.getRefId();
+        final Long receiverId = req.getReceiverId();
 
         switch (type) {
             case CHAT -> {
-                if (refId == null && req.getRoomId() == null) {
+                // refId 우선순위: refId > roomId
+                Long roomId = (req.getRefId() != null) ? req.getRefId() : req.getRoomId();
+                if (roomId == null || req.getSenderName() == null || req.getMessage() == null) {
                     throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                refId = (refId != null) ? refId : req.getRoomId();
-                if (req.getSenderName() == null || req.getMessage() == null) {
-                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
-                }
-                ctx.put("senderName", req.getSenderName());
-                ctx.put("message", req.getMessage());
+                // 퍼사드 호출: 내부에서 ON/OFF 자동 반영
+                sendChat(receiverId, roomId, req.getSenderName(), req.getMessage());
             }
+
             case PARTNER_SUGGESTION -> {
-                if (refId == null && req.getSuggestionId() == null) {
+                Long suggestionId = (req.getRefId() != null) ? req.getRefId() : req.getSuggestionId();
+                if (suggestionId == null) {
                     throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                refId = (refId != null) ? refId : req.getSuggestionId();
+                sendPartnerSuggestion(receiverId, suggestionId);
             }
+
             case ORDER -> {
-                if (refId == null && req.getOrderId() == null) {
+                Long orderId = (req.getRefId() != null) ? req.getRefId() : req.getOrderId();
+                if (orderId == null || req.getTable_num() == null || req.getPaper_content() == null) {
                     throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                refId = (refId != null) ? refId : req.getOrderId();
-                if (req.getTable_num() == null || req.getPaper_content() == null) {
-                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
-                }
-                ctx.put("table_num", req.getTable_num());
-                ctx.put("paper_content", req.getPaper_content());
+                sendOrder(receiverId, orderId, req.getTable_num(), req.getPaper_content());
             }
+
             case PARTNER_PROPOSAL -> {
-                if (refId == null && req.getProposalId() == null) {
-                    throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
-                }
-                refId = (refId != null) ? refId : req.getProposalId();
-                if (req.getPartner_name() == null) {
+                Long proposalId = (req.getRefId() != null) ? req.getRefId() : req.getProposalId();
+                if (proposalId == null || req.getPartner_name() == null) {
                     throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
                 }
-                ctx.put("partner_name", req.getPartner_name());
+                sendPartnerProposal(receiverId, proposalId, req.getPartner_name());
             }
-            default -> throw new DatabaseException(ErrorStatus.INVALID_NOTIFICATION_TYPE);
-        }
-
-        if (req.getReceiverId() == null) {
-            throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
-        }
-
-        // OFF면 Outbox 적재 없이 Notification만 저장하고 종료
-        boolean enabled = isEnabled(req.getReceiverId(), type);
 
-        if (!enabled) {
-            // 기록만 남기고 발송은 스킵
-            var member = memberRepository.findMemberById(req.getReceiverId()).orElseThrow(
-                () -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)
-            );
-            var notification = notificationFactory.create(member, type, refId, ctx);
-            notificationRepository.save(notification);
-            return;
+            default -> throw new DatabaseException(ErrorStatus.INVALID_NOTIFICATION_TYPE);
         }
-
-        createAndQueue(req.getReceiverId(), type, refId, ctx);
     }
 
     @Transactional
@@ -176,4 +155,81 @@ public boolean isEnabled(Long memberId, NotificationType type) {
                 .map(ns -> Boolean.TRUE.equals(ns.getEnabled())) // null → false 처리
                 .orElse(true); // 설정 없으면 기본 허용
     }
+
+
+    @Transactional
+    protected void sendIfEnabled(Long receiverId, NotificationType type, Long refId, Map ctx) {
+        // OFF면 기록만 남기고 종료, ON이면 Outbox 적재
+        if (!isEnabled(receiverId, type)) {
+            Member member = memberRepository.findMemberById(receiverId)
+                    .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER));
+            notificationRepository.save(notificationFactory.create(member, type, refId, ctx));
+            return;
+        }
+        createAndQueue(receiverId, type, refId, ctx);
+    }
+
+    @Transactional
+    @Override
+    public void sendChat(Long receiverId, Long roomId, String senderName, String message) {
+        if (receiverId == null || roomId == null || senderName == null || message == null) {
+            throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+        }
+        sendIfEnabled(
+                receiverId,
+                NotificationType.CHAT,
+                roomId, // Factory가 /chat/rooms/{refId}로 딥링크 생성
+                Map.of(
+                        "senderName", senderName,     // Factory가 title/preview 생성에 사용
+                        "message", message            // Factory가 미리보기 생성에 사용
+                )
+        );
+    }
+
+    @Transactional
+    @Override
+    public void sendPartnerSuggestion(Long receiverId, Long suggestionId) {
+        if (receiverId == null || suggestionId == null) {
+            throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+        }
+        sendIfEnabled(
+                receiverId,
+                NotificationType.PARTNER_SUGGESTION,
+                suggestionId,                    // /partner/suggestions/{refId}
+                Map.of()                         // 추가 ctx 없음
+        );
+    }
+
+    @Transactional
+    @Override
+    public void sendOrder(Long receiverId, Long orderId, String tableNum, String paperContent) {
+        if (receiverId == null || orderId == null || tableNum == null || paperContent == null) {
+            throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+        }
+        sendIfEnabled(
+                receiverId,
+                NotificationType.ORDER,
+                orderId,                         // /orders/{refId}
+                Map.of(
+                        "table_num", tableNum,       // Factory preview: "{table_num}번 테이블..."
+                        "paper_content", paperContent
+                )
+        );
+    }
+
+    @Transactional
+    @Override
+    public void sendPartnerProposal(Long receiverId, Long proposalId, String partnerName) {
+        if (receiverId == null || proposalId == null || partnerName == null) {
+            throw new DatabaseException(ErrorStatus.MISSING_NOTIFICATION_FIELD);
+        }
+        sendIfEnabled(
+                receiverId,
+                NotificationType.PARTNER_PROPOSAL,
+                proposalId,                      // /partner/proposals/{refId}
+                Map.of(
+                        "partner_name", partnerName  // Factory preview: "{partner_name}에서..."
+                )
+        );
+    }
 }

From ca8cc3edfacaccc67b5579c566521c6d29f1f157 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Sun, 7 Sep 2025 23:45:48 +1000
Subject: [PATCH 154/270] [FEAT/#71] - test.yml update

---
 src/test/resources/application-test.yml | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 9858a9e..8a3197b 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -54,4 +54,10 @@ kakao:
 aligo:
   key: dummy-aligo-key
   user-id: dummy-user-id
-  sender: 01012345678
\ No newline at end of file
+  sender: 01012345678
+
+rabbitmq:
+  host: dummy-host
+  port: 1234
+  username: rabbit-username
+  password: rabbit-password

From da4eda0763d703bc38f4000feb1ebc0bba46d6d7 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Mon, 8 Sep 2025 00:27:48 +1000
Subject: [PATCH 155/270] =?UTF-8?q?[FEAT/#71]=20-=20=ED=85=8C=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=20=EB=B0=98=EC=98=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assu/server/ServerApplicationTests.java   | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/test/java/com/assu/server/ServerApplicationTests.java b/src/test/java/com/assu/server/ServerApplicationTests.java
index 5faba62..64bd3ef 100644
--- a/src/test/java/com/assu/server/ServerApplicationTests.java
+++ b/src/test/java/com/assu/server/ServerApplicationTests.java
@@ -5,6 +5,8 @@
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.context.TestConfiguration;
 import org.springframework.context.annotation.Bean;
@@ -12,6 +14,9 @@
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.amqp.rabbit.connection.ConnectionFactory;
+
 
 @SpringBootTest
 @ActiveProfiles("test")
@@ -20,6 +25,11 @@ class ServerApplicationTests {
 	@Mock
 	private FirebaseMessaging firebaseMessaging;
 
+	@MockitoBean private ConnectionFactory connectionFactory;
+
+	@MockitoBean private RabbitTemplate rabbitTemplate;
+
+
 	@TestConfiguration
 	static class MockConfig {
 		@Bean
@@ -47,6 +57,16 @@ StringRedisTemplate stringRedisTemplate() {
 		JwtUtil jwtUtil() {
 			return Mockito.mock(JwtUtil.class);
 		}
+
+		@Bean(name = "rabbitListenerContainerFactory")
+		RabbitListenerContainerFactory rabbitListenerContainerFactory() {
+			var factory = Mockito.mock(RabbitListenerContainerFactory.class);
+			var container = Mockito.mock(org.springframework.amqp.rabbit.listener.MessageListenerContainer.class);
+			Mockito.when(factory.createListenerContainer(Mockito.any()))
+					.thenReturn(container);
+			return factory;
+		}
+
 	}
 
 	@Test

From 8d762d0df52b73238709c6f91851cc40228ce476 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 9 Sep 2025 02:39:50 +0900
Subject: [PATCH 156/270] =?UTF-8?q?[REFACTOR/#46]=20=ED=95=99=EC=83=9D=20?=
 =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90?=
 =?UTF-8?q?=EA=B0=80=EC=9E=85=20SSO=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C?=
 =?UTF-8?q?=20=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 기존 비밀번호 기반 인증을 숭실대 SSO 토큰 기반으로 변경
- StudentLoginRequest, StudentSignUpRequest 등 기존 DTO 제거
- StudentTokenSignUpRequest, StudentTokenAuthPayload 등 SSO 기반 DTO 추가
- 암호화 관련 클래스들 제거 (AesGcmSchoolCredentialEncryptor, SchoolCredentialEncryptor, StudentPasswordEncoder)
- SSUAuthServiceImpl에서 유세인트 인증 로직 구현
- LoginService, SignUpService에서 SSO 기반 인증 플로우로 변경
- SSUAuth 엔티티 및 어댑터 수정
- JWT 토큰 발급 로직 SSO 기반으로 수정
---
 .../auth/controller/AuthController.java       | 89 +++++++++--------
 .../AesGcmSchoolCredentialEncryptor.java      | 58 -----------
 .../crypto/SchoolCredentialEncryptor.java     |  6 --
 .../auth/crypto/StudentPasswordEncoder.java   | 35 -------
 .../auth/dto/login/StudentLoginRequest.java   | 33 -------
 .../auth/dto/signup/StudentSignUpRequest.java | 25 -----
 .../dto/signup/StudentTokenSignUpRequest.java | 22 +++++
 .../signup/student/StudentAuthPayload.java    | 23 -----
 .../signup/student/StudentInfoPayload.java    | 36 -------
 .../student/StudentTokenAuthPayload.java      | 27 ++++++
 .../auth/dto/ssu/USaintAuthRequest.java       | 11 ++-
 .../auth/dto/ssu/USaintAuthResponse.java      |  2 +-
 .../server/domain/auth/entity/SSUAuth.java    |  5 -
 .../auth/security/adapter/SSUAuthAdapter.java | 37 +++++--
 .../domain/auth/security/jwt/JwtUtil.java     |  3 +-
 .../domain/auth/service/LoginService.java     |  4 +-
 .../domain/auth/service/LoginServiceImpl.java | 82 ++++++++++++----
 .../auth/service/SSUAuthServiceImpl.java      |  6 +-
 .../domain/auth/service/SignUpService.java    |  4 +-
 .../auth/service/SignUpServiceImpl.java       | 97 +++++++++++--------
 .../server/domain/user/entity/Student.java    | 23 ++++-
 .../server/global/config/ProjectConfig.java   | 15 ---
 src/main/resources/application.yml            |  2 +-
 src/test/resources/application-test.yml       |  5 -
 24 files changed, 280 insertions(+), 370 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequest.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java

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 896e432..385731e 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
@@ -3,16 +3,16 @@
 import com.assu.server.domain.auth.dto.login.CommonLoginRequest;
 import com.assu.server.domain.auth.dto.login.LoginResponse;
 import com.assu.server.domain.auth.dto.login.RefreshResponse;
-import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
 import com.assu.server.domain.auth.dto.phone.PhoneAuthRequestDTO;
 import com.assu.server.domain.auth.dto.signup.AdminSignUpRequest;
 import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequest;
 import com.assu.server.domain.auth.dto.signup.SignUpResponse;
-import com.assu.server.domain.auth.dto.signup.StudentSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.StudentTokenSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload;
 import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest;
 import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse;
 import com.assu.server.domain.auth.service.*;
-import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.domain.user.entity.enums.University;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import io.swagger.v3.oas.annotations.Operation;
@@ -22,6 +22,7 @@
 import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.RequestBody;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.MediaType;
@@ -82,27 +83,39 @@ public BaseResponse checkAuthNumber(
 
     @Operation(
             summary = "학생 회원가입 API",
-            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971?source=copy_link)\n" +
+            description = "# [v1.1 (2025-09-07)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971)\n" +
                     "- `application/json` 요청 바디를 사용합니다.\n" +
-                    "- 처리: users + ssu_auth 등 가입 레코드 생성, 휴대폰 인증 여부 확인.\n" +
-                    "- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+                    "- 처리: 유세인트 인증 → 학생 정보 추출 → 회원가입 완료\n" +
+                    "- 성공 시 201(Created)과 생성된 memberId 및 JWT 토큰 반환.\n" +
                     "\n**Request Body:**\n" +
-                    "  - `StudentSignUpRequest` 객체 (JSON, required): 학생 가입 정보\n" +
-                    "  - `email` (String, required): 이메일 주소\n" +
-                    "  - `password` (String, required): 비밀번호\n" +
+                    "  - `StudentTokenSignUpRequest` 객체 (JSON, required): 숭실대 학생 토큰 가입 정보\n" +
                     "  - `phoneNumber` (String, required): 휴대폰 번호\n" +
-                    "  - `studentNumber` (String, required): 학번\n" +
-                    "  - `name` (String, required): 학생 이름\n" +
-                    "  - `major` (Major enum, required): 전공\n" +
-                    "  - `grade` (Integer, required): 학년\n" +
-                    "  - `semester` (Integer, required): 학기\n" +
+                    "  - `marketingAgree` (Boolean, required): 마케팅 수신 동의\n" +
+                    "  - `locationAgree` (Boolean, required): 위치 정보 수집 동의\n" +
+                    "  - `StudentTokenAuthPayload` (Object, required): 유세인트 토큰 정보\n" +
+                    "    - `sToken` (String, required): 유세인트 sToken\n" +
+                    "    - `sIdno` (Integer, required): 유세인트 sIdno\n" +
+                    "    - `university` (University enum, required): 대학 이름 (SSU)\n" +
                     "\n**Response:**\n" +
-                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환"
+                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환\n" +
+                    "  - `memberId` (Long): 회원 ID\n" +
+                    "  - `tokens` (Object): JWT 토큰 정보"
+    )
+    @io.swagger.v3.oas.annotations.parameters.RequestBody(
+            required = true,
+            content = @Content(schema = @Schema(implementation = StudentTokenSignUpRequest.class))
     )
     @PostMapping(value = "/students/signup", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse signupStudent(
-            @Valid @RequestBody StudentSignUpRequest request) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupStudent(request));
+            @Valid @RequestBody StudentTokenSignUpRequest request
+    ) {
+        SignUpResponse response;
+        if(request.getStudentTokenAuth().getUniversity().equals(University.SSU)){
+            response = signUpService.signupSsuStudent(request);
+        } else {
+            response = null;
+        }
+        return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
     @Operation(
@@ -188,8 +201,6 @@ public BaseResponse signupAdmin(
         return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupAdmin(request, signImage));
     }
 
-
-    // 로그인 (파트너/관리자 공통)
     @Operation(
             summary = "공통 로그인 API",
             description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50?source=copy_link)\n" +
@@ -218,39 +229,37 @@ public BaseResponse loginCommon(
         return BaseResponse.onSuccess(SuccessStatus._OK, loginService.loginCommon(request));
     }
 
-
-    // 학생 로그인
-    @Operation(
-            summary = "학생 로그인 API",
-            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8?source=copy_link)\n" +
+    @Operation(summary = "학생 로그인 API",
+            description = "# [v1.1 (2025-09-07)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8?source=copy_link)\n" +
                     "- `application/json`로 호출합니다.\n" +
-                    "- 바디: `바디: `StudentLoginRequest(studentNumber, studentPassword, school)`.\n" +
-                    "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
+                    "- 바디: `StudentTokenLoginRequest(sToken, sIdno, university)`.\n" +
+                    "- 처리: 유세인트 인증 → 기존 회원 확인 → JWT 토큰 발급.\n" +
                     "- 성공 시 200(OK)과 토큰/만료시각 반환.\n" +
                     "\n**Request Body:**\n" +
-                    "  - `StudentLoginRequest` 객체 (JSON, required): 학생 로그인 정보\n" +
-                    "  - `studentNumber` (String, required): 학번\n" +
-                    "  - `studentPassword` (String, required): 학생 포털 비밀번호\n" +
-                    "  - `school` (String, required): 학교명\n" +
-                    "\n**Response:**\n" +
+                    "  - `StudentTokenAuthPayload` 객체 (JSON, required): 숭실대 학생 토큰 로그인 정보\n" +
+                    "  - `sToken` (String, required): 유세인트 sToken\n" +
+                    "  - `sIdno` (Integer, required): 유세인트 sIdno\n" +
+                    "  - `university` (University enum, required): 대학 이름 (SSU)\n" +
+                   "\n**Response:**\n" +
                     "  - 성공 시 200(OK)과 `LoginResponse` 객체 반환\n" +
                     "  - `accessToken` (String): 액세스 토큰\n" +
                     "  - `refreshToken` (String): 리프레시 토큰\n" +
                     "  - `expiresAt` (LocalDateTime): 토큰 만료 시각"
     )
-    @io.swagger.v3.oas.annotations.parameters.RequestBody(
-            required = true,
-            content = @Content(schema = @Schema(implementation = StudentLoginRequest.class))
-    )
+    @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(schema = @Schema(implementation = StudentTokenAuthPayload.class)))
     @PostMapping(value = "/students/login", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse loginStudent(
-            @RequestBody @Valid StudentLoginRequest request
+            @RequestBody @Valid StudentTokenAuthPayload request
     ) {
-        return BaseResponse.onSuccess(SuccessStatus._OK, loginService.loginStudent(request));
+        LoginResponse response;
+        if(request.getUniversity().equals(University.SSU)){
+            response = loginService.loginSsuStudent(request);
+        } else {
+            response = null;
+        }
+        return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
-
-    // 액세스 토큰 갱신
     @Operation(
             summary = "Access Token 갱신 API",
             description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed806ea8cff29f9cd8695a?source=copy_link)\n" +
@@ -281,8 +290,6 @@ public BaseResponse refreshToken(
         return BaseResponse.onSuccess(SuccessStatus._OK, loginService.refresh(refreshToken));
     }
 
-
-    // 로그아웃
     @Operation(
             summary = "로그아웃 API",
             description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/23a1197c19ed809e9a09fcd741f554c8?source=copy_link)\n" +
diff --git a/src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java b/src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java
deleted file mode 100644
index f75ea2a..0000000
--- a/src/main/java/com/assu/server/domain/auth/crypto/AesGcmSchoolCredentialEncryptor.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.assu.server.domain.auth.crypto;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Base64;
-
-public class AesGcmSchoolCredentialEncryptor implements SchoolCredentialEncryptor {
-
-    private static final String TRANSFORMATION = "AES/GCM/NoPadding";
-    private static final int GCM_TAG_BITS = 128;   // 16 bytes tag
-    private static final int IV_BYTES = 12;        // 96-bit IV (권장)
-    private final SecretKey key;
-    private final SecureRandom random = new SecureRandom();
-
-    public AesGcmSchoolCredentialEncryptor(byte[] keyBytes) {
-        this.key = new SecretKeySpec(keyBytes, "AES");
-    }
-
-    @Override
-    public String encrypt(String plain) {
-        try {
-            byte[] iv = new byte[IV_BYTES];
-            random.nextBytes(iv);
-
-            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
-            cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
-            byte[] ct = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
-
-            byte[] out = new byte[iv.length + ct.length];
-            System.arraycopy(iv, 0, out, 0, iv.length);
-            System.arraycopy(ct, 0, out, iv.length, ct.length);
-            return Base64.getEncoder().encodeToString(out);
-        } catch (Exception e) {
-            throw new IllegalStateException("Failed to encrypt school credential", e);
-        }
-    }
-
-    @Override
-    public String decrypt(String cipherB64) {
-        try {
-            byte[] all = Base64.getDecoder().decode(cipherB64);
-            byte[] iv = Arrays.copyOfRange(all, 0, IV_BYTES);
-            byte[] ct = Arrays.copyOfRange(all, IV_BYTES, all.length);
-
-            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
-            cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
-            byte[] pt = cipher.doFinal(ct);
-            return new String(pt, StandardCharsets.UTF_8);
-        } catch (Exception e) {
-            throw new IllegalStateException("Failed to decrypt school credential", e);
-        }
-    }
-}
diff --git a/src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java b/src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java
deleted file mode 100644
index 547c9d4..0000000
--- a/src/main/java/com/assu/server/domain/auth/crypto/SchoolCredentialEncryptor.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.assu.server.domain.auth.crypto;
-
-public interface SchoolCredentialEncryptor {
-    String encrypt(String plain);   // -> Base64(iv+ciphertext)
-    String decrypt(String cipher);  // Base64(iv+ciphertext) -> plain
-}
diff --git a/src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java b/src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java
deleted file mode 100644
index 50ee46a..0000000
--- a/src/main/java/com/assu/server/domain/auth/crypto/StudentPasswordEncoder.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.assu.server.domain.auth.crypto;
-
-import lombok.RequiredArgsConstructor;
-import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.stereotype.Component;
-
-@Component
-@RequiredArgsConstructor
-public class StudentPasswordEncoder implements PasswordEncoder {
-
-    private final SchoolCredentialEncryptor encryptor;
-
-    @Override
-    public String encode(CharSequence rawPassword) {
-        // 회원가입/갱신 시 암호문 저장이 필요하면 사용 (AES-GCM 암호화)
-        return encryptor.encrypt(rawPassword.toString());
-    }
-
-    @Override
-    public boolean matches(CharSequence rawPassword, String encodedCipher) {
-        try {
-            String plain = encryptor.decrypt(encodedCipher);
-            return constantTimeEquals(plain, rawPassword.toString());
-        } catch (Exception e) {
-            return false;
-        }
-    }
-
-    private boolean constantTimeEquals(String a, String b) {
-        if (a == null || b == null || a.length() != b.length()) return false;
-        int r = 0;
-        for (int i = 0; i < a.length(); i++) r |= a.charAt(i) ^ b.charAt(i);
-        return r == 0;
-    }
-}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java b/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
deleted file mode 100644
index 7241f27..0000000
--- a/src/main/java/com/assu/server/domain/auth/dto/login/StudentLoginRequest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.assu.server.domain.auth.dto.login;
-
-import com.assu.server.domain.user.entity.enums.University;
-import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Size;
-import lombok.*;
-
-/** 학생 로그인 요청 (현재 서비스 로직이 이메일/비밀번호를 사용 중이면 LoginRequest를 그대로 사용해도 됩니다) */
-@Getter
-@Setter
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-@Schema(description = "학생 로그인 요청")
-public class StudentLoginRequest {
-
-    @Schema(description = "학번", example = "20001234")
-    @NotBlank(message = "학번은 필수입니다.")
-    @Size(max = 10, message = "학번은 10자를 넘을 수 없습니다.")
-    private String studentNumber;
-
-    @Schema(description = "로그인 비밀번호(평문)", example = "P@ssw0rd!")
-    @NotBlank(message = "비밀번호는 필수입니다.")
-    @Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.")
-    private String studentPassword;
-
-    @Schema(description = "대학 이름", example = "SSU")
-    @NotNull(message = "대학 이름은 필수입니다.")
-    private University university;
-}
-
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java
deleted file mode 100644
index b4e378c..0000000
--- a/src/main/java/com/assu/server/domain/auth/dto/signup/StudentSignUpRequest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.assu.server.domain.auth.dto.signup;
-
-import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest;
-import com.assu.server.domain.auth.dto.signup.student.StudentAuthPayload;
-import com.assu.server.domain.auth.dto.signup.student.StudentInfoPayload;
-import jakarta.validation.Valid;
-import jakarta.validation.constraints.*;
-import lombok.*;
-import lombok.experimental.SuperBuilder;
-
-/** 학생 가입: JSON */
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-@SuperBuilder
-public class StudentSignUpRequest extends CommonSignUpRequest {
-
-    @Valid
-    @NotNull
-    private StudentAuthPayload studentAuth;
-
-    @Valid
-    @NotNull
-    private StudentInfoPayload studentInfo;
-}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequest.java b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequest.java
new file mode 100644
index 0000000..c5051e6
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/StudentTokenSignUpRequest.java
@@ -0,0 +1,22 @@
+package com.assu.server.domain.auth.dto.signup;
+
+import com.assu.server.domain.auth.dto.signup.common.CommonSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+/** 학생 가입: sToken, sIdno 기반 */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder
+public class StudentTokenSignUpRequest extends CommonSignUpRequest {
+
+    @Valid
+    @NotNull
+    private StudentTokenAuthPayload studentTokenAuth;
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java
deleted file mode 100644
index 89eeef2..0000000
--- a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentAuthPayload.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.assu.server.domain.auth.dto.signup.student;
-
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.Pattern;
-import jakarta.validation.constraints.Size;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-public class StudentAuthPayload {
-    @Pattern(regexp = "^\\d{8,10}$", message = "학번은 숫자 8~10자리여야 합니다.")
-    @NotBlank
-    private String studentNumber;
-
-    @Size(min = 4, max = 64, message = "비밀번호 길이가 올바르지 않습니다.")
-    @NotBlank
-    private String studentPassword; // 저장 전 대칭키 암호화 권장
-}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java
deleted file mode 100644
index d1477a7..0000000
--- a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentInfoPayload.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.assu.server.domain.auth.dto.signup.student;
-
-import com.assu.server.domain.user.entity.enums.Department;
-import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
-import com.assu.server.domain.user.entity.enums.Major;
-import com.assu.server.domain.user.entity.enums.University;
-import jakarta.validation.constraints.*;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-public class StudentInfoPayload {
-
-    @NotNull
-    private Department department;          // 단과대
-
-    @NotNull
-    private EnrollmentStatus enrollmentStatus; // 재학 상태: ENROLLED, LEAVE, GRADUATED
-
-    @NotBlank
-    @Pattern(regexp = "^[1-5]{1}-[1-2]$", message = "yearSemester는 Y-N 형식이어야 합니다. 예: 4-1")
-    @Size(max = 10)
-    private String yearSemester;        // 예: 2025-1
-
-    @NotNull
-    private University university;          // 학교명
-
-    @NotBlank
-    @Size(max = 50)
-    private String major;               // 전공
-}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java
new file mode 100644
index 0000000..8707439
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java
@@ -0,0 +1,27 @@
+package com.assu.server.domain.auth.dto.signup.student;
+
+import com.assu.server.domain.user.entity.enums.University;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class StudentTokenAuthPayload {
+    @Schema(description = "유세인트 sToken", example = "Vy3zFySFx5FASz175Kx7AzKyuSFQEgQ...")
+    @NotNull(message = "sToken은 필수입니다.")
+    @JsonProperty(value = "sToken")
+    private String sToken;
+
+    @Schema(description = "유세인트 sIdno", example = "20211438")
+    @NotNull(message = "sIdno는 필수입니다.")
+    @JsonProperty(value = "sIdno")
+    private String sIdno;
+
+    private University university;
+}
+
diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java
index b089dfb..7e3cac9 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthRequest.java
@@ -1,18 +1,19 @@
 package com.assu.server.domain.auth.dto.ssu;
 
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.*;
 import org.jetbrains.annotations.NotNull;
 
 @Getter
+@Setter
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
 public class USaintAuthRequest {
     @NotNull
+    @JsonProperty(value = "sToken")
     private String sToken;
     @NotNull
-    private Integer sIdno;
+    @JsonProperty(value = "sIdno")
+    private String sIdno;
 }
diff --git a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java
index 5682de0..d4fd445 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/ssu/USaintAuthResponse.java
@@ -9,7 +9,7 @@
 @AllArgsConstructor
 @Builder
 public class USaintAuthResponse {
-    private Integer id;
+    private String studentNumber;
     private String name;
     private String enrollmentStatus;
     private String yearSemester;
diff --git a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
index 76ad400..a60aea6 100644
--- a/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
+++ b/src/main/java/com/assu/server/domain/auth/entity/SSUAuth.java
@@ -32,11 +32,6 @@ public class SSUAuth extends BaseEntity {
     @Column(name = "student_number", length = 20, nullable = false)
     private String studentNumber;
 
-    // TEXT 컬럼
-    @Lob
-    @Column(name = "password_cipher", columnDefinition = "TEXT", nullable = false)
-    private String passwordCipher;
-
     @Column(name = "is_authenticated", nullable = false)
     private Boolean isAuthenticated = Boolean.FALSE;
 
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
index 0f763c6..0b52fab 100644
--- a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
@@ -1,6 +1,5 @@
 package com.assu.server.domain.auth.security.adapter;
 
-import com.assu.server.domain.auth.crypto.StudentPasswordEncoder;
 import com.assu.server.domain.auth.entity.AuthRealm;
 import com.assu.server.domain.auth.entity.SSUAuth;
 import com.assu.server.domain.auth.exception.CustomAuthException;
@@ -13,11 +12,12 @@
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Component;
 
+import java.time.LocalDateTime;
+
 @Component
 @RequiredArgsConstructor
 public class SSUAuthAdapter implements RealmAuthAdapter {
     private final SSUAuthRepository ssuAuthRepository;
-    private final StudentPasswordEncoder studentPasswordEncoder; // AES-GCM 비교
 
     @Override public boolean supports(AuthRealm realm) { return realm == AuthRealm.SSU; }
 
@@ -29,16 +29,18 @@ public UserDetails loadUserDetails(String studentNumber) {
         boolean enabled = m.getIsActivated() == ActivationStatus.ACTIVE;
         String authority = "ROLE_" + m.getRole().name();
 
+        // sToken/sIdno 기반 인증이므로 더미 패스워드 사용
         return org.springframework.security.core.userdetails.User
                 .withUsername(sa.getStudentNumber())
-                .password(sa.getPasswordCipher()) // 암호문, 비교는 Encoder가 담당
+                .password("") // 더미 패스워드 (실제 인증은 sToken/sIdno로 수행)
                 .authorities(authority)
                 .accountExpired(false).accountLocked(false).credentialsExpired(false)
                 .disabled(!enabled)
                 .build();
     }
 
-    @Override public Member loadMember(String studentNumber) {
+    @Override
+    public Member loadMember(String studentNumber) {
         return ssuAuthRepository.findByStudentNumber(studentNumber)
                 .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER))
                 .getMember();
@@ -49,17 +51,34 @@ public void registerCredentials(Member member, String studentNumber, String rawP
         if (ssuAuthRepository.existsByStudentNumber(studentNumber)) {
             throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
         }
-        String cipher = studentPasswordEncoder.encode(rawPassword);
         ssuAuthRepository.save(
                 SSUAuth.builder()
                         .member(member)
                         .studentNumber(studentNumber)
-                        .passwordCipher(cipher)
                         .isAuthenticated(true)
+                        .authenticatedAt(LocalDateTime.now())
                         .build()
         );
     }
 
-    @Override public PasswordEncoder passwordEncoder() { return studentPasswordEncoder; }
-    @Override public String authRealmValue() { return "SSU"; }
-}
+    @Override
+    public PasswordEncoder passwordEncoder() {
+        // 더미 패스워드 인코더 (실제 인증은 sToken/sIdno로 수행)
+        return new PasswordEncoder() {
+            @Override
+            public String encode(CharSequence rawPassword) {
+                return "";
+            }
+
+            @Override
+            public boolean matches(CharSequence rawPassword, String encodedPassword) {
+                return true; // 항상 true 반환 (실제 인증은 sToken/sIdno로 수행)
+            }
+        };
+    }
+
+    @Override
+    public String authRealmValue() {
+        return "SSU";
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
index b3ae7f3..34a5b37 100644
--- a/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
+++ b/src/main/java/com/assu/server/domain/auth/security/jwt/JwtUtil.java
@@ -234,9 +234,8 @@ public Authentication getAuthenticationFromExpiredAccessToken(String expiredAcce
             username = member.getCommonAuth().getEmail();
             password = member.getCommonAuth().getPassword();
         } else if (realm == AuthRealm.SSU){
-            // 예: 학생 Realm
             username = member.getSsuAuth().getStudentNumber();
-            password = member.getSsuAuth().getPasswordCipher();
+            password = ""; // 더미 처리
         }
 
         // DB에서 조회한 member를 직접 넣어줌
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginService.java b/src/main/java/com/assu/server/domain/auth/service/LoginService.java
index d95714c..ff73136 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginService.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginService.java
@@ -3,10 +3,10 @@
 import com.assu.server.domain.auth.dto.login.CommonLoginRequest;
 import com.assu.server.domain.auth.dto.login.LoginResponse;
 import com.assu.server.domain.auth.dto.login.RefreshResponse;
-import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
+import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload;
 
 public interface LoginService {
     LoginResponse loginCommon(CommonLoginRequest request);
-    LoginResponse loginStudent(StudentLoginRequest request);
+    LoginResponse loginSsuStudent(StudentTokenAuthPayload request);
     RefreshResponse refresh(String refreshToken);
 }
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
index 5d38188..3177e2a 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -3,7 +3,9 @@
 import com.assu.server.domain.auth.dto.login.CommonLoginRequest;
 import com.assu.server.domain.auth.dto.login.LoginResponse;
 import com.assu.server.domain.auth.dto.login.RefreshResponse;
-import com.assu.server.domain.auth.dto.login.StudentLoginRequest;
+import com.assu.server.domain.auth.dto.signup.student.StudentTokenAuthPayload;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse;
 import com.assu.server.domain.auth.dto.signup.Tokens;
 import com.assu.server.domain.auth.entity.AuthRealm;
 import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
@@ -11,11 +13,15 @@
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
+import com.assu.server.domain.user.entity.Student;
+import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
+import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 
@@ -25,6 +31,8 @@ public class LoginServiceImpl implements LoginService {
 
     private final AuthenticationManager authenticationManager;
     private final JwtUtil jwtUtil;
+    private final SSUAuthService ssuAuthService;
+    private final StudentRepository studentRepository;
 
     // 공통/학생/기타 학교까지 모두 여기로 주입
     private final List realmAuthAdapters;
@@ -74,34 +82,52 @@ public LoginResponse loginCommon(CommonLoginRequest request) {
     }
 
     /**
-     * 학생 로그인: 학번/학교 비밀번호 기반.
-     * 1) 인증 성공 시 SSUAuth 조회
-     * 2) JWT 발급: username=studentNumber, authRealm=SSU
+     * 숭실대 학생 로그인: sToken, sIdno 기반.
+     * 1) 유세인트 인증으로 학생 정보 확인
+     * 2) 기존 회원 확인
+     * 3) Student 정보 업데이트 (유세인트에서 크롤링한 최신 정보로)
+     * 4) JWT 발급: username=studentNumber, authRealm=SSU
      */
     @Override
-    public LoginResponse loginStudent(StudentLoginRequest request) {
-
-        String realmStr = request.getUniversity().toString();  // University → AuthRealm 매핑
+    @Transactional
+    public LoginResponse loginSsuStudent(StudentTokenAuthPayload request) {
+        // 1) 유세인트 인증
+        USaintAuthRequest authRequest = USaintAuthRequest.builder()
+                        .sToken(request.getSToken())
+                        .sIdno(request.getSIdno())
+                        .build();
+
+        USaintAuthResponse authResponse = ssuAuthService.uSaintAuth(authRequest);
+
+        // 2) 기존 회원 확인
+        String realmStr = request.getUniversity().toString();
         AuthRealm authRealm = AuthRealm.valueOf(realmStr);
         RealmAuthAdapter adapter = pickAdapter(authRealm);
 
-        Authentication authentication = authenticationManager.authenticate(
-                new LoginUsernamePasswordAuthenticationToken(
-                        authRealm,
-                        request.getStudentNumber(),
-                        request.getStudentPassword()
-                )
+        Member member = adapter.loadMember(authResponse.getStudentNumber().toString());
+
+        // 3) Student 정보 업데이트 (유세인트에서 크롤링한 최신 정보로)
+        Student student = member.getStudentProfile();
+        if (student == null) {
+                throw new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER);
+        }
+
+        // 유세인트에서 크롤링한 최신 정보로 업데이트
+        student.updateStudentInfo(
+                authResponse.getName(),
+                authResponse.getMajor(),
+                parseEnrollmentStatus(authResponse.getEnrollmentStatus()),
+                authResponse.getYearSemester()
         );
 
-        // identifier = studentNumber
-        Member member = adapter.loadMember(authentication.getName());
+        studentRepository.save(student);
 
-        // 토큰 발급 (Access 미저장, Refresh는 Redis 저장)
+        // 4) 토큰 발급
         Tokens tokens = jwtUtil.issueTokens(
-                member.getId(),
-                authentication.getName(), // studentNumber
-                member.getRole(),
-                adapter.authRealmValue()    // 예: "SSU"
+                        member.getId(),
+                        authResponse.getStudentNumber().toString(), // studentNumber
+                        member.getRole(),
+                        adapter.authRealmValue() // 예: "SSU"
         );
 
         return LoginResponse.builder()
@@ -131,4 +157,20 @@ public RefreshResponse refresh(String refreshToken) {
                 rotated.getRefreshToken()
         );
     }
+
+    private EnrollmentStatus parseEnrollmentStatus(String status) {
+        if (status == null || status.isBlank()) {
+            return EnrollmentStatus.ENROLLED;
+        }
+        if (status.contains("재학")) {
+            return EnrollmentStatus.ENROLLED;
+        } else if (status.contains("휴학")) {
+            return EnrollmentStatus.LEAVE;
+        } else if (status.contains("졸업")) {
+            return EnrollmentStatus.GRADUATED;
+        } else {
+            // 기본값은 재학으로 설정
+            return EnrollmentStatus.ENROLLED;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
index ef73dac..5c22bdf 100644
--- a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
@@ -33,7 +33,7 @@ public class SSUAuthServiceImpl implements SSUAuthService {
     public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) {
 
         String sToken = uSaintAuthRequest.getSToken();
-        Integer sIdno = uSaintAuthRequest.getSIdno();
+        String sIdno = uSaintAuthRequest.getSIdno();
 
         // 1) SSO 로그인 요청
         ResponseEntity uSaintSSOResponseEntity;
@@ -124,7 +124,7 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) {
             switch (dt.text()) {
                 case "학번" -> {
                     try {
-                        usaintAuthResponse.setId(Integer.valueOf(strong.text()));
+                        usaintAuthResponse.setStudentNumber(strong.text());
                     } catch (NumberFormatException e) {
                         log.error("Invalid studentId format: {}", strong.text());
                         throw new CustomAuthException(ErrorStatus.SSU_SAINT_PARSE_FAILED);
@@ -157,7 +157,7 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) {
         return usaintAuthResponse;
     }
 
-    private ResponseEntity requestUSaintSSO(String sToken, Integer sIdno) {
+    private ResponseEntity requestUSaintSSO(String sToken, String sIdno) {
         String url = USaintSSOUrl + "?sToken=" + sToken + "&sIdno=" + sIdno;
 
         return webClient.get()
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 7ae7e70..8cd8cd0 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
@@ -3,11 +3,11 @@
 import com.assu.server.domain.auth.dto.signup.AdminSignUpRequest;
 import com.assu.server.domain.auth.dto.signup.PartnerSignUpRequest;
 import com.assu.server.domain.auth.dto.signup.SignUpResponse;
-import com.assu.server.domain.auth.dto.signup.StudentSignUpRequest;
+import com.assu.server.domain.auth.dto.signup.StudentTokenSignUpRequest;
 import org.springframework.web.multipart.MultipartFile;
 
 public interface SignUpService {
-    SignUpResponse signupStudent(StudentSignUpRequest req);
+    SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req);
     SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile licenseImage);
     SignUpResponse signupAdmin(AdminSignUpRequest 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 a40a91c..a6bbde6 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
@@ -4,9 +4,11 @@
 import com.assu.server.domain.admin.repository.AdminRepository;
 import com.assu.server.domain.auth.dto.signup.*;
 import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
-import com.assu.server.domain.auth.dto.signup.student.StudentInfoPayload;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest;
+import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse;
 import com.assu.server.domain.auth.entity.AuthRealm;
 import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.auth.repository.SSUAuthRepository;
 import com.assu.server.domain.auth.security.adapter.RealmAuthAdapter;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.domain.common.enums.ActivationStatus;
@@ -18,10 +20,10 @@
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.domain.user.entity.Student;
-import com.assu.server.domain.user.entity.enums.Major;
+import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
+import com.assu.server.domain.user.entity.enums.University;
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
-import com.assu.server.global.config.KakaoLocalClient;
 import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
@@ -34,7 +36,6 @@
 import java.util.List;
 import java.util.Optional;
 
-
 @Service
 @RequiredArgsConstructor
 public class SignUpServiceImpl implements SignUpService {
@@ -50,9 +51,10 @@ public class SignUpServiceImpl implements SignUpService {
     private final AmazonS3Manager amazonS3Manager;
     private final JwtUtil jwtUtil;
 
-    private final KakaoLocalClient kakaoLocalClient;
     private final GeometryFactory geometryFactory;
     private final StoreRepository storeRepository;
+    private final SSUAuthService ssuAuthService;
+    private final SSUAuthRepository ssuAuthRepository;
 
     private RealmAuthAdapter pickAdapter(AuthRealm realm) {
         return realmAuthAdapters.stream()
@@ -61,16 +63,29 @@ private RealmAuthAdapter pickAdapter(AuthRealm realm) {
                 .orElseThrow(() -> new CustomAuthException(ErrorStatus.AUTHORIZATION_EXCEPTION));
     }
 
-    /* 학생: JSON */
+    /* 숭실대 학생: sToken, sIdno 기반 회원가입 */
     @Override
     @Transactional
-    public SignUpResponse signupStudent(StudentSignUpRequest req) {
+    public SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req) {
         // 중복 체크
         if (memberRepository.existsByPhoneNum(req.getPhoneNumber())) {
             throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
         }
 
-        // 1) member 생성
+        // 1) 유세인트 인증 및 학생 정보 추출
+        USaintAuthRequest authRequest = USaintAuthRequest.builder()
+                .sToken(req.getStudentTokenAuth().getSToken())
+                .sIdno(req.getStudentTokenAuth().getSIdno())
+                .build();
+
+        USaintAuthResponse authResponse = ssuAuthService.uSaintAuth(authRequest);
+
+        // 학번 중복 체크
+        if (ssuAuthRepository.existsByStudentNumber(authResponse.getStudentNumber().toString())) {
+                throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
+        }
+
+        // 2) member 생성
         Member member = memberRepository.save(
                 Member.builder()
                         .phoneNum(req.getPhoneNumber())
@@ -80,42 +95,29 @@ public SignUpResponse signupStudent(StudentSignUpRequest req) {
                         .build()
         );
 
-        // 2) RealmAuthAdapter 로 자격 저장 (학교별 암호화 전략 반영됨)
-        RealmAuthAdapter adapter = pickAdapter(AuthRealm.valueOf(req.getStudentInfo().getUniversity().toString()));
-        adapter.registerCredentials(member, req.getStudentAuth().getStudentNumber(), req.getStudentAuth().getStudentPassword());
-
-        // 3) Student 프로필 생성
-        StudentInfoPayload info = req.getStudentInfo();
-        Major major;
-        switch (info.getMajor()) {
-            case "컴퓨터학부" -> major = Major.COM;
-            case "소프트웨어학부" -> major = Major.SW;
-            case "글로벌미디어학부" -> major = Major.GM;
-            case "미디어경영학과" -> major = Major.MB;
-            case "AI융합학부" -> major = Major.AI;
-            case "전자정보공학부" -> major = Major.EE;
-            case "정보보호학과" -> major = Major.IP;
-            default -> major = null;
-        }
+        // 3) SSUAuth 생성 (학번만 저장)
+        RealmAuthAdapter adapter = pickAdapter(AuthRealm.SSU);
+        adapter.registerCredentials(member, authResponse.getStudentNumber().toString(), ""); // 더미 패스워드
+
+        // 4) Student 프로필 생성 (크롤링된 정보 사용)
+        Student student = Student.builder()
+                .member(member)
+                .name(authResponse.getName())
+                .major(authResponse.getMajor())
+                .enrollmentStatus(parseEnrollmentStatus(authResponse.getEnrollmentStatus()))
+                .yearSemester(authResponse.getYearSemester())
+                .university(University.SSU) // 고정값
+                .stamp(0)
+                .build();
 
-        studentRepository.save(
-                Student.builder()
-                        .member(member)
-                        .department(info.getDepartment())
-                        .enrollmentStatus(info.getEnrollmentStatus())
-                        .yearSemester(info.getYearSemester())
-                        .university(info.getUniversity())
-                        .stamp(0)
-                        .major(major)
-                        .build()
-        );
+        studentRepository.save(student);
 
-        // 4) 토큰 발급
+        // 5) JWT 토큰 발급
         Tokens tokens = jwtUtil.issueTokens(
                 member.getId(),
-                req.getStudentAuth().getStudentNumber(),
+                authResponse.getStudentNumber().toString(), // studentNumber
                 UserRole.STUDENT,
-                adapter.authRealmValue()
+                "SSU"
         );
 
         return SignUpResponse.builder()
@@ -281,6 +283,23 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
                 .build();
     }
 
+    private EnrollmentStatus parseEnrollmentStatus(String status) {
+        if (status == null || status.isBlank()) {
+            return EnrollmentStatus.ENROLLED;
+        }
+
+        if (status.contains("재학")) {
+            return EnrollmentStatus.ENROLLED;
+        } else if (status.contains("휴학")) {
+            return EnrollmentStatus.LEAVE;
+        } else if (status.contains("졸업")) {
+            return EnrollmentStatus.GRADUATED;
+        } else {
+            // 기본값은 재학으로 설정
+            return EnrollmentStatus.ENROLLED;
+        }
+    }
+
     public Point toPoint(Double lat, Double lng) {
         if (lat == null || lng == null) return null;
         Point p = geometryFactory.createPoint(new Coordinate(lng, lat)); // x=lng, y=lat
diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java
index e63aa8f..b8c1cb4 100644
--- a/src/main/java/com/assu/server/domain/user/entity/Student.java
+++ b/src/main/java/com/assu/server/domain/user/entity/Student.java
@@ -1,6 +1,5 @@
 package com.assu.server.domain.user.entity;
 
-
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.user.entity.enums.Department;
 import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
@@ -27,13 +26,14 @@ public class Student {
     @MapsId
     private Member member;
 
+    private String name;
+
     @Enumerated(EnumType.STRING)
     private Department department;
 
     @Enumerated(EnumType.STRING)
     private EnrollmentStatus enrollmentStatus;
 
-    @Pattern(regexp = "^[0-9]{1}-[1-2]$", message = "yearSemester는 Y-N 형식이어야 합니다. 예: 3-1")
     private String yearSemester;
 
     @Enumerated(EnumType.STRING)
@@ -49,9 +49,24 @@ public void setMember(Member member) {
     }
 
     public void setStamp() {
-        if(this.stamp ==10)
-            this.stamp=1;
+        if (this.stamp == 10)
+            this.stamp = 1;
         else
             this.stamp++;
     }
+
+    /**
+     * 유세인트에서 크롤링한 최신 정보로 학생 정보를 업데이트합니다.
+     * 
+     * @param name             학생 이름
+     * @param major            전공
+     * @param enrollmentStatus 학적 상태
+     * @param yearSemester     학년/학기
+     */
+    public void updateStudentInfo(String name, Major major, EnrollmentStatus enrollmentStatus, String yearSemester) {
+        this.name = name;
+        this.major = major;
+        this.enrollmentStatus = enrollmentStatus;
+        this.yearSemester = yearSemester;
+    }
 }
diff --git a/src/main/java/com/assu/server/global/config/ProjectConfig.java b/src/main/java/com/assu/server/global/config/ProjectConfig.java
index 2efd491..62ede62 100644
--- a/src/main/java/com/assu/server/global/config/ProjectConfig.java
+++ b/src/main/java/com/assu/server/global/config/ProjectConfig.java
@@ -1,29 +1,14 @@
 package com.assu.server.global.config;
 
-import com.assu.server.domain.auth.crypto.AesGcmSchoolCredentialEncryptor;
-import com.assu.server.domain.auth.crypto.SchoolCredentialEncryptor;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 
-import java.util.Base64;
-
 @Configuration
 public class ProjectConfig {
     @Bean
     public PasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();
     }
-
-    @Bean
-    public SchoolCredentialEncryptor schoolCredentialEncryptor(@Value("${assu.security.school-crypto.base64-key}") String base64key) {
-        byte[] keyBytes = Base64.getDecoder().decode(base64key);
-        int len = keyBytes.length; // 16, 24, 32만 허용
-        if (len != 16 && len != 24 && len != 32) {
-            throw new IllegalStateException("AES key must be 16/24/32 bytes after Base64 decoding");
-        }
-        return new AesGcmSchoolCredentialEncryptor(keyBytes);
-    }
 }
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index b6722ba..bce6a32 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -10,7 +10,7 @@ spring:
       - optional:file:/app/config/application-secret.yml
   jpa:
     hibernate:
-      ddl-auto: update
+      ddl-auto: create
     properties:
       hibernate:
         jdbc:
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 8a3197b..ac859d1 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -21,11 +21,6 @@ jwt:
   access-valid-seconds: 3600
   refresh-valid-seconds: 1209600
 
-assu:
-  security:
-    school-crypto:
-      base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=" #"dummy-base64-key"를 Base64로 인코딩한 값
-
   messaging:
     rabbit:
       enabled: false

From d7b8d34d75f2a8a566fd4a60922f9f07aa50ce0e Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 9 Sep 2025 02:40:44 +0900
Subject: [PATCH 157/270] =?UTF-8?q?[CHORE/#46]=20application.yml=20update?=
 =?UTF-8?q?=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/resources/application.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index bce6a32..b6722ba 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -10,7 +10,7 @@ spring:
       - optional:file:/app/config/application-secret.yml
   jpa:
     hibernate:
-      ddl-auto: create
+      ddl-auto: update
     properties:
       hibernate:
         jdbc:

From d89938bfd3ee773ce4070baa45c388e219dde025 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Tue, 9 Sep 2025 12:36:58 +0900
Subject: [PATCH 158/270] =?UTF-8?q?[HOTFIX]=20docker=20network=20external?=
 =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 docker-compose.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 59c3991..9b1f2ac 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -35,4 +35,4 @@ services:
 
 networks:
   assu-network:
-    driver: bridge
+    external: true

From 3f3b2920acaf0003f10099f197382a161304695a Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Tue, 9 Sep 2025 14:07:56 +0900
Subject: [PATCH 159/270] =?UTF-8?q?[FEAT/#52]=20admin=20=EB=A1=9C=EA=B7=B8?=
 =?UTF-8?q?=EC=9D=B8=20=EB=B0=94=EA=BE=B8=EA=B3=A0=20matching=20admin=20?=
 =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/admin/entity/Admin.java     |  8 ++++++++
 .../domain/admin/repository/AdminRepository.java       | 10 +++++-----
 .../assu/server/domain/admin/service/AdminService.java |  2 +-
 .../server/domain/admin/service/AdminServiceImpl.java  |  4 +++-
 .../auth/dto/signup/common/CommonAuthPayload.java      | 10 ++++++++++
 .../server/domain/auth/service/SignUpServiceImpl.java  |  3 +++
 6 files changed, 30 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
index 2fa4cf5..288b3f5 100644
--- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java
+++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
@@ -1,8 +1,10 @@
 package com.assu.server.domain.admin.entity;
 
 
+import com.assu.server.domain.user.entity.enums.Department;
 import com.assu.server.domain.user.entity.enums.Major;
 import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.user.entity.enums.University;
 
 import jakarta.persistence.Entity;
 import jakarta.persistence.EnumType;
@@ -50,6 +52,12 @@ public class Admin {
     @Enumerated(EnumType.STRING)
     private Major major;
 
+    @Enumerated(EnumType.STRING)
+    private Department department;
+
+    @Enumerated(EnumType.STRING)
+    private University university;
+
     @JdbcTypeCode(SqlTypes.GEOMETRY)
     private Point point;
 
diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
index c00a1b1..45010c8 100644
--- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
+++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
@@ -19,11 +19,11 @@ public interface AdminRepository extends JpaRepository {
 
 	// 여기 예원이 머지하고 수정
 	@Query("SELECT a FROM Admin a WHERE " +
-		"a.name LIKE %:university% OR " +
-		"a.name LIKE %:department% OR " +
-		"a.major = :major")
-	List findMatchingAdmins(@Param("university") String university,
-		@Param("department") String department,
+		"(a.university = :university AND a.department IS NULL AND a.major IS NULL) OR " +
+		"(a.university = :university AND a.department = :department AND a.major IS NULL) OR " +
+		"(a.university = :university AND a.department = :department AND a.major = :major)")
+	List findMatchingAdmins(@Param("university") University university,
+		@Param("department") Department department,
 		@Param("major") Major major);
 
 	Optional findByName(String name);
diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminService.java b/src/main/java/com/assu/server/domain/admin/service/AdminService.java
index 269f0a4..32da6ff 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminService.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminService.java
@@ -10,7 +10,7 @@
 
 // PaperQueryServiceImpl 이 AdminService 참조 중 -> 순환참조 문제 발생하지 않도록 주의
 public interface AdminService {
-	List findMatchingAdmins(String university, String department, Major major);
+	List findMatchingAdmins(University university, Department department, Major major);
 
     AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId);
 
diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
index afde2f7..824d562 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
@@ -6,11 +6,13 @@
 import com.assu.server.domain.admin.dto.AdminResponseDTO;
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
+import com.assu.server.domain.user.entity.enums.Department;
 import com.assu.server.domain.user.entity.enums.Major;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.user.entity.enums.University;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
 import java.util.concurrent.ThreadLocalRandom;
@@ -24,7 +26,7 @@ public class AdminServiceImpl implements AdminService {
     private final PartnerRepository partnerRepository;
 	@Override
 	@Transactional
-	public List findMatchingAdmins(String university, String department, Major major){
+	public List findMatchingAdmins(University university, Department department, Major major){
 
 
 		List adminList = adminRepository.findMatchingAdmins(university, department,major);
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java
index fcdccd9..bb80d8b 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/common/CommonAuthPayload.java
@@ -1,6 +1,10 @@
 package com.assu.server.domain.auth.dto.signup.common;
 
 import com.assu.server.domain.auth.exception.annotation.PasswordMatches;
+import com.assu.server.domain.user.entity.enums.Department;
+import com.assu.server.domain.user.entity.enums.Major;
+import com.assu.server.domain.user.entity.enums.University;
+
 import jakarta.validation.constraints.Size;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -19,4 +23,10 @@ public class CommonAuthPayload {
 
     @Size(min = 8, max = 72) @NotBlank
     private String password;
+
+    private Department department;
+
+    private Major major;
+
+    private University university;
 }
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 a40a91c..17a9b62 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
@@ -254,6 +254,9 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
         // 3) Partner 프로필 생성
         adminRepository.save(
                 Admin.builder()
+                    .major(req.getCommonAuth().getMajor())
+                    .department(req.getCommonAuth().getDepartment())
+                    .university(req.getCommonAuth().getUniversity())
                         .member(member)
                         .name(info.getName())
                         .officeAddress(address)

From ebe4dacac9ca0ae800a795369df8e7565ba28e28 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Tue, 9 Sep 2025 14:08:16 +0900
Subject: [PATCH 160/270] =?UTF-8?q?[FIX/#52]=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../CertificationSessionManager.java          |  2 +-
 .../config/CertifyWebSocketConfig.java        |  2 +-
 .../service/CertificationServiceImpl.java     | 33 ++++++++++---
 .../converter/PartnershipConverter.java       | 49 ++++++++++++-------
 .../dto/PaperContentResponseDTO.java          |  1 +
 .../service/PaperQueryServiceImpl.java        |  4 +-
 6 files changed, 60 insertions(+), 31 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java
index 624d4e0..2b36a58 100644
--- a/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java
+++ b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java
@@ -16,7 +16,7 @@ public void openSession(Long sessionId) {
 	}
 
 	public void addUserToSession(Long sessionId, Long userId) {
-		sessionUserMap.getOrDefault(sessionId, ConcurrentHashMap.newKeySet()).add(userId);
+		sessionUserMap.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(userId);
 	}
 
 	public int getCurrentUserCount(Long sessionId) {
diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 06e797f..7d1ce5b 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -17,7 +17,7 @@ public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer
 	private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
 	@Override
 	public void configureMessageBroker(MessageBrokerRegistry config) {
-		config.enableSimpleBroker("/certification/progress"); // 인증현황을 받아보기 위한 구독 주소
+		config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
 		config.setApplicationDestinationPrefixes("/certification"); // 클라이언트가 인증 요청을 보내는 주소
 	}
 
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 e65e948..3362258 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
@@ -90,13 +90,13 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto,
 		// 제휴 대상인지 확인하기
 		Long adminId = dto.getAdminId();
 		Student student = member.getStudentProfile();
-		List admins = adminService.findMatchingAdmins(student.getUniversity().toString(), student.getDepartment().toString(), student.getMajor());
+		List admins = adminService.findMatchingAdmins(student.getUniversity(), student.getDepartment(), student.getMajor());
 
 		boolean matched = admins.stream()
 			.anyMatch(admin -> admin.getId().equals(adminId));
 
 		if (!matched) {
-			throw new IllegalArgumentException("관리자 정보가 일치하지 않습니다.");
+			throw new IllegalArgumentException("학생과 매치되지 않는 정보입니다.");
 		}
 
 
@@ -117,21 +117,38 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto,
 		sessionManager.addUserToSession(sessionId, userId);
 		int currentCertifiedNumber = sessionManager.getCurrentUserCount(sessionId);
 
-		messagingTemplate.convertAndSend("/certification/progress"+sessionId,
-			new CurrentProgress.CertificationNumber(currentCertifiedNumber));
-
+		// messagingTemplate.convertAndSend("/certification/progress/"+sessionId,
+		// 	new CurrentProgress.CertificationNumber(currentCertifiedNumber));
+		//
+		// if(currentCertifiedNumber >= session.getPeopleNumber()){
+		// 	session.setIsCertified(true);
+		// 	session.setStatus(SessionStatus.COMPLETED);
+		// 	associateCertificationRepository.save(session);
+		//
+		//
+		// 	messagingTemplate.convertAndSend("/certification/progress/"+sessionId,
+		// 		new CurrentProgress.CompletedNotification("인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId))
+		// 	);
+		// }
 		if(currentCertifiedNumber >= session.getPeopleNumber()){
 			session.setIsCertified(true);
 			session.setStatus(SessionStatus.COMPLETED);
 			associateCertificationRepository.save(session);
 
-
-			messagingTemplate.convertAndSend("/certification/progress"+sessionId,
-				new CurrentProgress.CompletedNotification("인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId))
+			// 완료 알림에 현재 인원수도 포함
+			messagingTemplate.convertAndSend("/certification/progress/" + sessionId,
+				new CurrentProgress.CompletedNotification(
+					"인증이 완료되었습니다.",
+					sessionManager.snapshotUserIds(sessionId)
+				)
 			);
+		} else {
+			messagingTemplate.convertAndSend("/certification/progress/" + sessionId,
+				new CurrentProgress.CertificationNumber(currentCertifiedNumber));
 		}
 
 
+
 	}
 
 	@Override
diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index 7394ced..c8b8fb1 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -78,11 +78,7 @@ public static List toPaperContents(
 
 
 
-	public static List toContentResponseList(List contents) {
-		return contents.stream()
-			.map(PartnershipConverter::toContentResponse)
-			.toList();
-	}
+
     public static List> toGoodsBatches(
             PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO
     ) {
@@ -105,6 +101,15 @@ public static List> toGoodsBatches(
         return batches;
     }
 
+
+
+	public static List toContentResponseList(List contents) {
+		return contents.stream()
+			.map(PartnershipConverter::toContentResponse)
+			.toList();
+	}
+
+
 	public static PaperContentResponseDTO.storePaperContentResponse toContentResponse(PaperContent content) {
 		List goodsList = extractGoods(content);
 		Integer peopleValue = extractPeople(content);
@@ -112,26 +117,14 @@ public static PaperContentResponseDTO.storePaperContentResponse toContentRespons
 
 		return PaperContentResponseDTO.storePaperContentResponse.builder()
 			.adminName(content.getPaper().getAdmin().getName())
+			.cost(content.getCost())
 			.paperContent(paperContentText)
 			.contentId(content.getId())
 			.goods(goodsList)
 			.people(peopleValue)
 			.build();
 	}
-    public static Paper toPaperForManual(
-            Admin admin, Store store,
-            LocalDate start, LocalDate end,
-            ActivationStatus status
-    ) {
-        return Paper.builder()
-                .admin(admin)
-                .store(store)
-                .partner(null)
-                .isActivated(status)
-                .partnershipPeriodStart(start)
-                .partnershipPeriodEnd(end)
-                .build();
-    }
+
 
 	private static List extractGoods(PaperContent content) {
 		if (content.getOptionType() == OptionType.SERVICE && content.getCategory() != null) {
@@ -192,6 +185,24 @@ else if (content.getCriterionType() == CriterionType.PRICE &&
 
 		return result;
 	}
+
+
+	public static Paper toPaperForManual(
+		Admin admin, Store store,
+		LocalDate start, LocalDate end,
+		ActivationStatus status
+	) {
+		return Paper.builder()
+			.admin(admin)
+			.store(store)
+			.partner(null)
+			.isActivated(status)
+			.partnershipPeriodStart(start)
+			.partnershipPeriodEnd(end)
+			.build();
+	}
+
+
     public static List toPaperContentsForManual(
             List options,
             Paper paper
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java
index 60d9355..3aff7d4 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java
@@ -18,5 +18,6 @@ public static class storePaperContentResponse{
 		Long contentId;
 		List goods;
 		Integer people;
+		Long cost;
 	}
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java
index 84b88d0..48d9b91 100644
--- a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java
@@ -47,8 +47,8 @@ public PaperResponseDTO.partnershipContent getStorePaperContent(Long storeId, Me
 
 		// 유저의 학교, 단과대, 학부 정보를 조회하여 일치하는 admin을 찾습니다.
 		List adminList = adminService.findMatchingAdmins(
-			student.getUniversity().toString(),
-			student.getDepartment().toString(),
+			student.getUniversity(),
+			student.getDepartment(),
 			student.getMajor());
 
 		// // 한번 더 거르기 위해서

From ade4bf9e49f3c03782db9ca15ae0bb1063f1704f Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Tue, 9 Sep 2025 16:19:52 +0900
Subject: [PATCH 161/270] =?UTF-8?q?[MOD/#14]=20=20-=20=EC=BB=A8=ED=8A=B8?=
 =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EB=B2=84=EC=A0=80=EB=8B=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/SuggestionController.java      | 21 ++++++++++++--
 .../converter/SuggestionConverter.java        | 16 +++++++----
 .../suggestion/dto/SuggestionResponseDTO.java | 22 +++++++++++----
 .../suggestion/service/SuggestionService.java |  5 +---
 .../service/SuggestionServiceImpl.java        | 28 +++++++++++++------
 5 files changed, 65 insertions(+), 27 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
index 7fd5b20..f711ab5 100644
--- a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
+++ b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
@@ -7,12 +7,14 @@
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
 
+@Tag(name = "Suggestion", description = "제휴 건의 API")
 @RestController
 @RequiredArgsConstructor // 파라미터가 있어야만 하는 생성자
 @RequestMapping("/suggestion") // suggestion 아래에서 시작
@@ -20,11 +22,12 @@ public class SuggestionController {
 
     private final SuggestionService suggestionService;
 
+
+    @PostMapping
     @Operation(
             summary = "제휴 건의 API",
-            description = "건의대상, 제휴 희망 가게, 희망 혜택을 입력하세요."
+            description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 관리자에게 제휴를 건의합니다.\n"
     )
-    @PostMapping
     public BaseResponse writeSuggestion(
             @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO,
             @AuthenticationPrincipal PrincipalDetails pd
@@ -33,11 +36,23 @@ public BaseResponse writeSugge
         return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO, userId));
     }
 
+    @GetMapping("/admin")
+    @Operation(
+            summary = "제휴 건의대상 조회 API",
+            description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 현재 로그인한 학생(User)이 제휴를 건의할 수 있는 학생회(Admin)을 조회합니다.\n"
+    )
+    public BaseResponse getSuggestionAdmins(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        Long userId = pd.getMember().getId();
+        return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.getSuggestionAdmins(userId));
+    }
+
+    @GetMapping("/list")
     @Operation(
             summary = "제휴 건의 조회 API",
             description = "모든 제휴 건의를 조회합니다."
     )
-    @GetMapping("/list")
     public BaseResponse> getSuggestions(
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
diff --git a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
index 381f4df..3757531 100644
--- a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
+++ b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
@@ -1,7 +1,6 @@
 package com.assu.server.domain.suggestion.converter;
 
 import com.assu.server.domain.admin.entity.Admin;
-import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO;
 import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO;
 import com.assu.server.domain.suggestion.entity.Suggestion;
@@ -15,10 +14,9 @@ public class SuggestionConverter {
     public static SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestionResultDTO(Suggestion suggestion){
         return SuggestionResponseDTO.WriteSuggestionResponseDTO.builder()
                 .suggestionId(suggestion.getId())
-                .memberId(suggestion.getStudent().getId())
-                .studentNumber(suggestion.getStudent().getStudentNumber())
-                .suggestionSubjectId(suggestion.getAdmin().getId())
-                .suggestionStore(suggestion.getStoreName())
+                .userId(suggestion.getStudent().getId())
+                .adminId(suggestion.getAdmin().getId())
+                .storeName(suggestion.getStoreName())
                 .suggestionBenefit(suggestion.getContent())
                 .build();
     }
@@ -38,8 +36,8 @@ public static SuggestionResponseDTO.GetSuggestionResponseDTO GetSuggestionResult
         return SuggestionResponseDTO.GetSuggestionResponseDTO.builder()
                 .suggestionId(s.getId())
                 .createdAt(s.getCreatedAt())
+                .storeName(s.getStoreName())
                 .content(s.getContent())
-                .studentNumber(student.getStudentNumber())
                 .enrollmentStatus(student.getEnrollmentStatus())
                 .studentMajor(student.getMajor())
                 .build();
@@ -50,4 +48,10 @@ public static List toGetSuggesti
                 .map(SuggestionConverter::GetSuggestionResultDTO)
                 .collect(Collectors.toList());
     }
+
+    public static SuggestionResponseDTO.GetSuggestionAdminsDTO GetSuggestionAdminsResultDTO(Student student) {
+        return SuggestionResponseDTO.GetSuggestionAdminsDTO.builder()
+                .adminId()
+                .build()
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java
index 51de446..5a4772f 100644
--- a/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/suggestion/dto/SuggestionResponseDTO.java
@@ -1,6 +1,5 @@
     package com.assu.server.domain.suggestion.dto;
 
-    import com.assu.server.domain.admin.entity.Admin;
     import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
     import com.assu.server.domain.user.entity.enums.Major;
     import lombok.AllArgsConstructor;
@@ -18,10 +17,9 @@ public class SuggestionResponseDTO {
         @Builder
         public static class WriteSuggestionResponseDTO {
             private Long suggestionId; // 제안 번호
-            private Long memberId; // 제안인 아이디
-            private Long studentNumber; // 제안인 학번
-            private Long suggestionSubjectId; // 건의 대상 아이디
-            private String suggestionStore; // 희망 가게 이름
+            private Long userId; // 제안인 아이디
+            private Long adminId; // 건의 대상 아이디
+            private String storeName; // 희망 가게 이름
             private String suggestionBenefit; // 희망 혜택
         }
 
@@ -32,9 +30,23 @@ public static class WriteSuggestionResponseDTO {
         public static class GetSuggestionResponseDTO {
             private Long suggestionId;
             private LocalDateTime createdAt;
+            private String storeName;
             private String content;
             private Major studentMajor;
             private Long studentNumber;
             private EnrollmentStatus enrollmentStatus;
         }
+
+        @Getter
+        @NoArgsConstructor
+        @AllArgsConstructor
+        @Builder
+        public static class GetSuggestionAdminsDTO {
+            private Long adminId;
+            private String adminName;
+            private Long departId;
+            private String departName;
+            private Long majorId;
+            private String majorName;
+        }
     }
diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java
index 716ccd3..d5b9365 100644
--- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java
+++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionService.java
@@ -1,12 +1,8 @@
 package com.assu.server.domain.suggestion.service;
 
-import com.assu.server.domain.partnership.dto.PartnershipRequestDTO;
-import com.assu.server.domain.partnership.dto.PartnershipResponseDTO;
 import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO;
 import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO;
-import com.assu.server.domain.suggestion.entity.Suggestion;
 import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
 
 import java.util.List;
 
@@ -19,4 +15,5 @@ SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(
 
     List getSuggestions(Long adminId);
 
+    SuggestionResponseDTO.GetSuggestionAdminsDTO getSuggestionAdmins(Long userId);
 }
diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
index 1508dc5..2a345ff 100644
--- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
@@ -2,14 +2,6 @@
 
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
-import com.assu.server.domain.partnership.converter.PartnershipConverter;
-import com.assu.server.domain.partnership.dto.PartnershipResponseDTO;
-import com.assu.server.domain.partnership.entity.Goods;
-import com.assu.server.domain.partnership.entity.Paper;
-import com.assu.server.domain.partnership.entity.PaperContent;
-import com.assu.server.domain.partnership.repository.PaperContentRepository;
-import com.assu.server.domain.partnership.repository.PaperRepository;
-import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.suggestion.converter.SuggestionConverter;
 import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO;
 import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO;
@@ -22,7 +14,6 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
-import java.util.Collections;
 import java.util.List;
 
 @Service
@@ -55,4 +46,23 @@ public List getSuggestions(Long
 
         return SuggestionConverter.toGetSuggestionDTOList(list);
     }
+
+    @Override
+    public SuggestionResponseDTO.GetSuggestionAdminsDTO getSuggestionAdmins(Long userId) {
+
+        Student student = studentRepository.findById(userId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
+
+        List adminList = adminRepository.findMatchingAdmins(
+                student.getUniversity().toString(),
+                student.getDepartment().toString(),
+                student.getMajor()
+        );
+
+        Admin universityAdmin = null;
+        Admin departmentAdmin = null;
+        Admin majorAdmin = null;
+
+        return null;
+    }
 }

From 72b30f4c6fa00ad20cc08dac9ed4c5b8fa2a295a Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Tue, 9 Sep 2025 20:15:21 +0900
Subject: [PATCH 162/270] =?UTF-8?q?[FIX/#82]=20admin=20=ED=9A=8C=EC=9B=90?=
 =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20certifica?=
 =?UTF-8?q?tion=20dto=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/repository/AdminRepository.java     |  2 ++
 .../auth/controller/AuthController.java       |  3 ++
 .../controller/CertificationController.java   | 17 +++++++++-
 .../converter/CertificationConverter.java     |  4 +--
 .../dto/CertificationRequestDTO.java          |  8 ++---
 .../dto/CertificationResponseDTO.java         |  1 -
 .../service/CertificationServiceImpl.java     |  8 ++---
 .../user/controller/StudentController.java    | 31 ++++++++++++++-----
 8 files changed, 55 insertions(+), 19 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
index 45010c8..2a43867 100644
--- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
+++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
@@ -81,4 +81,6 @@ List searchPartneredByName(
     );
 
     Long member(Member member);
+
+	Optional findById(Long id);
 }
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 385731e..9389c35 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
@@ -172,6 +172,9 @@ public BaseResponse signupPartner(
                     "  - `request` (JSON, required): `AdminSignUpRequest` 객체\n" +
                     "  - `email` (String, required): 이메일 주소\n" +
                     "  - `password` (String, required): 비밀번호\n" +
+                    "  - `university` (String, required): 대학교 Enum\n" +
+                    "  - `department` (String, required): 단과대 Enum\n" +
+                    "  - `major` (String, required): 전공 Enum\n" +
                     "  - `phoneNumber` (String, required): 휴대폰 번호\n" +
                     "  - `name` (String, required): 관리자 이름\n" +
                     "  - `department` (String, required): 소속 부서\n" +
diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
index 6b2ddef..3d26da6 100644
--- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
+++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
@@ -34,7 +34,22 @@ public class CertificationController {
 	private final MemberRepository memberRepository; // 지금은 그냥 임시 데이터 하드 코딩이라 여기에 둔거여
 
 	@PostMapping("/certification/session")
-	@Operation(summary = "세션 정보를 요청하는 api", description = "인원 수 기준이 요구되는 제휴일 때 세션을 만들고, 대표자 QR에 담을 정보를 요청하는 api 입니다.")
+	@Operation(
+		summary = "세션 정보 요청 API",
+		description = "# [v1.0 (2025-09-09)](https://www.notion.so/22b1197c19ed80bb8484d99cc6ce715b?source=copy_link)\n" +
+			"- `multipart/form-data`로 호출합니다.\n" +
+			"- 파트: `payload`(JSON, CertificationRequest.groupRequest)\n" +
+			"- 처리: 정보 바탕으로 sessionManager에 session생성\n" +
+			"- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+			"\n**Request Parts:**\n" +
+			"  - `request` (JSON, required): `CertificationRequest.groupRequest` 객체\n" +
+			"  - `people` (Integer, required): 인증이 필요한 인원\n" +
+			"  - `storeId` (Long, required): 스토어 id\n"+
+			"  - `adminId` (Long, required): 관리자 id\n"+
+			"  - `tableNumber` (Integer, required): 테이블 넘버\n"+
+			"\n**Response:**\n" +
+			"  - 성공 시 201(Created)와 sessionId, adminId 반환"
+	)
 	public ResponseEntity> getSessionId(
 		@AuthenticationPrincipal PrincipalDetails pd,
 		@RequestBody CertificationRequestDTO.groupRequest dto
diff --git a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java
index bbc0fb7..0e4c812 100644
--- a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java
+++ b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java
@@ -19,9 +19,9 @@ public static AssociateCertification toAssociateCertification(CertificationReque
 			.build();
 	}
 
-	public static CertificationResponseDTO.getSessionIdResponse toSessionIdResponse(Long sessionId, Long adminId){
+	public static CertificationResponseDTO.getSessionIdResponse toSessionIdResponse(Long sessionId){
 		return CertificationResponseDTO.getSessionIdResponse.builder()
-			.sessionId(sessionId).adminId(adminId)
+			.sessionId(sessionId)
 			.build();
 	}
 
diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java
index 7e3d2fa..971f337 100644
--- a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java
@@ -8,15 +8,15 @@ public class CertificationRequestDTO {
 	@Getter
 	public static class groupRequest{
 		Integer people;
-		String storeName;
-		String adminName;
+		Long storeId;
+		Long adminId;
 		Integer tableNumber;
 	}
 
 	@Getter
 	public static class personalRequest{
-		String storeName;
-		String adminName;
+		Long storeId;
+		Long adminId;
 		Integer tableNumber;
 	}
 
diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java
index b63966f..f1f0c50 100644
--- a/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationResponseDTO.java
@@ -12,6 +12,5 @@ public class CertificationResponseDTO {
 	@AllArgsConstructor
 	public static class getSessionIdResponse {
 		Long sessionId;
-		Long adminId;
 	}
 }
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 3362258..347299b 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
@@ -57,12 +57,12 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId(
 		Long userId = member.getId();
 
 		// admin id 추출
-		Admin admin = adminRepository.findByName(dto.getAdminName()).orElseThrow(
+		Admin admin = adminRepository.findById(dto.getAdminId()).orElseThrow(
 			() -> new GeneralException(ErrorStatus.NO_SUCH_ADMIN)
 		);
 
 		// store id 추출
-		Store store = storeRepository.findByName(dto.getStoreName()).orElseThrow(
+		Store store = storeRepository.findById(dto.getStoreId()).orElseThrow(
 			() -> new GeneralException(ErrorStatus.NO_SUCH_STORE)
 		);
 
@@ -79,7 +79,7 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId(
 		// 세션 여는 대표자는 제일 먼저 인증
 		sessionManager.addUserToSession(sessionId, userId);
 
-		return CertificationConverter.toSessionIdResponse(sessionId, admin.getId());
+		return CertificationConverter.toSessionIdResponse(sessionId);
 
 	}
 
@@ -154,7 +154,7 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto,
 	@Override
 	public void certificatePersonal(CertificationRequestDTO.personalRequest dto, Member member){
 		// store id 추출
-		Store store = storeRepository.findByName(dto.getStoreName()).orElseThrow(
+		Store store = storeRepository.findById(dto.getStoreId()).orElseThrow(
 			() -> new GeneralException(ErrorStatus.NO_SUCH_STORE)
 		);
 
diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
index 4cb83ad..d50db89 100644
--- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java
+++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
@@ -22,13 +22,26 @@
 @RestController
 @Tag(name = "유저 관련 api", description = "유저와 관련된 로직을 처리하는 api")
 @RequiredArgsConstructor
-@RequestMapping("/user")
+@RequestMapping("/students")
 public class StudentController {
 
 	private final StudentService studentService;
 
-	@GetMapping("/partnership/{year}/{month}")
-	@Operation(summary = "유저의 제휴 내역을 조회", description = "건수 및 금액으로 조회")
+	@GetMapping("/partnerships/{year}/{month}")
+	@Operation(
+		summary = "월별 제휴 사용내역 조회 API",
+		description = "# [v1.0 (2025-09-09)](https://www.notion.so/_-2241197c19ed8134bd49d8841e841634?source=copy_link)\n" +
+			"- `multipart/form-data`로 호출합니다.\n" +
+			"- 처리: 정보 바탕으로 sessionManager에 session생성\n" +
+			"- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+			"\n**Request Parts:**\n" +
+			"  - `storeId` (Long, required): 스토어 id\n" +
+			"  - `year` (Integer, required): 년도\n" +
+			"  - `month` (Long, required): 월\n"+
+			"\n**Response:**\n" +
+			"  - 성공 시 partnership Usage 내역 반환 \n"+
+			"  - 해당 월에 사용한 제휴 수 반환"
+	)
 	public ResponseEntity> getMyPartnership(
 		@PathVariable int year, @PathVariable int month, @AuthenticationPrincipal PrincipalDetails pd
 	){
@@ -40,10 +53,14 @@ public ResponseEntity> getMyPartn
 
 
 
-    @Operation(
-            summary = "스탬프 조회 API",
-            description = "Authorization 후에 사용해주세요."
-    )
+	@Operation(
+		summary = "사용자 stamp 개수 조회 API",
+		description = "# [v1.0 (2025-09-09)](https://www.notion.so/_-2241197c19ed8134bd49d8841e841634?source=copy_link)\n" +
+			"- `multipart/form-data`로 호출합니다.\n" +
+			"- 처리: 정보 바탕으로 sessionManager에 session생성\n" +
+			"\n**Response:**\n" +
+			"  - stamp 개수 반환 \n"
+	)
     @GetMapping("/stamp")
     public BaseResponse getStamp(
             @AuthenticationPrincipal PrincipalDetails pd

From fe336e849d309afe67bdf9f411efac45fa688937 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 10 Sep 2025 00:50:59 +1000
Subject: [PATCH 163/270] =?UTF-8?q?[FEAT/#77]=20-=20=EC=95=8C=EB=A6=BC=20?=
 =?UTF-8?q?=ED=98=84=EC=9E=AC=20=EC=84=A4=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20?=
 =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20-=20=EC=9D=BD=EC=A7=80=20=EC=95=8A?=
 =?UTF-8?q?=EC=9D=80=20=EC=95=8C=EB=A6=BC=20=EC=A1=B4=EC=9E=AC=20=EC=97=AC?=
 =?UTF-8?q?=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/NotificationController.java    | 47 +++++++++++++---
 .../converter/NotificationConverter.java      | 15 +++++
 .../dto/NotificationSettingsResponse.java     | 14 +++++
 .../notification/entity/NotificationType.java |  4 +-
 .../repository/NotificationRepository.java    |  5 +-
 .../NotificationSettingRepository.java        |  3 +-
 .../service/NotificationCommandService.java   |  2 +-
 .../NotificationCommandServiceImpl.java       | 56 +++++++++++++++----
 .../service/NotificationQueryService.java     |  3 +
 .../service/NotificationQueryServiceImpl.java | 44 ++++++++++++++-
 10 files changed, 168 insertions(+), 25 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/notification/dto/NotificationSettingsResponse.java

diff --git a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
index 9077fb2..cf3860b 100644
--- a/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
+++ b/src/main/java/com/assu/server/domain/notification/controller/NotificationController.java
@@ -73,15 +73,46 @@ public BaseResponse queue(@Valid @RequestBody QueueNotificationRequest r
         return BaseResponse.onSuccess(SuccessStatus._OK, "Notification delivery succeeded.");
     }
 
-    @Operation(summary = "알림 유형별 ON/OFF 토글 API",
+    @Operation(
+            summary = "알림 유형별 ON/OFF 토글 API",
             description = "# [v1.0 (2025-09-02)](https://www.notion.so/on-off-2511197c19ed80aeb4eed3c502691361?source=copy_link)\n" +
-                    "- 토글 형식으로 유형별 알림을 ON/OFF 합니다.\n"+
-                    "  - type: Path Variable, NotificationType [CHAT / PARTNER_SUGGESTION / PARTNER_PROPOSAL / ORDER]\n")
+                    "- 토글 형식으로 유형별 알림을 ON/OFF 합니다.\n" +
+                    "  - type: Path Variable, NotificationType \n" +
+                    "  - 지원 값: [CHAT / PARTNER_SUGGESTION / PARTNER_PROPOSAL / ORDER / PARTNER_ALL / ADMIN_ALL]\n" +
+                    "  - PARTNER_ALL: CHAT + ORDER를 함께 토글\n" +
+                    "  - ADMIN_ALL: CHAT + PARTNER_SUGGESTION + PARTNER_PROPOSAL을 함께 토글"
+    )
     @PutMapping("/{type}")
-    public BaseResponse toggle(@AuthenticationPrincipal PrincipalDetails pd,
-                                       @PathVariable("type") NotificationType type) {
-        boolean newValue = command.toggle(pd.getMemberId(), type);
-        return BaseResponse.onSuccess(SuccessStatus._OK,
-                "Notification setting toggled: now enabled=" + newValue);
+    public BaseResponse toggle(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @PathVariable("type") NotificationType type
+    ) {
+        Map settings = command.toggle(pd.getMemberId(), type);
+        return BaseResponse.onSuccess(SuccessStatus._OK, new NotificationSettingsResponse(settings));
+    }
+
+    @Operation(
+            summary = "알림 현재 설정 조회 API",
+            description = "# [v1.0 (2025-09-02)](https://clumsy-seeder-416.notion.site/2691197c19ed80de9b92d96db3608cdf?source=copy_link)\n" +
+                    "- 현재 로그인 사용자의 알림 설정 상태를 반환합니다.\n"
+    )
+    @GetMapping("/settings")
+    public BaseResponse getSettings(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        NotificationSettingsResponse res = query.loadSettings(pd.getMemberId());
+        return BaseResponse.onSuccess(SuccessStatus._OK, res);
+    }
+
+    @Operation(
+            summary = "읽지 않은 알림 존재 여부 조회 API",
+            description = "# [v1.0 (2025-09-02)](https://clumsy-seeder-416.notion.site/2691197c19ed809a81fec6eb3282ec3a?source=copy_link)\n" +
+                    "- 현재 로그인 사용자의 읽지 않은 알림 존재 여부를 반환합니다.\n" +
+                    "- 결과: true | false"
+    )
+    @GetMapping("/unread-exists")
+    public BaseResponse unreadExists(@AuthenticationPrincipal PrincipalDetails pd) {
+        boolean exists = query.hasUnread(pd.getMemberId());
+        return BaseResponse.onSuccess(SuccessStatus._OK, exists);
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java b/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java
index 7dc85cb..898a25a 100644
--- a/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java
+++ b/src/main/java/com/assu/server/domain/notification/converter/NotificationConverter.java
@@ -1,10 +1,17 @@
 package com.assu.server.domain.notification.converter;
 
 import com.assu.server.domain.notification.dto.NotificationResponseDTO;
+import com.assu.server.domain.notification.dto.NotificationSettingsResponse;
 import com.assu.server.domain.notification.entity.Notification;
+import com.assu.server.domain.notification.entity.NotificationSetting;
+import com.assu.server.domain.notification.entity.NotificationType;
 
 import java.time.Duration;
 import java.time.LocalDateTime;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
 public class NotificationConverter {
     public static NotificationResponseDTO toDto(Notification n) {
@@ -41,4 +48,12 @@ public static String toTimeAgo(LocalDateTime createdAt) {
         long days = hours / 24;
         return days + "일 전";
     }
+
+    // 저장 대상: 개별 타입만 (그룹 제외)
+    private static final EnumSet BASE_TYPES = EnumSet.of(
+            NotificationType.CHAT,
+            NotificationType.ORDER,
+            NotificationType.PARTNER_SUGGESTION,
+            NotificationType.PARTNER_PROPOSAL
+    );
 }
diff --git a/src/main/java/com/assu/server/domain/notification/dto/NotificationSettingsResponse.java b/src/main/java/com/assu/server/domain/notification/dto/NotificationSettingsResponse.java
new file mode 100644
index 0000000..1fd4bbd
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/notification/dto/NotificationSettingsResponse.java
@@ -0,0 +1,14 @@
+package com.assu.server.domain.notification.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class NotificationSettingsResponse {
+    private Map settings;
+}
diff --git a/src/main/java/com/assu/server/domain/notification/entity/NotificationType.java b/src/main/java/com/assu/server/domain/notification/entity/NotificationType.java
index d1c79d2..0eceb51 100644
--- a/src/main/java/com/assu/server/domain/notification/entity/NotificationType.java
+++ b/src/main/java/com/assu/server/domain/notification/entity/NotificationType.java
@@ -6,7 +6,9 @@ public enum NotificationType {
     CHAT("chat"),
     PARTNER_SUGGESTION("partner_suggestion"),
     PARTNER_PROPOSAL("partner_proposal"),
-    ORDER("order");
+    ORDER("order"),
+    PARTNER_ALL("partner_all"), // 채팅, 주문 안내
+    ADMIN_ALL("admin_all"); // 채팅, 제휴 건의, 제휴 제안
 
     private final String code;
     NotificationType(String code) { this.code = code; }
diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java
index 1e4933b..3254a14 100644
--- a/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java
+++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationRepository.java
@@ -16,6 +16,7 @@
 import java.util.Optional;
 
 public interface NotificationRepository extends JpaRepository {
-    Page findByReceiverId(Long receiverId, Pageable pageable);
-    Page findByReceiverIdAndIsReadFalse(Long receiverId, Pageable pageable);
+    Page findByReceiverIdAndTypeNot(Long receiverId, NotificationType type, Pageable pageable);
+    Page findByReceiverIdAndIsReadFalseAndTypeNot(Long receiverId, NotificationType type, Pageable pageable);
+    boolean existsByReceiverIdAndIsReadFalseAndTypeNot(Long receiverId, NotificationType type); // ← 핵심
 }
diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java
index 155d789..46144e0 100644
--- a/src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java
+++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationSettingRepository.java
@@ -4,9 +4,10 @@
 import com.assu.server.domain.notification.entity.NotificationType;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.List;
 import java.util.Optional;
 
 public interface NotificationSettingRepository extends JpaRepository {
     Optional findByMemberIdAndType(Long memberId, NotificationType type);
-    boolean existsByMemberIdAndTypeAndEnabledTrue(Long memberId, NotificationType type);
+    List findAllByMemberId(Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
index c02e0c1..bbb3d8d 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandService.java
@@ -11,7 +11,7 @@ public interface NotificationCommandService {
     Notification createAndQueue(Long receiverId, NotificationType type, Long refId, Map ctx);
     void markRead(Long notificationId, Long currentMemberId) throws AccessDeniedException;
     void queue(QueueNotificationRequest req);
-    boolean toggle(Long memberId, NotificationType type);
+    Map toggle(Long memberId, NotificationType type);
     boolean isEnabled(Long memberId, NotificationType type);
 
     void sendChat(Long receiverId, Long roomId, String senderName, String message);
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
index 5b8557d..37895cd 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationCommandServiceImpl.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.notification.service;
 
 
+import com.assu.server.domain.common.enums.UserRole;
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.notification.dto.QueueNotificationRequest;
@@ -17,9 +18,8 @@
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Service;
 
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @Service
 @RequiredArgsConstructor
@@ -129,23 +129,59 @@ public void queue(QueueNotificationRequest req) {
 
     @Transactional
     @Override
-    public boolean toggle(Long memberId, NotificationType type) {
+    public Map toggle(Long memberId, NotificationType type) {
+        Member member = memberRepository.findMemberById(memberId)
+                .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER));
 
-        Member member = memberRepository.findMemberById(memberId).orElseThrow(
-            () -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)
-        );
+        // 그룹 타입 처리 (기존 그대로)
+        if (type == NotificationType.PARTNER_ALL) {
+            toggleSingle(member, NotificationType.CHAT);
+            toggleSingle(member, NotificationType.ORDER);
+        } else if (type == NotificationType.ADMIN_ALL) {
+            toggleSingle(member, NotificationType.CHAT);
+            toggleSingle(member, NotificationType.PARTNER_SUGGESTION);
+            toggleSingle(member, NotificationType.PARTNER_PROPOSAL);
+        } else {
+            toggleSingle(member, type);
+        }
+
+        boolean isAdmin = member.getRole() == UserRole.ADMIN; // Role enum을 쓰는 경우
+
+        // ADMIN: CHAT, PARTNER_SUGGESTION, PARTNER_PROPOSAL
+        // PARTNER: CHAT, ORDER
+        EnumSet visibleTypes = isAdmin
+                ? EnumSet.of(NotificationType.CHAT, NotificationType.PARTNER_SUGGESTION, NotificationType.PARTNER_PROPOSAL)
+                : EnumSet.of(NotificationType.CHAT, NotificationType.ORDER);
+
+        // 기본값 true로 모두 채운 후, DB 값으로 덮어쓰기
+        Map result = new LinkedHashMap<>();
+        for (NotificationType t : visibleTypes) {
+            result.put(t.name(), true); // DB에 없으면 true
+        }
+
+        List rows = notificationSettingRepository.findAllByMemberId(memberId);
+        for (NotificationSetting s : rows) {
+            if (visibleTypes.contains(s.getType())) {
+                result.put(s.getType().name(), Boolean.TRUE.equals(s.getEnabled()));
+            }
+        }
+
+        return result;
+    }
+
+    private boolean toggleSingle(Member member, NotificationType type) {
         NotificationSetting setting = notificationSettingRepository
-                .findByMemberIdAndType(memberId, type)
+                .findByMemberIdAndType(member.getId(), type)
                 .orElse(NotificationSetting.builder()
                         .member(member)
                         .type(type)
                         .enabled(true) // 기본값
                         .build());
 
-        setting.setEnabled(!setting.getEnabled()); // 토글
+        setting.setEnabled(!setting.getEnabled());
         notificationSettingRepository.save(setting);
 
-        return setting.getEnabled(); // 변경된 값 반환
+        return setting.getEnabled();
     }
 
     @Transactional
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java
index 9ff02af..6d7f4e4 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryService.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.notification.service;
 
 import com.assu.server.domain.notification.dto.NotificationResponseDTO;
+import com.assu.server.domain.notification.dto.NotificationSettingsResponse;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 
@@ -8,4 +9,6 @@
 
 public interface NotificationQueryService {
     Map getNotifications(String status, int page, int size, Long memberId);
+    NotificationSettingsResponse loadSettings(Long memberId);
+    boolean hasUnread(Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
index 0cfa082..011d11d 100644
--- a/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/notification/service/NotificationQueryServiceImpl.java
@@ -1,12 +1,19 @@
 package com.assu.server.domain.notification.service;
 
+import com.assu.server.domain.common.enums.UserRole;
+import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.notification.converter.NotificationConverter;
 import com.assu.server.domain.notification.dto.NotificationResponseDTO;
+import com.assu.server.domain.notification.dto.NotificationSettingsResponse;
 import com.assu.server.domain.notification.entity.Notification;
+import com.assu.server.domain.notification.entity.NotificationSetting;
+import com.assu.server.domain.notification.entity.NotificationType;
 import com.assu.server.domain.notification.repository.NotificationRepository;
+import com.assu.server.domain.notification.repository.NotificationSettingRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
+import com.assu.server.global.exception.GeneralException;
 import org.springframework.transaction.annotation.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
@@ -15,8 +22,10 @@
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 
+import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 
 
 @Service
@@ -24,6 +33,7 @@
 public class NotificationQueryServiceImpl implements NotificationQueryService {
     private final NotificationRepository notificationRepository;
     private final MemberRepository memberRepository;
+    private final NotificationSettingRepository notificationSettingRepository;
 
     @Transactional
     @Override
@@ -44,8 +54,8 @@ public Map getNotifications(String status, int page, int size, L
         // 2) 조회
         Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "id"));
         Page rawPage = s.equals("unread")
-                ? notificationRepository.findByReceiverIdAndIsReadFalse(memberId, pageable)
-                : notificationRepository.findByReceiverId(memberId, pageable);
+                ? notificationRepository.findByReceiverIdAndIsReadFalseAndTypeNot(memberId, NotificationType.CHAT, pageable)
+                : notificationRepository.findByReceiverIdAndTypeNot(memberId, NotificationType.CHAT, pageable);
 
         Page p = rawPage.map(NotificationConverter::toDto);
 
@@ -58,4 +68,34 @@ public Map getNotifications(String status, int page, int size, L
         body.put("totalElements", p.getTotalElements());
         return body;
     }
+
+    @Override
+    public NotificationSettingsResponse loadSettings(Long memberId) {
+        Member member = memberRepository.findMemberById(memberId)
+                .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER));
+
+        // 역할별로 노출할 타입 고정
+        Set visible = member.getRole() == UserRole.ADMIN
+                ? EnumSet.of(NotificationType.CHAT, NotificationType.PARTNER_SUGGESTION, NotificationType.PARTNER_PROPOSAL)
+                : EnumSet.of(NotificationType.CHAT, NotificationType.ORDER);
+
+        // 기본 true로 채워두고, DB 값으로 덮어쓰기
+        Map map = new LinkedHashMap<>();
+        for (NotificationType t : visible) map.put(t.name(), true);
+
+        for (NotificationSetting s : notificationSettingRepository.findAllByMemberId(memberId)) {
+            if (visible.contains(s.getType())) {
+                map.put(s.getType().name(), Boolean.TRUE.equals(s.getEnabled()));
+            }
+        }
+        return new NotificationSettingsResponse(map);
+    }
+
+    @Override
+    public boolean hasUnread(Long memberId) {
+        if (!memberRepository.existsById(memberId)) {
+            throw new DatabaseException(ErrorStatus.NO_SUCH_MEMBER);
+        }
+        return notificationRepository.existsByReceiverIdAndIsReadFalseAndTypeNot(memberId, NotificationType.CHAT);
+    }
 }

From 168e96735b69295bf7dc86f8bfca6bb69529f797 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 10 Sep 2025 02:32:35 +0900
Subject: [PATCH 164/270] =?UTF-8?q?[HOTFIX]=20=EB=B3=80=EA=B2=BD=EB=90=9C?=
 =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?=
 =?UTF-8?q?=ED=95=9C=20auth=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/auth/security/jwt/JwtAuthFilter.java | 14 +++++++++++---
 .../server/global/config/SecurityConfig.java    | 17 +++++++++++++----
 2 files changed, 24 insertions(+), 7 deletions(-)

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 d5fbf70..0ad4bd4 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
@@ -48,7 +48,15 @@ public class JwtAuthFilter extends OncePerRequestFilter {
     private static final String[] WHITELIST = {
             "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
             "/swagger-resources/**", "/webjars/**",
-            "/auth/login/**", "/auth/signup/**", "/auth/phone-numbers/**"
+            // Auth (로그아웃 제외)
+            "/auth/phone-verification/send",
+            "/auth/phone-verification/verify",
+            "/auth/students/signup",
+            "/auth/partners/signup",
+            "/auth/admins/signup",
+            "/auth/commons/login",
+            "/auth/students/login",
+            "/auth/students/ssu-verify"
     };
 
     /**
@@ -60,7 +68,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
         String uri = request.getRequestURI();
         if ("OPTIONS".equalsIgnoreCase(request.getMethod()))
             return true; // CORS preflight 우회
-        if (PATH.match("/auth/refresh", uri))
+        if (PATH.match("/auth/tokens/refresh", uri))
             return false; // 토큰 재발급은 필터 적용
         for (String p : WHITELIST)
             if (PATH.match(p, uri))
@@ -98,7 +106,7 @@ protected void doFilterInternal(
         String requestUri = request.getRequestURI();
 
         // ───────── 재발급 경로 처리 (/auth/token/reissue) ─────────
-        if (PATH.match("/auth/refresh", requestUri)) {
+        if (PATH.match("/auth/tokens/refresh", requestUri)) {
             String refreshToken = request.getHeader("refreshToken");
             try {
                 // Bearer 헤더 검증
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 8b22b93..6226353 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -26,13 +26,22 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
                                 "/swagger-resources/**", "/webjars/**"
                         ).permitAll()
+
                         // 로그아웃은 인증 필요
                         .requestMatchers("/auth/logout").authenticated()
-                        // 그 외 Auth 전체 공개
-                        .requestMatchers(
-                                "/auth/**"
+
+                        .requestMatchers(// Auth (로그아웃 제외)
+                                "/auth/phone-verification/send",
+                                "/auth/phone-verification/verify",
+                                "/auth/students/signup",
+                                "/auth/partners/signup",
+                                "/auth/admins/signup",
+                                "/auth/commons/login",
+                                "/auth/students/login",
+                                "/auth/students/ssu-verify"
                         ).permitAll()
-                        // 나머지는 인증 필요
+
+                        // 나머지 요청은 JwtAuthFilter가 화이트리스트/보호자원 판별
                         .anyRequest().authenticated()
                 )
                 .formLogin(form -> form.disable())

From 81672cf682a475b92a38a70f82f6a95cc5a7f458 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 10 Sep 2025 21:54:17 +1000
Subject: [PATCH 165/270] =?UTF-8?q?[FEAT/#77]=20-=20TTS=20=EC=84=A4?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |  1 +
 .../assu/server/infra/firebase/FcmClient.java | 33 ++++++++++---------
 2 files changed, 19 insertions(+), 15 deletions(-)

diff --git a/build.gradle b/build.gradle
index cac628a..44aa4fc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -21,6 +21,7 @@ configurations {
 }
 
 repositories {
+	google()
 	mavenCentral()
 }
 
diff --git a/src/main/java/com/assu/server/infra/firebase/FcmClient.java b/src/main/java/com/assu/server/infra/firebase/FcmClient.java
index bf4e8d1..81fb040 100644
--- a/src/main/java/com/assu/server/infra/firebase/FcmClient.java
+++ b/src/main/java/com/assu/server/infra/firebase/FcmClient.java
@@ -1,11 +1,9 @@
 package com.assu.server.infra.firebase;
 
 import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository;
-import com.assu.server.domain.member.entity.Member;
-import com.assu.server.domain.notification.entity.Notification;
 import com.google.api.core.ApiFuture;
+import com.google.firebase.messaging.AndroidConfig;
 import com.google.firebase.messaging.FirebaseMessaging;
-import com.google.firebase.messaging.FirebaseMessagingException;
 import com.google.firebase.messaging.Message;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -24,7 +22,7 @@ public class FcmClient {
     private final FirebaseMessaging messaging;
     private final DeviceTokenRepository tokenRepo;
 
-    // 전송 타임아웃 (필요 시 2~5초로 조정)
+    // 전송 타임아웃
     private static final Duration SEND_TIMEOUT = Duration.ofSeconds(3);
 
     public void sendToMemberId(Long memberId, String title, String body, Map data) {
@@ -36,24 +34,28 @@ public void sendToMemberId(Long memberId, String title, String body, Map tokens = tokenRepo.findActiveTokensByMemberId(memberId);
         if (tokens.isEmpty()) return;
 
-        // 2) 데이터 안전하게 추출
+        // 2) 널 세이프 처리
         final String _title = title == null ? "" : title;
         final String _body  = body  == null ? "" : body;
 
-        String type  = data != null && data.get("type") != null ? data.get("type") : "";
-        String refId = data != null && data.get("refId") != null ? data.get("refId") : "";
-        String deeplink = data != null && data.get("deeplink") != null ? data.get("deeplink") : "";
-        String notificationId = data != null && data.get("notificationId") != null ? data.get("notificationId") : "";
+        String type           = (data != null && data.get("type") != null) ? data.get("type") : "";
+        String refId          = (data != null && data.get("refId") != null) ? data.get("refId") : "";
+        String deeplink       = (data != null && data.get("deeplink") != null) ? data.get("deeplink") : "";
+        String notificationId = (data != null && data.get("notificationId") != null) ? data.get("notificationId") : "";
 
-        // 3) 각 토큰에 FCM 전송
+        // 3) 각 토큰에 FCM 전송 (data-only + HIGH)
         for (String token : tokens) {
             Message msg = Message.builder()
                     .setToken(token)
-                    .setNotification(com.google.firebase.messaging.Notification.builder()
-                            .setTitle(_title)
-                            .setBody(_body)
+                    .setAndroidConfig(AndroidConfig.builder()
+                            .setPriority(AndroidConfig.Priority.HIGH)
+                            // 필요 시 TTL 등 추가 가능
+                            // .setTtl(10_000L) // 10초
                             .build())
-                    .putData("type", type)
+                    // --- data-only payload ---
+                    .putData("title", _title)
+                    .putData("body",  _body)
+                    .putData("type",  type)
                     .putData("refId", refId)
                     .putData("deeplink", deeplink)
                     .putData("notificationId", notificationId)
@@ -61,7 +63,8 @@ public void sendToMemberId(Long memberId, String title, String body, Map future = messaging.sendAsync(msg);
-                future.get(SEND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+                String messageId = future.get(SEND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+                log.debug("[FCM] sent messageId={} memberId={}", messageId, memberId);
             } catch (TimeoutException te) {
                 log.warn("[FCM] timeout ({} ms) memberId={}", SEND_TIMEOUT.toMillis(), memberId);
                 throw new RuntimeException("FCM timeout", te);

From a73c5025bac3e23a88e094a82862f792be7ef2b1 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Wed, 10 Sep 2025 21:04:13 +0900
Subject: [PATCH 166/270] =?UTF-8?q?[FIX/#82]=20=EC=9B=B9=EC=86=8C=EC=BC=93?=
 =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD=20=EC=9D=B8=EC=A6=9D?=
 =?UTF-8?q?=20=EA=B3=BC=EC=A0=95=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../config/CertifyWebSocketConfig.java        |  2 +-
 .../dto/CertificationProgressResponseDTO.java | 49 +++++++++++++++++++
 .../certification/dto/CurrentProgress.java    | 35 -------------
 .../service/CertificationServiceImpl.java     | 27 ++--------
 .../converter/PartnershipConverter.java       |  1 +
 .../dto/PaperContentResponseDTO.java          |  1 +
 .../server/global/config/SecurityConfig.java  |  1 +
 7 files changed, 56 insertions(+), 60 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java
 delete mode 100644 src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java

diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 7d1ce5b..75c9ae3 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -24,7 +24,7 @@ public void configureMessageBroker(MessageBrokerRegistry config) {
 	@Override
 	public void registerStompEndpoints(StompEndpointRegistry registry) {
 		registry.addEndpoint("/ws")           // 클라이언트 WebSocket 연결 주소
-			.setAllowedOriginPatterns("*").withSockJS(); // CORS 허용
+			.setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
 	}
 
 	@Override
diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java
new file mode 100644
index 0000000..4221fe9
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java
@@ -0,0 +1,49 @@
+package com.assu.server.domain.certification.dto;
+
+import java.util.List;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+// public class CurrentProgress {
+// 	private int count;
+//
+//
+// 	@Getter
+// 	public static class CertificationNumber{
+// 		public CertificationNumber(int count){
+// 			this.count= count;
+// 		}
+//
+// 		int count;
+// 	}
+//
+// 	@Getter
+// 	public static class CompletedNotification{
+// 		public CompletedNotification(String message, List userIds){
+//
+// 			this.message= message;
+// 			this.userIds= userIds;
+// 		}
+// 		String message;
+// 		List userIds;
+// 	}
+
+// }
+@Getter
+@AllArgsConstructor
+public class CertificationProgressResponseDTO {
+	private String type;
+	private Integer count;
+	private String message;
+	private List userIds;
+
+	// 생성자들
+	public static CertificationProgressResponseDTO progress(int count) {
+		return new CertificationProgressResponseDTO("progress", count, null, null);
+	}
+
+	public static CertificationProgressResponseDTO completed(String message, List userIds) {
+		return new CertificationProgressResponseDTO("completed", userIds.size(), message, userIds);
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java b/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java
deleted file mode 100644
index 919e946..0000000
--- a/src/main/java/com/assu/server/domain/certification/dto/CurrentProgress.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.assu.server.domain.certification.dto;
-
-import java.util.List;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-@Getter
-@AllArgsConstructor
-public class CurrentProgress {
-	private int count;
-
-
-	@Getter
-	public static class CertificationNumber{
-		public CertificationNumber(int count){
-
-			this.count= count;
-		}
-		int count;
-	}
-
-	@Getter
-	public static class CompletedNotification{
-		public CompletedNotification(String message, List userIds){
-
-			this.message= message;
-			this.userIds= userIds;
-		}
-		String message;
-		List userIds;
-	}
-
-}
\ No newline at end of file
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 347299b..0092308 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
@@ -1,7 +1,6 @@
 package com.assu.server.domain.certification.service;
 
 import java.time.Duration;
-import java.time.LocalDateTime;
 import java.util.List;
 
 
@@ -13,18 +12,16 @@
 import com.assu.server.domain.certification.SessionTimeoutManager;
 import com.assu.server.domain.certification.component.CertificationSessionManager;
 import com.assu.server.domain.certification.converter.CertificationConverter;
+import com.assu.server.domain.certification.dto.CertificationProgressResponseDTO;
 import com.assu.server.domain.certification.dto.CertificationRequestDTO;
 import com.assu.server.domain.certification.dto.CertificationResponseDTO;
-import com.assu.server.domain.certification.dto.CurrentProgress;
 import com.assu.server.domain.certification.entity.AssociateCertification;
-import com.assu.server.domain.certification.entity.QRCertification;
 import com.assu.server.domain.certification.entity.enums.SessionStatus;
 import com.assu.server.domain.certification.repository.AssociateCertificationRepository;
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.store.entity.Store;
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.domain.user.entity.Student;
-import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.GeneralException;
 import jakarta.transaction.Transactional;
@@ -39,7 +36,6 @@ public class CertificationServiceImpl implements CertificationService {
 	private final AdminRepository adminRepository;
 	private final StoreRepository storeRepository;
 	private final AssociateCertificationRepository associateCertificationRepository;
-	private final StudentRepository studentRepository;
 
 	// 세션 메니저
 	private final CertificationSessionManager sessionManager;
@@ -117,19 +113,6 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto,
 		sessionManager.addUserToSession(sessionId, userId);
 		int currentCertifiedNumber = sessionManager.getCurrentUserCount(sessionId);
 
-		// messagingTemplate.convertAndSend("/certification/progress/"+sessionId,
-		// 	new CurrentProgress.CertificationNumber(currentCertifiedNumber));
-		//
-		// if(currentCertifiedNumber >= session.getPeopleNumber()){
-		// 	session.setIsCertified(true);
-		// 	session.setStatus(SessionStatus.COMPLETED);
-		// 	associateCertificationRepository.save(session);
-		//
-		//
-		// 	messagingTemplate.convertAndSend("/certification/progress/"+sessionId,
-		// 		new CurrentProgress.CompletedNotification("인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId))
-		// 	);
-		// }
 		if(currentCertifiedNumber >= session.getPeopleNumber()){
 			session.setIsCertified(true);
 			session.setStatus(SessionStatus.COMPLETED);
@@ -137,14 +120,10 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto,
 
 			// 완료 알림에 현재 인원수도 포함
 			messagingTemplate.convertAndSend("/certification/progress/" + sessionId,
-				new CurrentProgress.CompletedNotification(
-					"인증이 완료되었습니다.",
-					sessionManager.snapshotUserIds(sessionId)
-				)
-			);
+				new CertificationProgressResponseDTO("completed", currentCertifiedNumber, "인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId)));
 		} else {
 			messagingTemplate.convertAndSend("/certification/progress/" + sessionId,
-				new CurrentProgress.CertificationNumber(currentCertifiedNumber));
+				new CertificationProgressResponseDTO("progress", currentCertifiedNumber, null, null));
 		}
 
 
diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index c8b8fb1..51be039 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -116,6 +116,7 @@ public static PaperContentResponseDTO.storePaperContentResponse toContentRespons
 		String paperContentText = buildPaperContentText(content, goodsList, peopleValue);
 
 		return PaperContentResponseDTO.storePaperContentResponse.builder()
+			.adminId(content.getPaper().getAdmin().getId())
 			.adminName(content.getPaper().getAdmin().getName())
 			.cost(content.getCost())
 			.paperContent(paperContentText)
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java
index 3aff7d4..46dd325 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PaperContentResponseDTO.java
@@ -13,6 +13,7 @@ public class PaperContentResponseDTO {
 	@NoArgsConstructor
 	@AllArgsConstructor
 	public static class storePaperContentResponse{
+		Long adminId;
 		String adminName;
 		String paperContent;
 		Long contentId;
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 8b22b93..a543700 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -32,6 +32,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                         .requestMatchers(
                                 "/auth/**"
                         ).permitAll()
+                    .requestMatchers("/ws/**").permitAll()
                         // 나머지는 인증 필요
                         .anyRequest().authenticated()
                 )

From 8a58d3aca3e4ade64441b0a2c72e1ed2d4603492 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Fri, 12 Sep 2025 20:23:33 +0900
Subject: [PATCH 167/270] =?UTF-8?q?refactor/#38=20-=20chatting=20Controlle?=
 =?UTF-8?q?r=20=EC=88=98=EC=A0=95=20-=20=EC=B1=84=ED=8C=85=20api=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/chat/config/WebSocketConfig.java   | 11 ++-
 .../chat/controller/ChatController.java       |  5 +-
 .../domain/chat/converter/ChatConverter.java  |  2 +
 .../domain/chat/dto/ChatResponseDTO.java      | 12 ++-
 .../domain/chat/service/ChatServiceImpl.java  | 28 ++++++-
 .../server/global/config/SecurityConfig.java  |  3 +
 src/main/resources/application.yml            | 15 +++-
 .../assu/server/ServerApplicationTests.java   | 82 ++++++++++++-------
 src/test/resources/application-test.yml       | 19 ++++-
 9 files changed, 137 insertions(+), 40 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
index 0f9920b..cdb02cb 100644
--- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
@@ -11,8 +11,17 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
     @Override
     public void registerStompEndpoints(StompEndpointRegistry registry) {
         registry.addEndpoint("/ws/chat")  // 클라이언트 WebSocket 연결 지점
-                .setAllowedOriginPatterns("http://localhost:63342")
+                .setAllowedOriginPatterns(
+                        "http://localhost:63342",
+                        "http://localhost:5173",     // Vite 기본
+                        "http://localhost:3000",     // CRA/Next 기본
+                        "http://127.0.0.1:*",
+                        "http://192.168.*.*:*")       // 같은 LAN의 실제 기기 테스트용
                 .withSockJS();             // fallback for old browsers
+
+        // ✅ 모바일/안드로이드용 (네이티브 WebSocket)
+        registry.addEndpoint("/ws/chat-native")
+                .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅
     }
 
     @Override
diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
index d09bcff..d7893a9 100644
--- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
+++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java
@@ -7,6 +7,7 @@
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.messaging.handler.annotation.MessageMapping;
 import org.springframework.messaging.handler.annotation.Payload;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -16,6 +17,7 @@
 
 import java.util.List;
 
+@Slf4j
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/chat")
@@ -59,8 +61,9 @@ public BaseResponse>
     )
     @MessageMapping("/send")
     public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) {
+        log.info("[WS] handleMessage IN: {}", request);   // ★ 호출 여부 확인
         ChatResponseDTO.SendMessageResponseDTO response = chatService.handleMessage(request);
-
+        log.info("[WS] handleMessage SAVED id={}", response.messageId()); // 저장 확인용
         simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), response);
     }
 
diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
index 0f35123..70fe10c 100644
--- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
+++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
@@ -65,8 +65,10 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me
         return ChatResponseDTO.SendMessageResponseDTO.builder()
                 .roomId(message.getChattingRoom().getId())
                 .senderId(message.getSender().getId())
+                .receiverId(message.getReceiver().getId())
                 .message(message.getMessage())
                 .sentAt(message.getCreatedAt())
+                .messageType(message.getType())
                 .build();
     }
 
diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
index 1d5ac80..29bbaa1 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java
@@ -1,5 +1,8 @@
 package com.assu.server.domain.chat.dto;
 
+import com.assu.server.domain.chat.entity.enums.MessageType;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.google.protobuf.Enum;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
@@ -23,16 +26,23 @@ public static class CreateChatRoomResponseDTO {
     // 메시지 전송
     @Builder
     public record SendMessageResponseDTO(
+        Long messageId,
         Long roomId,
         Long senderId,
+        Long receiverId,
         String message,
+        MessageType messageType,
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
         LocalDateTime sentAt
     ) {}
 
     // 메시지 읽음 처리
     public record ReadMessageResponseDTO(
         Long roomId,
-        int readCount
+        Long readerId,
+        List readMessagesId,
+        int readCount,
+        boolean isRead
     ) {}
 
     // 채팅방 들어갔을 때 조회
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index 379fec6..e4ef31b 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -22,9 +22,15 @@
 import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
 import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+@Slf4j
 @Service
 @RequiredArgsConstructor
 public class ChatServiceImpl implements ChatService {
@@ -76,6 +82,7 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C
     }
 
     @Override
+    @Transactional
     public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) {
         // 유효성 검사
         ChattingRoom room = chatRepository.findById(request.roomId())
@@ -86,9 +93,17 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
 
         Message message = ChatConverter.toMessageEntity(request, room, sender, receiver);
-        messageRepository.save(message);
-
-        return ChatConverter.toSendMessageDTO(message);
+//        messageRepository.save(message);
+        log.info("saved message start");
+        Message saved = messageRepository.saveAndFlush(message);
+        log.info("saved message middle");
+        log.info("saved message id={}, roomId={}, senderId={}, receiverId={}",
+                saved.getId(), room.getId(), sender.getId(), receiver.getId());
+
+        log.info("saved message end");
+        boolean exists = messageRepository.existsById(saved.getId());
+        log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제
+        return ChatConverter.toSendMessageDTO(saved);
     }
 
     @Transactional
@@ -96,10 +111,15 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM
     public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId) {
 
         List unreadMessages = messageRepository.findUnreadMessagesByRoomAndReceiver(roomId, memberId);
+        List readMessagesIdList = new ArrayList<>();
 
+        for(Message unreadMessage : unreadMessages) {
+            readMessagesIdList.add(unreadMessage.getId());
+        }
         unreadMessages.forEach(Message::markAsRead);
 
-        return new ChatResponseDTO.ReadMessageResponseDTO(roomId, unreadMessages.size());
+
+        return new ChatResponseDTO.ReadMessageResponseDTO(roomId, memberId,readMessagesIdList, unreadMessages.size(), true);
     }
 
     @Override
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 8b22b93..276f25a 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -21,6 +21,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                 .authorizeHttpRequests(auth -> auth
                         .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
 
+                        // ✅ WebSocket 핸드셰이크 허용 (네이티브 + SockJS 모두 포함)
+                        .requestMatchers("/ws/**").permitAll()
+
                         // Swagger 등 공개 리소스
                         .requestMatchers(
                                 "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index c76f75b..bd32da9 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -17,8 +17,21 @@ spring:
           time_zone: Asia/Seoul
         show_sql: true
         highlight_sql : true
+  lifecycle:
+    timeout-per-shutdown-phase: 30s
+    rabbitmq:
+      listener:
+        simple:
+          acknowledge-mode: manual
+          prefetch: 20
+          concurrency: 1
+          max-concurrency: 4
+          default-requeue-rejected: false
 
 logging:
   level:
     org.springframework.web: DEBUG
-    org.springframework.web.client.DefaultRestClient: OFF
\ No newline at end of file
+    org.springframework.web.client.DefaultRestClient: OFF
+
+server:
+  shutdown: graceful
\ No newline at end of file
diff --git a/src/test/java/com/assu/server/ServerApplicationTests.java b/src/test/java/com/assu/server/ServerApplicationTests.java
index 5faba62..267381e 100644
--- a/src/test/java/com/assu/server/ServerApplicationTests.java
+++ b/src/test/java/com/assu/server/ServerApplicationTests.java
@@ -1,10 +1,13 @@
 package com.assu.server;
 
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
+import com.google.api.client.http.javanet.ConnectionFactory;
 import com.google.firebase.messaging.FirebaseMessaging;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.context.TestConfiguration;
 import org.springframework.context.annotation.Bean;
@@ -12,45 +15,62 @@
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
 
 @SpringBootTest
 @ActiveProfiles("test")
 class ServerApplicationTests {
 
-	@Mock
-	private FirebaseMessaging firebaseMessaging;
+    @Mock
+    private FirebaseMessaging firebaseMessaging;
 
-	@TestConfiguration
-	static class MockConfig {
-		@Bean
-		FirebaseMessaging firebaseMessaging() {
-			return Mockito.mock(FirebaseMessaging.class);
-		}
+    @MockitoBean
+    private ConnectionFactory connectionFactory;
 
-		@Bean
-		RedisConnectionFactory redisConnectionFactory() {
-			return Mockito.mock(RedisConnectionFactory.class);
-		}
+    @MockitoBean private RabbitTemplate rabbitTemplate;
 
-		@Bean
+
+    @TestConfiguration
+    static class MockConfig {
+        @Bean
+        FirebaseMessaging firebaseMessaging() {
+            return Mockito.mock(FirebaseMessaging.class);
+        }
+
+        @Bean
+        RedisConnectionFactory redisConnectionFactory() {
+            return Mockito.mock(RedisConnectionFactory.class);
+        }
+
+        @Bean
         @SuppressWarnings("unchecked")
-		RedisTemplate redisTemplate() {
-			return Mockito.mock(RedisTemplate.class);
-		}
-
-		@Bean
-		StringRedisTemplate stringRedisTemplate() {
-			return Mockito.mock(StringRedisTemplate.class);
-		}
-
-		@Bean
-		JwtUtil jwtUtil() {
-			return Mockito.mock(JwtUtil.class);
-		}
-	}
-
-	@Test
-	void contextLoads() {
-	}
+        RedisTemplate redisTemplate() {
+            return Mockito.mock(RedisTemplate.class);
+        }
+
+        @Bean
+        StringRedisTemplate stringRedisTemplate() {
+            return Mockito.mock(StringRedisTemplate.class);
+        }
+
+        @Bean
+        JwtUtil jwtUtil() {
+            return Mockito.mock(JwtUtil.class);
+        }
+
+        @Bean(name = "rabbitListenerContainerFactory")
+        RabbitListenerContainerFactory rabbitListenerContainerFactory() {
+            var factory = Mockito.mock(RabbitListenerContainerFactory.class);
+            var container = Mockito.mock(org.springframework.amqp.rabbit.listener.MessageListenerContainer.class);
+            Mockito.when(factory.createListenerContainer(Mockito.any()))
+                    .thenReturn(container);
+            return factory;
+        }
+
+    }
+
+    @Test
+    void contextLoads() {
+    }
 
 }
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 26e940b..8a3197b 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -3,11 +3,16 @@ spring:
     exclude:
       - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
       - org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
+      - org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
   datasource:
     url: jdbc:h2:mem:testdb
     driver-class-name: org.h2.Driver
     username: sa
     password:
+  rabbitmq:
+    listener:
+      simple:
+        auto-startup: false
 
 jwt:
   header: Authorization
@@ -21,6 +26,12 @@ assu:
     school-crypto:
       base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=" #"dummy-base64-key"를 Base64로 인코딩한 값
 
+  messaging:
+    rabbit:
+      enabled: false
+    push:
+      enabled: false
+
 cloud:
   aws:
     s3:
@@ -43,4 +54,10 @@ kakao:
 aligo:
   key: dummy-aligo-key
   user-id: dummy-user-id
-  sender: 01012345678
\ No newline at end of file
+  sender: 01012345678
+
+rabbitmq:
+  host: dummy-host
+  port: 1234
+  username: rabbit-username
+  password: rabbit-password

From 7fd4d8737cefb01de8e4d85801d231053e8fbf9b Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Sun, 14 Sep 2025 00:50:48 +0900
Subject: [PATCH 168/270] =?UTF-8?q?[REFACTOR/#89]=20=ED=95=99=EC=83=9D=20?=
 =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=8B=9C=EC=97=90=20Dep?=
 =?UTF-8?q?artment=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Major 와 Department 매핑
- signupSsuStudent 메소드에 적용
---
 .../domain/auth/service/AuthService.java      |  4 ----
 .../domain/auth/service/AuthServiceImpl.java  |  9 -------
 .../auth/service/SignUpServiceImpl.java       |  1 +
 .../domain/user/entity/enums/Major.java       | 24 +++++++++++++------
 4 files changed, 18 insertions(+), 20 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/auth/service/AuthService.java
 delete mode 100644 src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java

diff --git a/src/main/java/com/assu/server/domain/auth/service/AuthService.java b/src/main/java/com/assu/server/domain/auth/service/AuthService.java
deleted file mode 100644
index cc98034..0000000
--- a/src/main/java/com/assu/server/domain/auth/service/AuthService.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.auth.service;
-
-public interface AuthService {
-}
diff --git a/src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java
deleted file mode 100644
index 70f844c..0000000
--- a/src/main/java/com/assu/server/domain/auth/service/AuthServiceImpl.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.assu.server.domain.auth.service;
-
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class AuthServiceImpl implements AuthService {
-}
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 eec2df2..9ceb1b0 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
@@ -103,6 +103,7 @@ public SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req) {
         Student student = Student.builder()
                 .member(member)
                 .name(authResponse.getName())
+                .department(authResponse.getMajor().getDepartment())
                 .major(authResponse.getMajor())
                 .enrollmentStatus(parseEnrollmentStatus(authResponse.getEnrollmentStatus()))
                 .yearSemester(authResponse.getYearSemester())
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
index 611757d..9d6f197 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
@@ -1,11 +1,21 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum Major {
-    SW, // 소프트웨어학부
-    GM, // 글로벌미디어학과
-    COM, // 컴퓨터학부
-    EE, // 전자정보공학부
-    IP, // 정보보호학과
-    AI, // AI융합학과
-    MB // 미디어경영학과
+    SW(Department.IT), // 소프트웨어학부
+    GM(Department.IT), // 글로벌미디어학과
+    COM(Department.IT), // 컴퓨터학부
+    EE(Department.IT), // 전자정보공학부
+    IP(Department.IT), // 정보보호학과
+    AI(Department.IT), // AI융합학과
+    MB(Department.IT); // 미디어경영학과
+
+    private final Department department;
+
+    Major(Department department) {
+        this.department = department;
+    }
+
+    public Department getDepartment() {
+        return department;
+    }
 }

From bbfedb857c85954fb1a226374e3ccd0241752b3f Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Sun, 14 Sep 2025 03:09:24 +0900
Subject: [PATCH 169/270] =?UTF-8?q?[FEAT/#92]=20=ED=9A=8C=EC=9B=90?=
 =?UTF-8?q?=ED=83=88=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 소프트 삭제 방식 (deletedAt 필드)
- 탈퇴 회원 재로그인 시 자동 복구
- 회원탈퇴 API 엔드포인트 추가
- 토큰 무효화 및 보안 처리
---
 .../auth/controller/AuthController.java       | 24 ++++++++++++++
 .../security/adapter/CommonAuthAdapter.java   | 33 ++++++++++++++-----
 .../auth/security/adapter/SSUAuthAdapter.java | 18 +++++++---
 .../member/controller/MemberController.java   |  4 ---
 .../member/converter/MemberConverter.java     |  4 ---
 .../domain/member/dto/MemberRequestDTO.java   |  4 ---
 .../domain/member/dto/MemberResponseDTO.java  |  4 ---
 .../server/domain/member/entity/Member.java   |  5 ++-
 .../member/repository/MemberRepository.java   |  7 +++-
 .../domain/member/service/MemberService.java  |  4 ---
 .../member/service/MemberServiceImpl.java     |  4 ---
 .../apiPayload/code/status/ErrorStatus.java   |  1 +
 12 files changed, 74 insertions(+), 38 deletions(-)
 delete mode 100644 src/main/java/com/assu/server/domain/member/controller/MemberController.java
 delete mode 100644 src/main/java/com/assu/server/domain/member/converter/MemberConverter.java
 delete mode 100644 src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java
 delete mode 100644 src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java
 delete mode 100644 src/main/java/com/assu/server/domain/member/service/MemberService.java
 delete mode 100644 src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java

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 9389c35..b553e69 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
@@ -40,6 +40,7 @@ public class AuthController {
     private final LoginService loginService;
     private final LogoutService logoutService;
     private final SSUAuthService ssuAuthService;
+    private final WithdrawalService withdrawalService;
 
     @Operation(
             summary = "휴대폰 인증번호 발송 API",
@@ -337,4 +338,27 @@ public BaseResponse ssuAuth(
         return BaseResponse.onSuccess(SuccessStatus._OK, ssuAuthService.uSaintAuth(request));
     }
 
+    @Operation(
+            summary = "회원 탈퇴 API",
+            description = "# [v1.0 (2025-01-XX)](회원탈퇴)\n" +
+                    "- 현재 로그인한 사용자의 회원 탈퇴를 처리합니다.\n" +
+                    "- 소프트 삭제 방식으로, 한 달 후 완전히 삭제됩니다.\n" +
+                    "- 탈퇴 즉시 모든 토큰이 무효화됩니다.\n" +
+                    "\n**Headers:**\n" +
+                    "  - `Authorization` (String, required): Bearer 토큰 형식의 액세스 토큰\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 성공 메시지 반환\n" +
+                    "  - 탈퇴 후 재로그인 불가능"
+    )
+    @PostMapping("/withdraw")
+    public BaseResponse withdrawMember(
+            @RequestHeader("Authorization")
+            @Parameter(name = "Authorization", description = "Access Token. 형식: `Bearer `", required = true,
+                    in = ParameterIn.HEADER, schema = @Schema(type = "string"))
+            String authorization
+    ) {
+        withdrawalService.withdrawCurrentUser(authorization);
+        return BaseResponse.onSuccess(SuccessStatus._OK, null);
+    }
+
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
index 60b941b..266d7ec 100644
--- a/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/CommonAuthAdapter.java
@@ -18,7 +18,10 @@ public class CommonAuthAdapter implements RealmAuthAdapter {
     private final CommonAuthRepository commonAuthRepository;
     private final PasswordEncoder passwordEncoder; // BCrypt
 
-    @Override public boolean supports(AuthRealm realm) { return realm == AuthRealm.COMMON; }
+    @Override
+    public boolean supports(AuthRealm realm) {
+        return realm == AuthRealm.COMMON;
+    }
 
     @Override
     public UserDetails loadUserDetails(String email) {
@@ -37,10 +40,19 @@ public UserDetails loadUserDetails(String email) {
                 .build();
     }
 
-    @Override public Member loadMember(String email) {
-        return commonAuthRepository.findByEmail(email)
+    @Override
+    public Member loadMember(String email) {
+        Member member = commonAuthRepository.findByEmail(email)
                 .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER))
                 .getMember();
+
+        // 탈퇴된 회원이 다시 로그인하면 복구
+        if (member.getDeletedAt() != null) {
+            member.setDeletedAt(null);
+            commonAuthRepository.save(member.getCommonAuth());
+        }
+
+        return member;
     }
 
     @Override
@@ -55,11 +67,16 @@ public void registerCredentials(Member member, String email, String rawPassword)
                         .email(email)
                         .password(hash)
                         .isEmailVerified(false)
-                        .build()
-        );
+                        .build());
     }
 
-    @Override public PasswordEncoder passwordEncoder() { return passwordEncoder; }
-    @Override public String authRealmValue() { return "COMMON"; }
-}
+    @Override
+    public PasswordEncoder passwordEncoder() {
+        return passwordEncoder;
+    }
 
+    @Override
+    public String authRealmValue() {
+        return "COMMON";
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
index 0b52fab..8dcc7b7 100644
--- a/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
+++ b/src/main/java/com/assu/server/domain/auth/security/adapter/SSUAuthAdapter.java
@@ -19,7 +19,10 @@
 public class SSUAuthAdapter implements RealmAuthAdapter {
     private final SSUAuthRepository ssuAuthRepository;
 
-    @Override public boolean supports(AuthRealm realm) { return realm == AuthRealm.SSU; }
+    @Override
+    public boolean supports(AuthRealm realm) {
+        return realm == AuthRealm.SSU;
+    }
 
     @Override
     public UserDetails loadUserDetails(String studentNumber) {
@@ -41,9 +44,17 @@ public UserDetails loadUserDetails(String studentNumber) {
 
     @Override
     public Member loadMember(String studentNumber) {
-        return ssuAuthRepository.findByStudentNumber(studentNumber)
+        Member member = ssuAuthRepository.findByStudentNumber(studentNumber)
                 .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER))
                 .getMember();
+
+        // 탈퇴된 회원이 다시 로그인하면 복구
+        if (member.getDeletedAt() != null) {
+            member.setDeletedAt(null);
+            ssuAuthRepository.save(member.getSsuAuth());
+        }
+
+        return member;
     }
 
     @Override
@@ -57,8 +68,7 @@ public void registerCredentials(Member member, String studentNumber, String rawP
                         .studentNumber(studentNumber)
                         .isAuthenticated(true)
                         .authenticatedAt(LocalDateTime.now())
-                        .build()
-        );
+                        .build());
     }
 
     @Override
diff --git a/src/main/java/com/assu/server/domain/member/controller/MemberController.java b/src/main/java/com/assu/server/domain/member/controller/MemberController.java
deleted file mode 100644
index 0c40203..0000000
--- a/src/main/java/com/assu/server/domain/member/controller/MemberController.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.member.controller;
-
-public class MemberController {
-}
diff --git a/src/main/java/com/assu/server/domain/member/converter/MemberConverter.java b/src/main/java/com/assu/server/domain/member/converter/MemberConverter.java
deleted file mode 100644
index 5f8e135..0000000
--- a/src/main/java/com/assu/server/domain/member/converter/MemberConverter.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.member.converter;
-
-public class MemberConverter {
-}
diff --git a/src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java b/src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java
deleted file mode 100644
index b407129..0000000
--- a/src/main/java/com/assu/server/domain/member/dto/MemberRequestDTO.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.member.dto;
-
-public class MemberRequestDTO {
-}
diff --git a/src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java b/src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java
deleted file mode 100644
index ea43988..0000000
--- a/src/main/java/com/assu/server/domain/member/dto/MemberResponseDTO.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.member.dto;
-
-public class MemberResponseDTO {
-}
diff --git a/src/main/java/com/assu/server/domain/member/entity/Member.java b/src/main/java/com/assu/server/domain/member/entity/Member.java
index 355e96e..552e57d 100644
--- a/src/main/java/com/assu/server/domain/member/entity/Member.java
+++ b/src/main/java/com/assu/server/domain/member/entity/Member.java
@@ -42,12 +42,15 @@ public class Member extends BaseEntity {
     @Enumerated(EnumType.STRING)
     @Column(name = "role", nullable = false)
     @JdbcTypeCode(SqlTypes.VARCHAR)
-    private UserRole role;  // STUDENT, ADMIN, PARTNER
+    private UserRole role; // STUDENT, ADMIN, PARTNER
 
     @Enumerated(EnumType.STRING)
     @Column(nullable = false)
     private ActivationStatus isActivated;  // ACTIVE, INACTIVE, SUSPEND
 
+    // 소프트 삭제를 위한 삭제 시점
+    private LocalDateTime deletedAt;
+
     // 역할별 프로필 - 선택적으로 연관
     @OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
     private Student studentProfile;
diff --git a/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
index 59d6d72..555a352 100644
--- a/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
+++ b/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
@@ -1,9 +1,13 @@
 package com.assu.server.domain.member.repository;
 
+import java.time.LocalDateTime;
+import java.util.List;
 import java.util.Optional;
 
 import com.assu.server.domain.member.entity.Member;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Repository;
 
 @Repository
@@ -11,5 +15,6 @@ public interface MemberRepository extends JpaRepository {
     boolean existsByPhoneNum(String phoneNum);
 
     Optional findMemberById(Long id);
-
+    
+    List findByDeletedAtBefore(LocalDateTime deletedAt);
 }
diff --git a/src/main/java/com/assu/server/domain/member/service/MemberService.java b/src/main/java/com/assu/server/domain/member/service/MemberService.java
deleted file mode 100644
index 56fa069..0000000
--- a/src/main/java/com/assu/server/domain/member/service/MemberService.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.member.service;
-
-public interface MemberService {
-}
diff --git a/src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java b/src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java
deleted file mode 100644
index 8b54110..0000000
--- a/src/main/java/com/assu/server/domain/member/service/MemberServiceImpl.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.assu.server.domain.member.service;
-
-public class MemberServiceImpl {
-}
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index a9b858f..fb776e8 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -54,6 +54,7 @@ public enum ErrorStatus implements BaseErrorCode {
     EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4006","이미 존재하는 이메일입니다."),
     EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 학번입니다."),
     NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "MEMBER_4009", "제휴업체를 찾을 수 없습니다."),
+    MEMBER_ALREADY_WITHDRAWN(HttpStatus.BAD_REQUEST, "MEMBER_4010", "이미 탈퇴된 회원입니다."),
 
     // 제휴 에러
     NO_SUCH_PAPER(HttpStatus.NOT_FOUND, "PAPER_9001", "제휴를 찾을 수 없습니다."),

From 0e6ce08c9942c660d9c2abf33e31c3da1aebb99d Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Sun, 14 Sep 2025 03:09:37 +0900
Subject: [PATCH 170/270] =?UTF-8?q?[FEAT/#92]=20=ED=9A=8C=EC=9B=90?=
 =?UTF-8?q?=ED=83=88=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 소프트 삭제 방식 (deletedAt 필드)
- 탈퇴 회원 재로그인 시 자동 복구
- 회원탈퇴 API 엔드포인트 추가
- 토큰 무효화 및 보안 처리
---
 .../auth/schduler/MemberCleanupScheduler.java | 44 +++++++++++++
 .../auth/service/WithdrawalService.java       |  6 ++
 .../auth/service/WithdrawalServiceImpl.java   | 63 +++++++++++++++++++
 3 files changed, 113 insertions(+)
 create mode 100644 src/main/java/com/assu/server/domain/auth/schduler/MemberCleanupScheduler.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java

diff --git a/src/main/java/com/assu/server/domain/auth/schduler/MemberCleanupScheduler.java b/src/main/java/com/assu/server/domain/auth/schduler/MemberCleanupScheduler.java
new file mode 100644
index 0000000..4638995
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/schduler/MemberCleanupScheduler.java
@@ -0,0 +1,44 @@
+package com.assu.server.domain.auth.schduler;
+
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MemberCleanupScheduler {
+
+    private final MemberRepository memberRepository;
+
+    @Scheduled(cron = "0 0 2 * * ?") // 매일 오전 2시
+    @Transactional
+    public void cleanupDeletedMembers() {
+        log.info("탈퇴 회원 완전 삭제 작업 시작");
+
+        // 한 달 전 시점 계산
+        LocalDateTime oneMonthAgo = LocalDateTime.now().minusMonths(1);
+
+        // 한 달 이상 전에 탈퇴한 회원들 조회
+        List membersToDelete = memberRepository.findByDeletedAtBefore(oneMonthAgo);
+
+        if (membersToDelete.isEmpty()) {
+            log.info("완전 삭제할 탈퇴 회원이 없습니다.");
+            return;
+        }
+
+        log.info("완전 삭제할 탈퇴 회원 수: {}", membersToDelete.size());
+
+        // 실제 데이터베이스에서 삭제
+        memberRepository.deleteAll(membersToDelete);
+
+        log.info("탈퇴 회원 완전 삭제 작업 완료: {}명 삭제됨", membersToDelete.size());
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java b/src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java
new file mode 100644
index 0000000..f7843c4
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/WithdrawalService.java
@@ -0,0 +1,6 @@
+package com.assu.server.domain.auth.service;
+
+public interface WithdrawalService {
+
+    void withdrawCurrentUser(String authorization);
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java
new file mode 100644
index 0000000..0532d9d
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/WithdrawalServiceImpl.java
@@ -0,0 +1,63 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.auth.security.jwt.JwtUtil;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import io.jsonwebtoken.Claims;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WithdrawalServiceImpl implements WithdrawalService {
+
+    private final MemberRepository memberRepository;
+    private final JwtUtil jwtUtil;
+
+    @Override
+    @Transactional
+    public void withdrawCurrentUser(String authorization) {
+        String rawAccessToken = jwtUtil.getTokenFromHeader(authorization);
+
+        // Access 토큰에서 memberId 추출
+        Claims claims = jwtUtil.validateTokenOnlySignature(rawAccessToken);
+        Long memberId = ((Number) claims.get("userId")).longValue();
+
+        log.info("현재 사용자 탈퇴 시작: memberId={}", memberId);
+
+        // 2) 회원 탈퇴 처리
+        withdrawMember(memberId);
+
+        // 3) 현재 Access 토큰을 블랙리스트에 등록
+        jwtUtil.blacklistAccess(rawAccessToken);
+
+        log.info("현재 사용자 탈퇴 완료: memberId={}", memberId);
+    }
+
+    private void withdrawMember(Long memberId) {
+        log.info("회원 탈퇴 시작: memberId={}", memberId);
+
+        // 1) 회원 존재 여부 확인
+        Member member = memberRepository.findById(memberId)
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+
+        // 2) 이미 탈퇴된 회원인지 확인
+        if (member.getDeletedAt() != null) {
+            throw new CustomAuthException(ErrorStatus.MEMBER_ALREADY_WITHDRAWN);
+        }
+
+        // 3) 소프트 삭제: deletedAt 필드에 현재 시간 설정
+        member.setDeletedAt(java.time.LocalDateTime.now());
+        memberRepository.save(member);
+
+        // 4) 해당 회원의 모든 토큰 무효화
+        jwtUtil.removeAllRefreshTokens(memberId);
+
+        log.info("회원 탈퇴 완료: memberId={}", memberId);
+    }
+}

From 25e4fb1c5a94184ab3c2e565a6b9fdf9dc7577f9 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Sun, 14 Sep 2025 07:09:59 +0900
Subject: [PATCH 171/270] =?UTF-8?q?[FEAT/#92]=20=EB=85=B8=EC=85=98=20->=20?=
 =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EB=B2=84=EC=A0=80=EB=8B=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../auth/controller/AuthController.java       | 131 +++++++++---------
 1 file changed, 67 insertions(+), 64 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 b553e69..848c2c3 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
@@ -84,10 +84,10 @@ public BaseResponse checkAuthNumber(
 
     @Operation(
             summary = "학생 회원가입 API",
-            description = "# [v1.1 (2025-09-07)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971)\n" +
+            description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971)\n" +
                     "- `application/json` 요청 바디를 사용합니다.\n" +
                     "- 처리: 유세인트 인증 → 학생 정보 추출 → 회원가입 완료\n" +
-                    "- 성공 시 201(Created)과 생성된 memberId 및 JWT 토큰 반환.\n" +
+                    "- 성공 시 201(Created)과 생성된 memberId, JWT 토큰, 기본 정보 반환.\n" +
                     "\n**Request Body:**\n" +
                     "  - `StudentTokenSignUpRequest` 객체 (JSON, required): 숭실대 학생 토큰 가입 정보\n" +
                     "  - `phoneNumber` (String, required): 휴대폰 번호\n" +
@@ -100,12 +100,15 @@ public BaseResponse checkAuthNumber(
                     "\n**Response:**\n" +
                     "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환\n" +
                     "  - `memberId` (Long): 회원 ID\n" +
-                    "  - `tokens` (Object): JWT 토큰 정보"
-    )
-    @io.swagger.v3.oas.annotations.parameters.RequestBody(
-            required = true,
-            content = @Content(schema = @Schema(implementation = StudentTokenSignUpRequest.class))
-    )
+                    "  - `role` (UserRole): 회원 역할 (STUDENT)\n" +
+                    "  - `status` (ActivationStatus): 회원 상태 (ACTIVE)\n" +
+                    "  - `tokens` (Object): JWT 토큰 정보 (accessToken, refreshToken, expiresAt)\n" +
+                    "  - `basicInfo` (UserBasicInfo): 사용자 기본 정보 (프론트 캐싱용)\n" +
+                    "    - `name` (String): 학생 이름\n" +
+                    "    - `university` (String): 대학교 (한글명)\n" +
+                    "    - `department` (String): 단과대 (한글명)\n" +
+                    "    - `major` (String): 전공/학과 (한글명)")
+    @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(schema = @Schema(implementation = StudentTokenSignUpRequest.class)))
     @PostMapping(value = "/students/signup", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse signupStudent(
             @Valid @RequestBody StudentTokenSignUpRequest request
@@ -119,13 +122,12 @@ public BaseResponse signupStudent(
         return BaseResponse.onSuccess(SuccessStatus._OK, response);
     }
 
-    @Operation(
-            summary = "제휴업체 회원가입 API",
-            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80d7a8f2c3a6fcd8b537?source=copy_link)\n" +
+    @Operation(summary = "제휴업체 회원가입 API",
+            description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed80d7a8f2c3a6fcd8b537)\n" +
                     "- `multipart/form-data`로 호출합니다.\n" +
                     "- 파트: `payload`(JSON, PartnerSignUpRequest) + `licenseImage`(파일, 사업자등록증).\n" +
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
-                    "- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+                    "- 성공 시 201(Created)과 생성된 memberId, JWT 토큰, 기본 정보 반환.\n" +
                     "\n**Request Parts:**\n" +
                     "  - `request` (JSON, required): `PartnerSignUpRequest` 객체\n" +
                     "  - `email` (String, required): 이메일 주소\n" +
@@ -137,38 +139,30 @@ public BaseResponse signupStudent(
                     "  - `address` (String, required): 회사 주소\n" +
                     "  - `licenseImage` (MultipartFile, required): 사업자등록증 이미지 파일\n" +
                     "\n**Response:**\n" +
-                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환"
-    )
+                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환\n" +
+                    "  - `memberId` (Long): 회원 ID\n" +
+                    "  - `role` (UserRole): 회원 역할 (PARTNER)\n" +
+                    "  - `status` (ActivationStatus): 회원 상태 (ACTIVE)\n" +
+                    "  - `tokens` (Object): JWT 토큰 정보 (accessToken, refreshToken, expiresAt)\n" +
+                    "  - `basicInfo` (UserBasicInfo): 사용자 기본 정보 (프론트 캐싱용)\n" +
+                    "    - `name` (String): 업체명\n" +
+                    "    - `university`, `department`, `major`: null (Partner는 해당 없음)")
     @PostMapping(value = "/partners/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse signupPartner(
-            @Valid @RequestPart("request")
-            @Parameter(
-                    description = "JSON 형식의 제휴업체 가입 정보",
+            @Valid @RequestPart("request") @Parameter(description = "JSON 형식의 제휴업체 가입 정보",
                     // 'request' 파트의 content type을 명시적으로 지정
-                    content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
-                            schema = @Schema(implementation = PartnerSignUpRequest.class))
-            )
-            PartnerSignUpRequest request,
+                    content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = PartnerSignUpRequest.class))) PartnerSignUpRequest request,
 
-            @RequestPart("licenseImage")
-            @Parameter(
-                    description = "사업자등록증 이미지 파일 (Multipart Part)",
-                    required = true,
-                    content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
-                            schema = @Schema(type = "string", format = "binary"))
-            )
-            MultipartFile licenseImage
-    ) {
+            @RequestPart("licenseImage") @Parameter(description = "사업자등록증 이미지 파일 (Multipart Part)", required = true, content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, schema = @Schema(type = "string", format = "binary"))) MultipartFile licenseImage) {
         return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupPartner(request, licenseImage));
     }
 
-    @Operation(
-            summary = "관리자 회원가입 API",
-            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48?source=copy_link)\n" +
+    @Operation(summary = "관리자 회원가입 API",
+            description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed80cdb98bc2b4d5042b48)\n" +
                     "- `multipart/form-data`로 호출합니다.\n" +
                     "- 파트: `payload`(JSON, AdminSignUpRequest) + `signImage`(파일, 신분증).\n" +
                     "- 처리: users + common_auth 생성, 이메일 중복/비밀번호 규칙 검증.\n" +
-                    "- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
+                    "- 성공 시 201(Created)과 생성된 memberId, JWT 토큰, 기본 정보 반환.\n" +
                     "\n**Request Parts:**\n" +
                     "  - `request` (JSON, required): `AdminSignUpRequest` 객체\n" +
                     "  - `email` (String, required): 이메일 주소\n" +
@@ -182,8 +176,16 @@ public BaseResponse signupPartner(
                     "  - `position` (String, required): 직책\n" +
                     "  - `signImage` (MultipartFile, required): 인감 이미지 파일\n" +
                     "\n**Response:**\n" +
-                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환"
-    )
+                    "  - 성공 시 201(Created)과 `SignUpResponse` 객체 반환\n" +
+                    "  - `memberId` (Long): 회원 ID\n" +
+                    "  - `role` (UserRole): 회원 역할 (ADMIN)\n" +
+                    "  - `status` (ActivationStatus): 회원 상태 (ACTIVE)\n" +
+                    "  - `tokens` (Object): JWT 토큰 정보 (accessToken, refreshToken, expiresAt)\n" +
+                    "  - `basicInfo` (UserBasicInfo): 사용자 기본 정보 (프론트 캐싱용)\n" +
+                    "    - `name` (String): 단체명/관리자 이름\n" +
+                    "    - `university` (String): 대학교 (한글명)\n" +
+                    "    - `department` (String): 단과대 (한글명)\n" +
+                    "    - `major` (String): 전공/학과 (한글명)")
     @PostMapping(value = "/admins/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public BaseResponse signupAdmin(
             @Valid @RequestPart("request")
@@ -205,27 +207,26 @@ public BaseResponse signupAdmin(
         return BaseResponse.onSuccess(SuccessStatus._OK, signUpService.signupAdmin(request, signImage));
     }
 
-    @Operation(
-            summary = "공통 로그인 API",
-            description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50?source=copy_link)\n" +
+    @Operation(summary = "공통 로그인 API"
+            , description = "# [v1.1 (2025-09-13)](https://clumsy-seeder-416.notion.site/2241197c19ed811c961be6a474de0e50)\n" +
                     "- `application/json`로 호출합니다.\n" +
                     "- 바디: `LoginRequest(email, password)`.\n" +
                     "- 처리: 자격 증명 검증 후 Access/Refresh 토큰 발급 및 저장.\n" +
-                    "- 성공 시 200(OK)과 토큰/만료시각 반환.\n" +
+                    "- 성공 시 200(OK)과 토큰, 만료시각, 기본 정보 반환.\n" +
                     "\n**Request Body:**\n" +
                     "  - `CommonLoginRequest` 객체 (JSON, required): 로그인 정보\n" +
                     "  - `email` (String, required): 이메일 주소\n" +
                     "  - `password` (String, required): 비밀번호\n" +
                     "\n**Response:**\n" +
                     "  - 성공 시 200(OK)과 `LoginResponse` 객체 반환\n" +
-                    "  - `accessToken` (String): 액세스 토큰\n" +
-                    "  - `refreshToken` (String): 리프레시 토큰\n" +
-                    "  - `expiresAt` (LocalDateTime): 토큰 만료 시각"
-    )
-    @io.swagger.v3.oas.annotations.parameters.RequestBody(
-            required = true,
-            content = @Content(schema = @Schema(implementation = CommonLoginRequest.class))
-    )
+                    "  - `memberId` (Long): 회원 ID\n" +
+                    "  - `role` (UserRole): 회원 역할 (PARTNER/ADMIN)\n" +
+                    "  - `status` (ActivationStatus): 회원 상태\n" +
+                    "  - `tokens` (Object): JWT 토큰 정보 (accessToken, refreshToken, expiresAt)\n" +
+                    "  - `basicInfo` (UserBasicInfo): 사용자 기본 정보 (프론트 캐싱용)\n" +
+                    "    - `name` (String): 업체명/단체명/관리자 이름\n" +
+                    "    - `university`, `department`, `major`: Admin의 경우 한글명, Partner의 경우 null")
+    @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(schema = @Schema(implementation = CommonLoginRequest.class)))
     @PostMapping(value = "/commons/login", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse loginCommon(
             @RequestBody @Valid CommonLoginRequest request
@@ -233,12 +234,12 @@ public BaseResponse loginCommon(
         return BaseResponse.onSuccess(SuccessStatus._OK, loginService.loginCommon(request));
     }
 
-    @Operation(summary = "학생 로그인 API",
-            description = "# [v1.1 (2025-09-07)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8?source=copy_link)\n" +
+    @Operation(summary = "학생 로그인 API"
+                , description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed80f6b495fa37f8c084a8)\n" +
                     "- `application/json`로 호출합니다.\n" +
                     "- 바디: `StudentTokenLoginRequest(sToken, sIdno, university)`.\n" +
                     "- 처리: 유세인트 인증 → 기존 회원 확인 → JWT 토큰 발급.\n" +
-                    "- 성공 시 200(OK)과 토큰/만료시각 반환.\n" +
+                    "- 성공 시 200(OK)과 토큰, 만료시각, 기본 정보 반환.\n" +
                     "\n**Request Body:**\n" +
                     "  - `StudentTokenAuthPayload` 객체 (JSON, required): 숭실대 학생 토큰 로그인 정보\n" +
                     "  - `sToken` (String, required): 유세인트 sToken\n" +
@@ -246,10 +247,15 @@ public BaseResponse loginCommon(
                     "  - `university` (University enum, required): 대학 이름 (SSU)\n" +
                    "\n**Response:**\n" +
                     "  - 성공 시 200(OK)과 `LoginResponse` 객체 반환\n" +
-                    "  - `accessToken` (String): 액세스 토큰\n" +
-                    "  - `refreshToken` (String): 리프레시 토큰\n" +
-                    "  - `expiresAt` (LocalDateTime): 토큰 만료 시각"
-    )
+                    "  - `memberId` (Long): 회원 ID\n" +
+                    "  - `role` (UserRole): 회원 역할 (STUDENT)\n" +
+                    "  - `status` (ActivationStatus): 회원 상태\n" +
+                    "  - `tokens` (Object): JWT 토큰 정보 (accessToken, refreshToken, expiresAt)\n" +
+                    "  - `basicInfo` (UserBasicInfo): 사용자 기본 정보 (프론트 캐싱용)\n" +
+                    "    - `name` (String): 학생 이름\n" +
+                    "    - `university` (String): 대학교 (한글명)\n" +
+                    "    - `department` (String): 단과대 (한글명)\n" +
+                    "    - `major` (String): 전공/학과 (한글명)")
     @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(schema = @Schema(implementation = StudentTokenAuthPayload.class)))
     @PostMapping(value = "/students/login", consumes = MediaType.APPLICATION_JSON_VALUE)
     public BaseResponse loginStudent(
@@ -282,15 +288,12 @@ public BaseResponse loginStudent(
                     "  - 성공 시 200(OK)과 새 토큰/만료시각 반환."
     )
     @Parameters({
-            @Parameter(name = "Authorization", description = "Access Token (만료 허용). 형식: `Bearer `", required = true,
-                    in = ParameterIn.HEADER, schema = @Schema(type = "string")),
-            @Parameter(name = "RefreshToken", description = "Refresh Token", required = true,
-                    in = ParameterIn.HEADER, schema = @Schema(type = "string"))
+            @Parameter(name = "Authorization", description = "Access Token (만료 허용). 형식: `Bearer `", required = true, in = ParameterIn.HEADER, schema = @Schema(type = "string")),
+            @Parameter(name = "RefreshToken", description = "Refresh Token", required = true, in = ParameterIn.HEADER, schema = @Schema(type = "string"))
     })
     @PostMapping("/tokens/refresh")
     public BaseResponse refreshToken(
-            @RequestHeader("RefreshToken") String refreshToken
-    ) {
+            @RequestHeader("RefreshToken") String refreshToken) {
         return BaseResponse.onSuccess(SuccessStatus._OK, loginService.refresh(refreshToken));
     }
 
@@ -340,7 +343,7 @@ public BaseResponse ssuAuth(
 
     @Operation(
             summary = "회원 탈퇴 API",
-            description = "# [v1.0 (2025-01-XX)](회원탈퇴)\n" +
+            description = "# [v1.0 (2025-09-13)](https://clumsy-seeder-416.notion.site/2501197c19ed800a844bdafa2e2e8d2e?source=copy_link)\n" +
                     "- 현재 로그인한 사용자의 회원 탈퇴를 처리합니다.\n" +
                     "- 소프트 삭제 방식으로, 한 달 후 완전히 삭제됩니다.\n" +
                     "- 탈퇴 즉시 모든 토큰이 무효화됩니다.\n" +
@@ -348,9 +351,9 @@ public BaseResponse ssuAuth(
                     "  - `Authorization` (String, required): Bearer 토큰 형식의 액세스 토큰\n" +
                     "\n**Response:**\n" +
                     "  - 성공 시 200(OK)과 성공 메시지 반환\n" +
-                    "  - 탈퇴 후 재로그인 불가능"
+                    "  - 탈퇴 후 재로그인 가능"
     )
-    @PostMapping("/withdraw")
+    @PatchMapping("/withdraw")
     public BaseResponse withdrawMember(
             @RequestHeader("Authorization")
             @Parameter(name = "Authorization", description = "Access Token. 형식: `Bearer `", required = true,

From f8ac8213691e954d596617d60c4e6afeb0f36b50 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Sun, 14 Sep 2025 07:10:56 +0900
Subject: [PATCH 172/270] =?UTF-8?q?[REFACTOR/#89]=20=EB=A1=9C=EA=B7=B8?=
 =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?=
 =?UTF-8?q?=EC=8B=9C=20=EB=B0=98=ED=99=98=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/domain/admin/entity/Admin.java     |  2 +
 .../domain/auth/dto/common/UserBasicInfo.java | 29 ++++++++
 .../domain/auth/dto/login/LoginResponse.java  |  6 +-
 .../auth/dto/signup/SignUpResponse.java       | 19 ++++-
 .../student/StudentTokenAuthPayload.java      |  1 +
 .../domain/auth/service/LoginServiceImpl.java | 72 ++++++++++++++-----
 .../auth/service/SignUpServiceImpl.java       | 66 +++++++++++------
 .../domain/user/entity/enums/Department.java  | 12 +++-
 .../domain/user/entity/enums/Major.java       | 22 +++---
 .../domain/user/entity/enums/University.java  | 12 +++-
 10 files changed, 187 insertions(+), 54 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfo.java

diff --git a/src/main/java/com/assu/server/domain/admin/entity/Admin.java b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
index 288b3f5..875e526 100644
--- a/src/main/java/com/assu/server/domain/admin/entity/Admin.java
+++ b/src/main/java/com/assu/server/domain/admin/entity/Admin.java
@@ -14,6 +14,7 @@
 import jakarta.persistence.OneToOne;
 import jakarta.persistence.Id;
 import jakarta.persistence.*;
+import jakarta.validation.constraints.NotNull;
 import lombok.*;
 import org.hibernate.annotations.JdbcTypeCode;
 import org.hibernate.type.SqlTypes;
@@ -56,6 +57,7 @@ public class Admin {
     private Department department;
 
     @Enumerated(EnumType.STRING)
+    @NotNull
     private University university;
 
     @JdbcTypeCode(SqlTypes.GEOMETRY)
diff --git a/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfo.java b/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfo.java
new file mode 100644
index 0000000..6bbe804
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/common/UserBasicInfo.java
@@ -0,0 +1,29 @@
+package com.assu.server.domain.auth.dto.common;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Schema(description = "사용자 기본 정보")
+public class UserBasicInfo {
+
+    @Schema(description = "이름/업체명/단체명", example = "홍길동")
+    private String name;
+
+    @Schema(description = "대학교", example = "숭실대학교")
+    private String university;
+
+    @Schema(description = "단과대", example = "IT공과대학")
+    private String department;
+
+    @Schema(description = "전공/학과", example = "소프트웨어학부")
+    private String major;
+}
diff --git a/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java b/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java
index d456177..0f24d20 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/login/LoginResponse.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.auth.dto.login;
 
+import com.assu.server.domain.auth.dto.common.UserBasicInfo;
 import com.assu.server.domain.auth.dto.signup.Tokens;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.common.enums.UserRole;
@@ -7,8 +8,6 @@
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 
-import java.time.Instant;
-
 @Getter
 @Setter
 @NoArgsConstructor
@@ -29,4 +28,7 @@ public class LoginResponse {
 
     @Schema(description = "액세스 토큰/리프레시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
     private Tokens tokens;
+
+    @Schema(description = "사용자 기본 정보 (캐싱용)")
+    private UserBasicInfo basicInfo;
 }
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java b/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java
index 617d501..02bdd2a 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/SignUpResponse.java
@@ -1,23 +1,36 @@
 package com.assu.server.domain.auth.dto.signup;
 
+import com.assu.server.domain.auth.dto.common.UserBasicInfo;
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.common.enums.UserRole;
-import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-import java.time.Instant;
-import java.time.OffsetDateTime;
 
 @Getter
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Schema(description = "회원가입 성공 응답")
 public class SignUpResponse {
+
+    @Schema(description = "회원 ID", example = "123")
     private Long memberId;
+
+    @Schema(description = "회원 역할", example = "STUDENT")
     private UserRole role;
+
+    @Schema(description = "회원 상태", example = "ACTIVE")
     private ActivationStatus status;
+
+    @Schema(description = "액세스 토큰/리프레시 토큰")
     private Tokens tokens;
+
+    @Schema(description = "사용자 기본 정보 (캐싱용)")
+    private UserBasicInfo basicInfo;
 }
diff --git a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java
index 8707439..4290745 100644
--- a/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java
+++ b/src/main/java/com/assu/server/domain/auth/dto/signup/student/StudentTokenAuthPayload.java
@@ -25,3 +25,4 @@ public class StudentTokenAuthPayload {
     private University university;
 }
 
+
diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
index 3177e2a..17b72ba 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -1,5 +1,6 @@
 package com.assu.server.domain.auth.service;
 
+import com.assu.server.domain.auth.dto.common.UserBasicInfo;
 import com.assu.server.domain.auth.dto.login.CommonLoginRequest;
 import com.assu.server.domain.auth.dto.login.LoginResponse;
 import com.assu.server.domain.auth.dto.login.RefreshResponse;
@@ -14,7 +15,10 @@
 import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.security.jwt.JwtUtil;
 import com.assu.server.domain.user.entity.Student;
+import com.assu.server.domain.user.entity.enums.Department;
 import com.assu.server.domain.user.entity.enums.EnrollmentStatus;
+import com.assu.server.domain.user.entity.enums.Major;
+import com.assu.server.domain.user.entity.enums.University;
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import lombok.RequiredArgsConstructor;
@@ -56,9 +60,7 @@ public LoginResponse loginCommon(CommonLoginRequest request) {
                 new LoginUsernamePasswordAuthenticationToken(
                         AuthRealm.COMMON,
                         request.getEmail(),
-                        request.getPassword()
-                )
-        );
+                        request.getPassword()));
 
         RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON);
 
@@ -70,7 +72,7 @@ public LoginResponse loginCommon(CommonLoginRequest request) {
                 member.getId(),
                 authentication.getName(), // email
                 member.getRole(),
-                adapter.authRealmValue()    // "COMMON"
+                adapter.authRealmValue() // "COMMON"
         );
 
         return LoginResponse.builder()
@@ -78,6 +80,7 @@ public LoginResponse loginCommon(CommonLoginRequest request) {
                 .role(member.getRole())
                 .status(member.getIsActivated())
                 .tokens(tokens)
+                .basicInfo(buildUserBasicInfo(member))
                 .build();
     }
 
@@ -93,9 +96,9 @@ public LoginResponse loginCommon(CommonLoginRequest request) {
     public LoginResponse loginSsuStudent(StudentTokenAuthPayload request) {
         // 1) 유세인트 인증
         USaintAuthRequest authRequest = USaintAuthRequest.builder()
-                        .sToken(request.getSToken())
-                        .sIdno(request.getSIdno())
-                        .build();
+                .sToken(request.getSToken())
+                .sIdno(request.getSIdno())
+                .build();
 
         USaintAuthResponse authResponse = ssuAuthService.uSaintAuth(authRequest);
 
@@ -109,7 +112,7 @@ public LoginResponse loginSsuStudent(StudentTokenAuthPayload request) {
         // 3) Student 정보 업데이트 (유세인트에서 크롤링한 최신 정보로)
         Student student = member.getStudentProfile();
         if (student == null) {
-                throw new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER);
+            throw new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER);
         }
 
         // 유세인트에서 크롤링한 최신 정보로 업데이트
@@ -117,17 +120,16 @@ public LoginResponse loginSsuStudent(StudentTokenAuthPayload request) {
                 authResponse.getName(),
                 authResponse.getMajor(),
                 parseEnrollmentStatus(authResponse.getEnrollmentStatus()),
-                authResponse.getYearSemester()
-        );
+                authResponse.getYearSemester());
 
         studentRepository.save(student);
 
         // 4) 토큰 발급
         Tokens tokens = jwtUtil.issueTokens(
-                        member.getId(),
-                        authResponse.getStudentNumber().toString(), // studentNumber
-                        member.getRole(),
-                        adapter.authRealmValue() // 예: "SSU"
+                member.getId(),
+                authResponse.getStudentNumber().toString(), // studentNumber
+                member.getRole(),
+                adapter.authRealmValue() // 예: "SSU"
         );
 
         return LoginResponse.builder()
@@ -135,6 +137,7 @@ public LoginResponse loginSsuStudent(StudentTokenAuthPayload request) {
                 .role(member.getRole())
                 .status(member.getIsActivated())
                 .tokens(tokens)
+                .basicInfo(buildUserBasicInfo(member))
                 .build();
     }
 
@@ -154,8 +157,7 @@ public RefreshResponse refresh(String refreshToken) {
         return new RefreshResponse(
                 ((Number) jwtUtil.validateTokenOnlySignature(rotated.getAccessToken()).get("userId")).longValue(),
                 rotated.getAccessToken(),
-                rotated.getRefreshToken()
-        );
+                rotated.getRefreshToken());
     }
 
     private EnrollmentStatus parseEnrollmentStatus(String status) {
@@ -173,4 +175,42 @@ private EnrollmentStatus parseEnrollmentStatus(String status) {
             return EnrollmentStatus.ENROLLED;
         }
     }
+
+    /**
+     * 사용자 기본 정보를 빌드하는 헬퍼 메서드
+     */
+    private UserBasicInfo buildUserBasicInfo(Member member) {
+        UserBasicInfo.UserBasicInfoBuilder builder = UserBasicInfo.builder();
+
+        switch (member.getRole()) {
+            case STUDENT -> {
+                Student student = member.getStudentProfile();
+                if (student != null) {
+                    builder.name(student.getName())
+                            .university(student.getUniversity().getDisplayName())
+                            .department(student.getDepartment().getDisplayName())
+                            .major(student.getMajor().getDisplayName());
+                }
+            }
+            case ADMIN -> {
+                // Admin 엔티티에서 정보 추출
+                var admin = member.getAdminProfile();
+                if (admin != null) {
+                    builder.name(admin.getName())
+                            .university(admin.getUniversity().getDisplayName())
+                            .department(admin.getDepartment().getDisplayName())
+                            .major(admin.getMajor().getDisplayName());
+                }
+            }
+            case PARTNER -> {
+                // Partner 엔티티에서 정보 추출 (Partner는 name만 필요)
+                var partner = member.getPartnerProfile();
+                if (partner != null) {
+                    builder.name(partner.getName());
+                }
+            }
+        }
+
+        return builder.build();
+    }
 }
\ No newline at end of file
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 9ceb1b0..51cfa44 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
@@ -2,6 +2,7 @@
 
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
+import com.assu.server.domain.auth.dto.common.UserBasicInfo;
 import com.assu.server.domain.auth.dto.signup.*;
 import com.assu.server.domain.auth.dto.signup.common.CommonInfoPayload;
 import com.assu.server.domain.auth.dto.ssu.USaintAuthRequest;
@@ -82,7 +83,7 @@ public SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req) {
 
         // 학번 중복 체크
         if (ssuAuthRepository.existsByStudentNumber(authResponse.getStudentNumber().toString())) {
-                throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
+            throw new CustomAuthException(ErrorStatus.EXISTED_STUDENT);
         }
 
         // 2) member 생성
@@ -92,8 +93,7 @@ public SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req) {
                         .isPhoneVerified(true)
                         .role(UserRole.STUDENT)
                         .isActivated(ActivationStatus.ACTIVE)
-                        .build()
-        );
+                        .build());
 
         // 3) SSUAuth 생성 (학번만 저장)
         RealmAuthAdapter adapter = pickAdapter(AuthRealm.SSU);
@@ -118,14 +118,22 @@ public SignUpResponse signupSsuStudent(StudentTokenSignUpRequest req) {
                 member.getId(),
                 authResponse.getStudentNumber().toString(), // studentNumber
                 UserRole.STUDENT,
-                "SSU"
-        );
+                "SSU");
+
+        // 6) Student 정보로 직접 UserBasicInfo 생성
+        UserBasicInfo basicInfo = UserBasicInfo.builder()
+                .name(student.getName())
+                .university(student.getUniversity().getDisplayName())
+                .department(student.getDepartment().getDisplayName())
+                .major(student.getMajor().getDisplayName())
+                .build();
 
         return SignUpResponse.builder()
                 .memberId(member.getId())
                 .role(UserRole.STUDENT)
                 .status(member.getIsActivated())
                 .tokens(tokens)
+                .basicInfo(basicInfo)
                 .build();
     }
 
@@ -144,8 +152,7 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
                         .isPhoneVerified(true)
                         .role(UserRole.PARTNER)
                         .isActivated(ActivationStatus.ACTIVE) // Todo 초기에 SUSPEND 로직 추가해야함, 허가 후 ACTIVE
-                        .build()
-        );
+                        .build());
 
         // 2) RealmAuthAdapter 로 Common 자격 저장
         RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON);
@@ -176,8 +183,7 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
                         .point(point)
                         .latitude(lat)
                         .longitude(lng)
-                        .build()
-        );
+                        .build());
 
         // store 생성/연결
         Optional storeOpt = storeRepository.findBySameAddress(address, info.getDetailAddress());
@@ -207,14 +213,19 @@ public SignUpResponse signupPartner(PartnerSignUpRequest req, MultipartFile lice
                 member.getId(),
                 req.getCommonAuth().getEmail(),
                 UserRole.PARTNER,
-                adapter.authRealmValue()
-        );
+                adapter.authRealmValue());
+
+        // 5) Partner 정보로 직접 UserBasicInfo 생성
+        UserBasicInfo basicInfo = UserBasicInfo.builder()
+                .name(partner.getName())
+                .build();
 
         return SignUpResponse.builder()
                 .memberId(member.getId())
                 .role(UserRole.PARTNER)
                 .status(member.getIsActivated())
                 .tokens(tokens)
+                .basicInfo(basicInfo)
                 .build();
     }
 
@@ -233,8 +244,7 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
                         .isPhoneVerified(true)
                         .role(UserRole.ADMIN)
                         .isActivated(ActivationStatus.ACTIVE) // Todo 초기에 SUSPEND 로직 추가해야함, 허가 후 ACTIVE
-                        .build()
-        );
+                        .build());
 
         // 2) RealmAuthAdapter 로 Common 자격 저장
         RealmAuthAdapter adapter = pickAdapter(AuthRealm.COMMON);
@@ -254,12 +264,12 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
         Double lng = sp.getLongitude();
         Point point = toPoint(lat, lng);
 
-        // 3) Partner 프로필 생성
-        adminRepository.save(
+        // 3) Admin 프로필 생성
+        Admin admin = adminRepository.save(
                 Admin.builder()
-                    .major(req.getCommonAuth().getMajor())
-                    .department(req.getCommonAuth().getDepartment())
-                    .university(req.getCommonAuth().getUniversity())
+                        .major(req.getCommonAuth().getMajor())
+                        .department(req.getCommonAuth().getDepartment())
+                        .university(req.getCommonAuth().getUniversity())
                         .member(member)
                         .name(info.getName())
                         .officeAddress(address)
@@ -268,22 +278,31 @@ public SignUpResponse signupAdmin(AdminSignUpRequest req, MultipartFile signImag
                         .point(point)
                         .latitude(lat)
                         .longitude(lng)
-                        .build()
-        );
+                        .build());
 
         // 4) 토큰 발급
         Tokens tokens = jwtUtil.issueTokens(
                 member.getId(),
                 req.getCommonAuth().getEmail(),
                 UserRole.ADMIN,
-                adapter.authRealmValue()
-        );
+                adapter.authRealmValue());
+
+        // 5) Admin 정보로 직접 UserBasicInfo 생성 + null check
+        String department = admin.getDepartment() != null ? admin.getDepartment().getDisplayName() : null;
+        String major = admin.getMajor() != null ? admin.getMajor().getDisplayName() : null;
+        UserBasicInfo basicInfo = UserBasicInfo.builder()
+                .name(admin.getName())
+                .university(admin.getUniversity().getDisplayName())
+                .department(department)
+                .major(major)
+                .build();
 
         return SignUpResponse.builder()
                 .memberId(member.getId())
                 .role(UserRole.ADMIN)
                 .status(member.getIsActivated())
                 .tokens(tokens)
+                .basicInfo(basicInfo)
                 .build();
     }
 
@@ -305,7 +324,8 @@ private EnrollmentStatus parseEnrollmentStatus(String status) {
     }
 
     public Point toPoint(Double lat, Double lng) {
-        if (lat == null || lng == null) return null;
+        if (lat == null || lng == null)
+            return null;
         Point p = geometryFactory.createPoint(new Coordinate(lng, lat)); // x=lng, y=lat
         p.setSRID(4326);
         return p;
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Department.java b/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
index 66ce387..668c9dd 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
@@ -1,5 +1,15 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum Department {
-    IT
+    IT("IT대학");
+
+    private final String displayName;
+
+    Department(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
index 9d6f197..2fbf076 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
@@ -1,21 +1,27 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum Major {
-    SW(Department.IT), // 소프트웨어학부
-    GM(Department.IT), // 글로벌미디어학과
-    COM(Department.IT), // 컴퓨터학부
-    EE(Department.IT), // 전자정보공학부
-    IP(Department.IT), // 정보보호학과
-    AI(Department.IT), // AI융합학과
-    MB(Department.IT); // 미디어경영학과
+    SW(Department.IT, "소프트웨어학부"),
+    GM(Department.IT, "글로벌미디어학과"),
+    COM(Department.IT, "컴퓨터학부"),
+    EE(Department.IT, "전자정보공학부"),
+    IP(Department.IT, "정보보호학과"),
+    AI(Department.IT, "AI융합학과"),
+    MB(Department.IT, "미디어경영학과");
 
     private final Department department;
+    private final String displayName;
 
-    Major(Department department) {
+    Major(Department department, String displayName) {
         this.department = department;
+        this.displayName = displayName;
     }
 
     public Department getDepartment() {
         return department;
     }
+
+    public String getDisplayName() {
+        return displayName;
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/University.java b/src/main/java/com/assu/server/domain/user/entity/enums/University.java
index 1270336..ab18969 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/University.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/University.java
@@ -1,5 +1,15 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum University {
-    SSU
+    SSU("숭실대학교");
+
+    private final String displayName;
+
+    University(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
 }

From 4a7172cb9ae71b0f2861674498bfcdccdce557d7 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Sun, 14 Sep 2025 17:16:17 +0900
Subject: [PATCH 173/270] =?UTF-8?q?[FIX/#82]=20=EC=9E=90=EC=9E=98=ED=95=9C?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../config/CertifyWebSocketConfig.java        |  9 ++-
 .../config/StompAuthChannelInterceptor.java   | 15 ++++
 .../controller/CertificationController.java   | 24 +++---
 .../GroupCertificationController.java         | 48 ++++++++++++
 .../controller/WebSocketTestController.java   | 16 ++++
 .../converter/CertificationConverter.java     |  2 +
 .../dto/CertificationRequestDTO.java          | 16 ++--
 .../dto/GroupSessionRequest.java              | 21 ++++++
 .../CertificationWebsocketHandler.java        | 41 ++++++++++
 .../service/CertificationService.java         |  3 +-
 .../service/CertificationServiceImpl.java     | 10 ++-
 .../domain/map/converter/MapConverter.java    | 74 +++++++++++++++++++
 .../server/domain/map/dto/MapResponseDTO.java |  1 +
 .../domain/map/service/MapServiceImpl.java    | 41 +++++++++-
 .../controller/PaperController.java           |  1 -
 .../controller/PartnershipController.java     | 10 ++-
 .../converter/PartnershipConverter.java       |  5 +-
 .../dto/PartnershipRequestDTO.java            |  2 +
 .../repository/GoodsRepository.java           |  7 ++
 .../service/PartnershipServiceImpl.java       | 27 ++++++-
 .../user/controller/StudentController.java    | 33 +++++++--
 .../server/domain/user/entity/Student.java    |  5 +-
 .../PartnershipUsageRepository.java           |  8 ++
 .../domain/user/service/StudentService.java   |  5 ++
 .../user/service/StudentServiceImpl.java      | 44 +++++++++++
 .../apiPayload/code/status/SuccessStatus.java |  1 +
 src/main/resources/application.yml            |  4 +
 27 files changed, 425 insertions(+), 48 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java
 create mode 100644 src/main/java/com/assu/server/domain/certification/controller/WebSocketTestController.java
 create mode 100644 src/main/java/com/assu/server/domain/certification/dto/GroupSessionRequest.java
 create mode 100644 src/main/java/com/assu/server/domain/certification/handler/CertificationWebsocketHandler.java
 create mode 100644 src/main/java/com/assu/server/domain/map/converter/MapConverter.java

diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 75c9ae3..85d7fc2 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -1,8 +1,10 @@
 package com.assu.server.domain.certification.config;
 
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.messaging.simp.config.ChannelRegistration;
 import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
 import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
 import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@@ -18,17 +20,18 @@ public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer
 	@Override
 	public void configureMessageBroker(MessageBrokerRegistry config) {
 		config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
-		config.setApplicationDestinationPrefixes("/certification"); // 클라이언트가 인증 요청을 보내는 주소
+		config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
 	}
 
 	@Override
 	public void registerStompEndpoints(StompEndpointRegistry registry) {
-		registry.addEndpoint("/ws")           // 클라이언트 WebSocket 연결 주소
-			.setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
+		registry.addEndpoint("/ws").setAllowedOriginPatterns("*");          // 클라이언트 WebSocket 연결 주소
+			// .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
 	}
 
 	@Override
 	public void configureClientInboundChannel(ChannelRegistration registration) {
 		registration.interceptors(stompAuthChannelInterceptor);
 	}
+
 }
diff --git a/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
index d2eb1c0..b5da450 100644
--- a/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
+++ b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
@@ -7,7 +7,9 @@
 import org.springframework.messaging.support.ChannelInterceptor;
 import org.springframework.security.core.Authentication;
 import org.springframework.stereotype.Component;
+import lombok.extern.slf4j.Slf4j; // SLF4j 로그 추가
 
+@Slf4j // SLF4j 어노테이션 추가
 @Component
 @RequiredArgsConstructor
 public class StompAuthChannelInterceptor implements ChannelInterceptor {
@@ -17,19 +19,32 @@ public class StompAuthChannelInterceptor implements ChannelInterceptor {
 	@Override
 	public Message preSend(Message message, MessageChannel channel) {
 		StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
+		log.info("StompCommand: {}", accessor.getCommand()); // StompCommand 로그 추가
 
 		if (StompCommand.CONNECT.equals(accessor.getCommand())) {
+			log.info("CONNECT command received.");
 			// 프론트에서 connect 시 Authorization 헤더 넣어야 함
 			String authHeader = accessor.getFirstNativeHeader("Authorization");
+			log.info("Authorization Header: {}", authHeader); // Authorization 헤더 로그 추가
+
 			if (authHeader != null && authHeader.startsWith("Bearer ")) {
 				String token = jwtUtil.getTokenFromHeader(authHeader);
+				log.info("Extracted Token: {}", token); // 추출된 토큰 로그 추가
 
 				// JwtUtil 이용해서 Authentication 복원
 				Authentication authentication = jwtUtil.getAuthentication(token);
+				log.info("Authentication restored: {}", authentication); // 복원된 인증 정보 로그 추가
 
 				// WebSocket 세션에 Authentication(UserPrincipal) 저장
 				accessor.setUser(authentication);
+				log.info("User principal set on accessor.");
+			} else {
+				log.warn("Authorization header is missing or not in Bearer format.");
 			}
+		} else if (StompCommand.SEND.equals(accessor.getCommand())) {
+			// SEND 명령어에 대한 로그 추가 (메시지 전송 시)
+			Object payload = message.getPayload();
+			log.info("SEND command received. Destination: {}, Payload: {}", accessor.getDestination(), payload);
 		}
 
 		return message;
diff --git a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
index 3d26da6..d86c928 100644
--- a/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
+++ b/src/main/java/com/assu/server/domain/certification/controller/CertificationController.java
@@ -60,27 +60,27 @@ public ResponseEntity> certifyGroup(
-		CertificationRequestDTO.groupSessionRequest dto  , PrincipalDetails pd
-
-	) {
-		certificationService.handleCertification(dto, pd.getMember());
-
-		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_CERTIFICATION_SUCCESS, null));
-	}
+	// @MessageMapping("/certify")
+	// @Operation(summary = "그룹 세션 인증 api", description = "그룹에 대한 세션 인증 요청을 보냅니다.")
+	// public ResponseEntity> certifyGroup(
+	// 	CertificationRequestDTO.groupSessionRequest dto  , PrincipalDetails pd
+	//
+	// ) {
+	// 	certificationService.handleCertification(dto, pd.getMember());
+	//
+	// 	return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.GROUP_CERTIFICATION_SUCCESS, null));
+	// }
 
 	@PostMapping("/certification/personal")
 	@Operation(summary = "개인 인증 api", description = "사실 크게 필요없는데, 제휴 내역 통계를 위해 데이터를 post하는 api 입니다. "
 		+ "가게 별 제휴를 조회하고 people값이 null 인 제휴를 선택한 경우 그룹 인증 대신 요청하는 api 입니다.")
-	public ResponseEntity> personalCertification(
+	public ResponseEntity> personalCertification(
 		@AuthenticationPrincipal PrincipalDetails pd,
 		@RequestBody CertificationRequestDTO.personalRequest dto
 	) {
 		certificationService.certificatePersonal(dto, pd.getMember());
 
-		return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.PERSONAL_CERTIFICATION_SUCCESS));
+		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PERSONAL_CERTIFICATION_SUCCESS, "null"));
 	}
 
 }
diff --git a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java
new file mode 100644
index 0000000..ee919c5
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java
@@ -0,0 +1,48 @@
+package com.assu.server.domain.certification.controller;
+
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Controller;
+
+import com.assu.server.domain.certification.dto.GroupSessionRequest;
+import com.assu.server.domain.certification.service.CertificationService;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.global.util.PrincipalDetails;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Controller // STOMP 메시지 처리를 위한 컨트롤러
+@RequiredArgsConstructor
+public class GroupCertificationController {
+
+	private final CertificationService certificationService;
+
+	@MessageMapping("/certify")
+	public void certifyGroup(@Payload GroupSessionRequest dto, SimpMessageHeaderAccessor headerAccessor) {
+		try {
+			log.info("### SUCCESS ### 인증 요청 메시지 수신 - adminId: {}, sessionId: {}", dto.getAdminId(), dto.getSessionId());
+
+			// Authentication에서 Member 정보 추출
+			Authentication auth = (Authentication) headerAccessor.getUser();
+			if (auth != null && auth.getPrincipal() instanceof PrincipalDetails) {
+				PrincipalDetails principalDetails = (PrincipalDetails) auth.getPrincipal();
+				// 실제 비즈니스 로직 호출
+				certificationService.handleCertification(dto, principalDetails.getMember());
+				log.info("### SUCCESS ### 그룹 인증 처리 완료");
+			}
+		} catch (Exception e) {
+			log.error("### ERROR ### 인증 처리 실패", e);
+		}
+	}
+
+	// @MessageMapping("/certify")
+	// public void certifyGroup(SimpMessageHeaderAccessor headerAccessor) {
+	// 	log.info("### DEBUG ### 메서드 진입!");
+	// 	log.info("### DEBUG ### User: {}", headerAccessor.getUser());
+	// 	log.info("### DEBUG ### SessionId: {}", headerAccessor.getSessionId());
+	// }
+}
diff --git a/src/main/java/com/assu/server/domain/certification/controller/WebSocketTestController.java b/src/main/java/com/assu/server/domain/certification/controller/WebSocketTestController.java
new file mode 100644
index 0000000..7460e5c
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/certification/controller/WebSocketTestController.java
@@ -0,0 +1,16 @@
+package com.assu.server.domain.certification.controller;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.stereotype.Controller;
+
+@Slf4j
+@Controller
+public class WebSocketTestController {
+
+	@MessageMapping("/test")
+	public void test(@Payload String payload) {
+		log.info("### 테스트용 메시지 수신 성공! 페이로드: {}", payload);
+	}
+}
diff --git a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java
index 0e4c812..e4422cb 100644
--- a/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java
+++ b/src/main/java/com/assu/server/domain/certification/converter/CertificationConverter.java
@@ -4,6 +4,7 @@
 import com.assu.server.domain.certification.dto.CertificationRequestDTO;
 import com.assu.server.domain.certification.dto.CertificationResponseDTO;
 import com.assu.server.domain.certification.entity.AssociateCertification;
+import com.assu.server.domain.certification.entity.enums.SessionStatus;
 import com.assu.server.domain.member.entity.Member;
 import com.assu.server.domain.store.entity.Store;
 
@@ -12,6 +13,7 @@ public static AssociateCertification toAssociateCertification(CertificationReque
 		return AssociateCertification.builder()
 			.store(store)
 			.partner(store.getPartner())
+			.status(SessionStatus.OPENED)
 			.isCertified(false)
 			.peopleNumber(dto.getPeople())
 			.tableNumber(dto.getTableNumber())
diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java
index 971f337..0ba578c 100644
--- a/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationRequestDTO.java
@@ -1,7 +1,9 @@
 package com.assu.server.domain.certification.dto;
 
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
 import lombok.Getter;
-
+import lombok.NoArgsConstructor;
 
 public class CertificationRequestDTO {
 
@@ -20,9 +22,11 @@ public static class personalRequest{
 		Integer tableNumber;
 	}
 
-	@Getter
-	public static class groupSessionRequest{
-		Long adminId;
-		Long sessionId;
-	}
+	// @Getter
+	// @NoArgsConstructor(access = AccessLevel.PROTECTED)
+	// @AllArgsConstructor
+	// public static class groupSessionRequest {
+	// 	private Long adminId;
+	// 	private Long sessionId;
+	// }
 }
diff --git a/src/main/java/com/assu/server/domain/certification/dto/GroupSessionRequest.java b/src/main/java/com/assu/server/domain/certification/dto/GroupSessionRequest.java
new file mode 100644
index 0000000..4099e37
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/certification/dto/GroupSessionRequest.java
@@ -0,0 +1,21 @@
+package com.assu.server.domain.certification.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@NoArgsConstructor
+@Setter
+public class GroupSessionRequest {
+	Long adminId;
+	Long sessionId;
+
+	@Override
+	public String toString() {
+		return "GroupSessionRequest{" +
+			"adminId=" + adminId +
+			", sessionId=" + sessionId +
+			'}';
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/certification/handler/CertificationWebsocketHandler.java b/src/main/java/com/assu/server/domain/certification/handler/CertificationWebsocketHandler.java
new file mode 100644
index 0000000..7009e67
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/certification/handler/CertificationWebsocketHandler.java
@@ -0,0 +1,41 @@
+// package com.assu.server.domain.certification.handler;
+//
+// import org.springframework.stereotype.Component;
+// import org.springframework.web.socket.CloseStatus;
+// import org.springframework.web.socket.TextMessage;
+// import org.springframework.web.socket.WebSocketSession;
+// import org.springframework.web.socket.handler.TextWebSocketHandler;
+//
+// @Component
+// public class CertificationWebsocketHandler extends TextWebSocketHandler {
+//
+// 	@Override
+// 	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+// 		// 클라이언트 연결이 성공적으로 수립되었을 때 호출됩니다.
+// 		System.out.println("Client connected: " + session.getId());
+// 	}
+//
+// 	@Override
+// 	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+// 		// 클라이언트로부터 텍스트 메시지를 받았을 때 호출됩니다.
+// 		String payload = message.getPayload();
+// 		System.out.println("Message received from " + session.getId() + ": " + payload);
+//
+// 		// 받은 메시지를 다시 클라이언트에게 보내거나 다른 로직을 처리합니다.
+// 		session.sendMessage(new TextMessage("Echo: " + payload));
+// 	}
+//
+// 	@Override
+// 	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+// 		// 클라이언트 연결이 종료되었을 때 호출됩니다.
+// 		System.out.println("Client disconnected: " + session.getId() + " with status " + status.getCode());
+// 	}
+//
+// 	@Override
+// 	public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+// 		// 전송 오류가 발생했을 때 호출됩니다.
+// 		System.err.println("Transport error for session " + session.getId() + ": " + exception.getMessage());
+// 		// 필요한 경우 연결을 종료하거나 오류를 처리합니다.
+// 		session.close(CloseStatus.SERVER_ERROR);
+// 	}
+// }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java
index 4e68dfe..cde9e47 100644
--- a/src/main/java/com/assu/server/domain/certification/service/CertificationService.java
+++ b/src/main/java/com/assu/server/domain/certification/service/CertificationService.java
@@ -2,13 +2,14 @@
 
 import com.assu.server.domain.certification.dto.CertificationRequestDTO;
 import com.assu.server.domain.certification.dto.CertificationResponseDTO;
+import com.assu.server.domain.certification.dto.GroupSessionRequest;
 import com.assu.server.domain.member.entity.Member;
 
 public interface CertificationService {
 
 	CertificationResponseDTO.getSessionIdResponse getSessionId(CertificationRequestDTO.groupRequest dto, Member member);
 
-	void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member);
+	void handleCertification(GroupSessionRequest dto, Member member);
 
 	void certificatePersonal(CertificationRequestDTO.personalRequest dto, Member member);
 }
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 0092308..01946fc 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
@@ -15,6 +15,7 @@
 import com.assu.server.domain.certification.dto.CertificationProgressResponseDTO;
 import com.assu.server.domain.certification.dto.CertificationRequestDTO;
 import com.assu.server.domain.certification.dto.CertificationResponseDTO;
+import com.assu.server.domain.certification.dto.GroupSessionRequest;
 import com.assu.server.domain.certification.entity.AssociateCertification;
 import com.assu.server.domain.certification.entity.enums.SessionStatus;
 import com.assu.server.domain.certification.repository.AssociateCertificationRepository;
@@ -70,7 +71,7 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId(
 
 		sessionManager.openSession(sessionId);
 		// 세션 생성 직후 만료 시간을 5분으로 설정
-		timeoutManager.scheduleTimeout(sessionId, Duration.ofMinutes(5));
+		timeoutManager.scheduleTimeout(sessionId, Duration.ofMinutes(100));// TODO: 나중에 5분으로 변경
 
 		// 세션 여는 대표자는 제일 먼저 인증
 		sessionManager.addUserToSession(sessionId, userId);
@@ -80,7 +81,7 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId(
 	}
 
 	@Override
-	public void handleCertification(CertificationRequestDTO.groupSessionRequest dto, Member member) {
+	public void handleCertification(GroupSessionRequest dto, Member member) {
 		Long userId = member.getId();
 
 		// 제휴 대상인지 확인하기
@@ -107,8 +108,11 @@ public void handleCertification(CertificationRequestDTO.groupSessionRequest dto,
 			throw new GeneralException(ErrorStatus.SESSION_NOT_OPENED);
 
 		boolean isDoubledUser= sessionManager.hasUser(sessionId, userId);
-		if(isDoubledUser)
+		if(isDoubledUser) {
+			messagingTemplate.convertAndSend("/certification/progress/"+sessionId,
+				new CertificationProgressResponseDTO("progress", 0,"doubled member", null));
 			throw new GeneralException(ErrorStatus.DOUBLE_CERTIFIED_USER);
+		}
 
 		sessionManager.addUserToSession(sessionId, userId);
 		int currentCertifiedNumber = sessionManager.getCurrentUserCount(sessionId);
diff --git a/src/main/java/com/assu/server/domain/map/converter/MapConverter.java b/src/main/java/com/assu/server/domain/map/converter/MapConverter.java
new file mode 100644
index 0000000..1d1a9af
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/map/converter/MapConverter.java
@@ -0,0 +1,74 @@
+package com.assu.server.domain.map.converter;
+
+import java.util.List;
+
+import com.assu.server.domain.partnership.entity.Goods;
+import com.assu.server.domain.partnership.entity.PaperContent;
+import com.assu.server.domain.partnership.entity.enums.CriterionType;
+import com.assu.server.domain.partnership.entity.enums.OptionType;
+
+public class MapConverter {
+
+
+
+
+	private static List extractGoods(PaperContent content) {
+		if (content.getOptionType() == OptionType.SERVICE ) {
+			return content.getGoods().stream()
+				.map(Goods::getBelonging)
+				.toList();
+		}
+		return null;
+	}
+
+	private static Integer extractPeople(PaperContent content) {
+		if (content.getCriterionType() == CriterionType.HEADCOUNT) {
+			return content.getPeople();
+		}
+		return null;
+	}
+
+	private static String buildPaperContentText(PaperContent content, List goodsList, Integer peopleValue) {
+		String result = "";
+
+		boolean isGoodsSingle = goodsList != null && goodsList.size() == 1;
+		boolean isGoodsMultiple = goodsList != null && goodsList.size() > 1;
+
+		// 1. HEADCOUNT + SERVICE + 여러 개 goods
+		if (content.getCriterionType() == CriterionType.HEADCOUNT &&
+			content.getOptionType() == OptionType.SERVICE &&
+			isGoodsMultiple) {
+			result = peopleValue + "명 이상 식사 시 " + content.getCategory() + " 제공";
+		}
+		// 2. HEADCOUNT + SERVICE + 단일 goods
+		else if (content.getCriterionType() == CriterionType.HEADCOUNT &&
+			content.getOptionType() == OptionType.SERVICE &&
+			isGoodsSingle) {
+			result = peopleValue + "명 이상 식사 시 " + goodsList.get(0) + " 제공";
+		}
+		// 3. HEADCOUNT + DISCOUNT
+		else if (content.getCriterionType() == CriterionType.HEADCOUNT &&
+			content.getOptionType() == OptionType.DISCOUNT) {
+			result = peopleValue + "명 이상 식사 시 " + content.getDiscount() + "% 할인";
+		}
+		// 4. PRICE + SERVICE + 여러 개 goods
+		else if (content.getCriterionType() == CriterionType.PRICE &&
+			content.getOptionType() == OptionType.SERVICE &&
+			isGoodsMultiple) {
+			result = content.getCost() + "원 이상 주문 시 " + content.getCategory() + " 제공";
+		}
+		// 5. PRICE + SERVICE + 단일 goods
+		else if (content.getCriterionType() == CriterionType.PRICE &&
+			content.getOptionType() == OptionType.SERVICE &&
+			isGoodsSingle) {
+			result = content.getCost() + "원 이상 주문 시 " + goodsList.get(0) + " 제공";
+		}
+		// 6. PRICE + DISCOUNT
+		else if (content.getCriterionType() == CriterionType.PRICE &&
+			content.getOptionType() == OptionType.DISCOUNT) {
+			result = content.getCost() + "원 이상 주문 시 " + content.getDiscount() + "% 할인";
+		}
+
+		return result;
+	}
+}
diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
index 90d78f2..195dacd 100644
--- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
@@ -48,6 +48,7 @@ public static class AdminMapResponseDTO {
     public static class StoreMapResponseDTO {
         private Long storeId;
         private Long adminId;
+        private String adminName;
         private String name;
         private String address;
         private Integer rate;
diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index 58b0af3..ae1bc8e 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -7,8 +7,11 @@
 import com.assu.server.domain.map.dto.MapResponseDTO;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
+import com.assu.server.domain.partnership.entity.Goods;
 import com.assu.server.domain.partnership.entity.Paper;
 import com.assu.server.domain.partnership.entity.PaperContent;
+import com.assu.server.domain.partnership.entity.enums.OptionType;
+import com.assu.server.domain.partnership.repository.GoodsRepository;
 import com.assu.server.domain.partnership.repository.PaperContentRepository;
 import com.assu.server.domain.partnership.repository.PaperRepository;
 import com.assu.server.domain.store.entity.Store;
@@ -16,6 +19,8 @@
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.config.KakaoLocalClient;
 import com.assu.server.global.exception.DatabaseException;
+import com.assu.server.global.exception.GeneralException;
+
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.locationtech.jts.geom.Coordinate;
@@ -33,10 +38,10 @@ public class MapServiceImpl implements MapService {
     private final AdminRepository adminRepository;
     private final PartnerRepository partnerRepository;
     private final StoreRepository storeRepository;
-    private final KakaoLocalClient kakaoLocalClient;
     private final PaperContentRepository paperContentRepository;
     private final PaperRepository paperRepository;
     private final GeometryFactory geometryFactory;
+    private final GoodsRepository goodsRepository;
 
     @Override
     public List getPartners(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) {
@@ -100,15 +105,19 @@ public List getStores(MapRequestDTO.ViewOnMa
             boolean hasPartner = (s.getPartner() != null);
 
             PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId())
-                    .orElse(null);
+                    .orElseThrow(
+                        () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT)
+                    );
 
             Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
                     .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
                     .orElse(null);
 
+            Admin admin = adminRepository.findById(adminId).orElse(null);
             return MapResponseDTO.StoreMapResponseDTO.builder()
                     .storeId(s.getId())
                     .adminId(adminId)
+                    .adminName(admin.getName())
                     .name(s.getName())
                     .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress())
                     .rate(s.getRate())
@@ -132,14 +141,38 @@ public List searchStores(String keyword) {
         return stores.stream().map(s -> {
             boolean hasPartner = s.getPartner() != null;
             PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId())
-                    .orElse(null);
+                    .orElseThrow(
+                        () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT)
+                    );
 
             Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
                     .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
                     .orElse(null);
 
+            Admin admin = adminRepository.findById(adminId).orElse(null);
+
+            String finalCategory = null;
+
+            if (content != null) {
+                // 2. content에 카테고리가 이미 존재하면 그 값을 사용합니다.
+                if (content.getCategory() != null) {
+                    finalCategory = content.getCategory();
+                }
+                // 3. 카테고리가 없고, 옵션 타입이 SERVICE인 경우 Goods를 조회합니다.
+                else if (content.getOptionType() == OptionType.SERVICE) {
+                    List goods = goodsRepository.findByContentId(content.getId());
+
+                    // 4. (가장 중요) goods 리스트가 비어있지 않은지 반드시 확인합니다.
+                    if (!goods.isEmpty()) {
+                        finalCategory = goods.get(0).getBelonging();
+                    }
+                    // goods가 비어있으면 finalCategory는 그대로 null로 유지됩니다.
+                }
+            }
+
             return MapResponseDTO.StoreMapResponseDTO.builder()
                     .storeId(s.getId())
+                    .adminName(admin.getName())
                     .adminId(adminId)
                     .name(s.getName())
                     .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress())
@@ -148,7 +181,7 @@ public List searchStores(String keyword) {
                     .optionType(content != null ? content.getOptionType() : null)
                     .people(content != null ? content.getPeople() : null)
                     .cost(content != null ? content.getCost() : null)
-                    .category(content != null ? content.getCategory() : null)
+                    .category(finalCategory)
                     .discountRate(content != null ? content.getDiscount() : null)
                     .hasPartner(hasPartner)
                     .latitude(s.getLatitude())
diff --git a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java
index b3c3523..6ae860c 100644
--- a/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java
+++ b/src/main/java/com/assu/server/domain/partnership/controller/PaperController.java
@@ -26,7 +26,6 @@
 public class PaperController {
 
 	private final PaperQueryService paperQueryService;
-	private final MemberRepository memberRepository;
 
 	@GetMapping("/store/{storeId}/papers")
 	@Operation(summary = "유저에게 적용 가능한 제휴 컨텐츠 조회", description = "유저가 속한 단과대, 학부 admin_id과 store_id 를 가진 제휴 컨텐츠 제공")
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 ff37634..0a1ebdd 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
@@ -7,10 +7,13 @@
 import org.springframework.web.bind.annotation.RestController;
 
 import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.notification.service.NotificationCommandService;
 import com.assu.server.domain.partnership.dto.PaperResponseDTO;
 import com.assu.server.domain.partnership.dto.PartnershipRequestDTO;
 import com.assu.server.domain.partnership.dto.PartnershipResponseDTO;
 import com.assu.server.domain.partnership.service.PartnershipService;
+import com.assu.server.domain.store.entity.Store;
+import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import com.assu.server.global.util.PrincipalDetails;
@@ -32,18 +35,19 @@
 public class PartnershipController {
 
 	private final PartnershipService partnershipService;
+    private final NotificationCommandService notificationCommandService;
 
+    private final StoreRepository storeRepository;
 
 	@PostMapping("/usage")
 	@Operation(summary= "유저의 인증 후 최종적으로 호출", description = "인증완료 화면 전에 바로 호출되어 유저의 제휴 내역에 데이터가 들어가게 됩니다. (개인 인증인 경우도 포함됩니다.)")
-	public ResponseEntity> finalPartnershipRequest(
+	public ResponseEntity> finalPartnershipRequest(
 		@AuthenticationPrincipal PrincipalDetails userDetails,@RequestBody PartnershipRequestDTO.finalRequest dto
 	) {
 		Member member = userDetails.getMember();
-
 		partnershipService.recordPartnershipUsage(dto, member);
 
-		return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.USER_PAPER_REQUEST_SUCCESS));
+		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.USER_PAPER_REQUEST_SUCCESS, null));
 	}
 
 
diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index 51be039..b80e9a4 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -26,12 +26,13 @@
 
 public class PartnershipConverter {
 
-	public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Student student) {
+	public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Student student, Long paperId) {
 		return PartnershipUsage.builder()
 			.adminName(dto.getAdminName())
 			.date(LocalDate.now())
 			.place(dto.getPlaceName())
 			.student(student)
+			.paperId(paperId)
 			.isReviewed(false)
 			.contentId(dto.getContentId())
 			.partnershipContent(dto.getPartnershipContent())
@@ -128,7 +129,7 @@ public static PaperContentResponseDTO.storePaperContentResponse toContentRespons
 
 
 	private static List extractGoods(PaperContent content) {
-		if (content.getOptionType() == OptionType.SERVICE && content.getCategory() != null) {
+		if (content.getOptionType() == OptionType.SERVICE ) {
 			return content.getGoods().stream()
 				.map(Goods::getBelonging)
 				.toList();
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
index 1d9344c..9470648 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
@@ -14,6 +14,8 @@
 public class PartnershipRequestDTO {
     @Getter
     public static class finalRequest{
+        Long storeId;
+        String tableNumber;
         String adminName;
         String placeName;
         String partnershipContent;
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java
index bede045..31edb84 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java
@@ -1,7 +1,14 @@
 package com.assu.server.domain.partnership.repository;
 
+import java.util.List;
+import java.util.Optional;
+
 import com.assu.server.domain.partnership.entity.Goods;
 import org.springframework.data.jpa.repository.JpaRepository;
 
 public interface GoodsRepository extends JpaRepository {
+
+	List findByContentId(Long contentId);
+
+	List findByContentIdIn(List contentIds);
 }
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 8a18d2f..f70a5f8 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
@@ -8,6 +8,8 @@
 import org.springframework.stereotype.Service;
 
 import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.notification.repository.NotificationRepository;
+import com.assu.server.domain.notification.service.NotificationCommandService;
 import com.assu.server.domain.partnership.converter.PartnershipConverter;
 import com.assu.server.domain.partnership.dto.PartnershipRequestDTO;
 import com.assu.server.domain.user.entity.PartnershipUsage;
@@ -33,6 +35,7 @@
 import com.assu.server.domain.store.repository.StoreRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
+import com.assu.server.global.exception.GeneralException;
 import com.assu.server.infra.s3.AmazonS3Manager;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
@@ -50,14 +53,21 @@ public class PartnershipServiceImpl implements PartnershipService {
 
 	private final PartnershipUsageRepository partnershipUsageRepository;
 	private final StudentRepository studentRepository;
+    private final PaperContentRepository contentRepository;
+    private final NotificationCommandService notificationService;
+
 
 	public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){
 
 
 		List usages = new ArrayList<>();
 
+        PaperContent content = contentRepository.findById(dto.getContentId()).orElseThrow(
+            () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT)
+        );
+        Long paperId = content.getPaper().getId();
 		// 1) 요청한 member 본인
-		usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile()));
+		usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile(), paperId));
         member.getStudentProfile().setStamp();
 
 		List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList());
@@ -65,14 +75,23 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe
 		for (Long userId : userIds) {
             if(userId != member.getId()){
                 Student student = studentRepository.getReferenceById(userId);
-                usages.add(PartnershipConverter.toPartnershipUsage(dto, student));
+                usages.add(PartnershipConverter.toPartnershipUsage(dto, student, paperId));
                 student.setStamp();
             }
 
 		}
 
-		partnershipUsageRepository.saveAll(usages);
-
+        Store store = storeRepository.findById(dto.getStoreId()).orElseThrow(
+            () -> new GeneralException(ErrorStatus.NO_SUCH_STORE)
+        );
+        Partner partner = store.getPartner();
+        if (partner != null) {
+            Long partnerId = partner.getId();
+            notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent());
+            partnershipUsageRepository.saveAll(usages);
+        } else {
+            throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER);
+        }
 	}
 
 
diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
index d50db89..d1c7517 100644
--- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java
+++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java
@@ -1,5 +1,7 @@
 package com.assu.server.domain.user.controller;
 
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -32,14 +34,12 @@ public class StudentController {
 		summary = "월별 제휴 사용내역 조회 API",
 		description = "# [v1.0 (2025-09-09)](https://www.notion.so/_-2241197c19ed8134bd49d8841e841634?source=copy_link)\n" +
 			"- `multipart/form-data`로 호출합니다.\n" +
-			"- 처리: 정보 바탕으로 sessionManager에 session생성\n" +
-			"- 성공 시 201(Created)과 생성된 memberId 반환.\n" +
 			"\n**Request Parts:**\n" +
-			"  - `storeId` (Long, required): 스토어 id\n" +
 			"  - `year` (Integer, required): 년도\n" +
 			"  - `month` (Long, required): 월\n"+
 			"\n**Response:**\n" +
 			"  - 성공 시 partnership Usage 내역 반환 \n"+
+			"  - 해당 storeId, storeName 반환"+
 			"  - 해당 월에 사용한 제휴 수 반환"
 	)
 	public ResponseEntity> getMyPartnership(
@@ -50,14 +50,37 @@ public ResponseEntity> getMyPartn
 		return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.PARTNERSHIP_HISTORY_SUCCESS, result));
 	}
 
+	@GetMapping("/usage")
+	@Operation(
+		summary = "월별 제휴 사용내역 조회 API",
+		description = "# [v1.0 (2025-09-10)](https://www.notion.so/_-24c1197c19ed809a9d81e8f928e8355f?source=copy_link)\n" +
+			"- `multipart/form-data`로 호출합니다.\n" +
+			"\n**Request:**\n" +
+			"  - page : (Int, required) 이상의 정수 \n" +
+			"  - size : (Int, required) 기본 값 10 \n" +
+			"  - sort : (String, required) createdAt,desc 문자열로 입력\n" +
+			"\n**Response:**\n" +
+			"  - 성공 시 리뷰 되지 않은 partnership Usage 내역 반환 \n"+
+			"  - StudentResponseTO.UsageDetailDTO 객체 반환 \n"
+
+	)
+	public ResponseEntity>> getUnreviewedUsage(
+		@AuthenticationPrincipal PrincipalDetails pd,
+		Pageable pageable
+	){
+		return ResponseEntity.ok(BaseResponse
+			.onSuccess(SuccessStatus.UNREVIEWED_HISTORY_SUCCESS,
+				studentService.getUnreviewedUsage(pd.getId(), pageable)));
+	}
+
 
 
 
 	@Operation(
 		summary = "사용자 stamp 개수 조회 API",
-		description = "# [v1.0 (2025-09-09)](https://www.notion.so/_-2241197c19ed8134bd49d8841e841634?source=copy_link)\n" +
+		description = "# [v1.0 (2025-09-09)](https://www.notion.so/2691197c19ed805c980dd546adee9301?source=copy_link)\n" +
 			"- `multipart/form-data`로 호출합니다.\n" +
-			"- 처리: 정보 바탕으로 sessionManager에 session생성\n" +
+			"- login 필요 "+
 			"\n**Response:**\n" +
 			"  - stamp 개수 반환 \n"
 	)
diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java
index b8c1cb4..e7ee270 100644
--- a/src/main/java/com/assu/server/domain/user/entity/Student.java
+++ b/src/main/java/com/assu/server/domain/user/entity/Student.java
@@ -49,10 +49,7 @@ public void setMember(Member member) {
     }
 
     public void setStamp() {
-        if (this.stamp == 10)
-            this.stamp = 1;
-        else
-            this.stamp++;
+        this.stamp++;
     }
 
     /**
diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
index 7553436..d07c216 100644
--- a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
+++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java
@@ -3,6 +3,8 @@
 import java.util.List;
 import java.util.Optional;
 
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
@@ -34,4 +36,10 @@ List findByYearAndMonth(
 	);
 
 	Optional findById(Long id);
+
+
+	@Query("SELECT pu FROM PartnershipUsage pu " +
+		"WHERE pu.student.id = :studentId " +
+		"AND (pu.isReviewed = false)")
+	Page findByUnreviewedUsage(Long studentId, Pageable pageable);
 }
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java
index 9c7a214..595613f 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentService.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java
@@ -1,8 +1,13 @@
 package com.assu.server.domain.user.service;
 
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
 import com.assu.server.domain.user.dto.StudentResponseDTO;
 
 public interface StudentService {
 	StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month);
     StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId);//조회
+
+	Page getUnreviewedUsage(Long memberId, Pageable pageable);
 }
diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
index 30232f3..64f7b5e 100644
--- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java
@@ -17,6 +17,10 @@
 import com.assu.server.global.exception.DatabaseException;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 
 @Service
@@ -68,4 +72,44 @@ public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int yea
 			)
 			.build();
 	}
+
+
+	@Override
+	@Transactional
+	public Page getUnreviewedUsage(Long memberId, Pageable pageable) {
+		// 프론트에서 1-based 페이지를 보낸 경우 0-based 로 보정
+		pageable = PageRequest.of(
+			Math.max(pageable.getPageNumber() - 1, 0),
+			pageable.getPageSize(),
+			pageable.getSort()
+		);
+
+		Page contentList =
+			partnershipUsageRepository.findByUnreviewedUsage(memberId, pageable);
+
+		return contentList.map(u -> {
+			// 1. partnershipUsage의 paperContentId 로 paperContent 조회
+			PaperContent paperContent = paperContentRepository.findById(u.getContentId())
+				.orElse(null);
+
+			// 2. store 추출
+			Store store = (paperContent != null) ? paperContent.getPaper().getStore() : null;
+
+			// 3. 날짜 포맷팅
+			LocalDateTime ld = u.getCreatedAt();
+			String formatDate = ld.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
+
+			return StudentResponseDTO.UsageDetailDTO.builder()
+				.partnershipUsageId(u.getId())
+				.adminName(u.getAdminName())
+				.storeName(u.getPlace())
+				.usedAt(formatDate)
+				.benefitDescription(u.getPartnershipContent())
+				.isReviewed(u.getIsReviewed())
+				.storeId((store != null) ? store.getId() : null) // store null 체크
+				.partnerId((store != null && store.getPartner() != null) ? store.getPartner().getId() : null)
+				.build();
+		});
+	}
+
 }
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
index 4e185c4..0c41f66 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
@@ -37,6 +37,7 @@ public enum SuccessStatus implements BaseCode {
     USER_PAPER_REQUEST_SUCCESS(HttpStatus.OK, "PAPER202", "제휴 요청이 성공적으로 처리되었습니다."),
 
     PARTNERSHIP_HISTORY_SUCCESS(HttpStatus.OK, "PARTNERSHIP202", "월 별 제휴 사용내역이 성공적으로 조회되었습니다."),
+    UNREVIEWED_HISTORY_SUCCESS(HttpStatus.OK, "PARTNERSHIP203", "리뷰 되지 않은 제휴 사용내역이 성공적으로 조회되었습니다."),
 
     // 그룹 인증
     GROUP_SESSION_CREATE(HttpStatus.OK, "GROUP201", "인증 세션 생성 및 대표자 구독이 완료되었습니다."),
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index b6722ba..4c10342 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -32,6 +32,10 @@ logging:
   level:
     org.springframework.web: DEBUG
     org.springframework.web.client.DefaultRestClient: OFF
+    org.springframework.messaging.simp: DEBUG
+    org.springframework.messaging.handler: DEBUG
+    org.springframework.messaging: DEBUG
+    org.springframework.web.socket: DEBUG
 
 server:
   shutdown: graceful
\ No newline at end of file

From b2988698f385eb21146407b42804817b60b49d2c Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Sun, 14 Sep 2025 17:24:14 +0900
Subject: [PATCH 174/270] =?UTF-8?q?[FIX]=20=EC=95=8C=EB=A6=AC=EA=B3=A0=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?=
 =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/auth/service/LoginServiceImpl.java     |  6 +++---
 .../apiPayload/code/status/ErrorStatus.java       |  1 +
 .../server/infra/aligo/client/AligoSmsClient.java | 15 +++++++++++++--
 .../server/infra/aligo/dto/AligoSendResponse.java |  3 +++
 4 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
index 17b72ba..1c85152 100644
--- a/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/LoginServiceImpl.java
@@ -197,9 +197,9 @@ private UserBasicInfo buildUserBasicInfo(Member member) {
                 var admin = member.getAdminProfile();
                 if (admin != null) {
                     builder.name(admin.getName())
-                            .university(admin.getUniversity().getDisplayName())
-                            .department(admin.getDepartment().getDisplayName())
-                            .major(admin.getMajor().getDisplayName());
+                            .university(admin.getUniversity() != null ? admin.getUniversity().getDisplayName() : null)
+                            .department(admin.getDepartment() != null ? admin.getDepartment().getDisplayName() : null)
+                            .major(admin.getMajor() != null ? admin.getMajor().getDisplayName() : null);
                 }
             }
             case PARTNER -> {
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index fb776e8..af3be54 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -32,6 +32,7 @@ public enum ErrorStatus implements BaseErrorCode {
 
     // 알리고 SMS 전송 관련 에러
     FAILED_TO_SEND_SMS(HttpStatus.INTERNAL_SERVER_ERROR, "ALIGO500", "알리고 SMS 전송에 실패했습니다."),
+    FAILED_TO_PARSE_ALIGO(HttpStatus.INTERNAL_SERVER_ERROR, "ALIGO500", "알리고 SMS 파싱에 실패했습니다."),
 
     // 인증 에러
     NOT_VERIFIED_PHONE_NUMBER(HttpStatus.BAD_REQUEST,"AUTH_4007","전화번호 인증에 실패했습니다."),
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 9578538..d5c89c0 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
@@ -6,6 +6,7 @@
 import com.assu.server.infra.aligo.exception.AligoException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Component;
@@ -13,7 +14,9 @@
 import org.springframework.util.MultiValueMap;
 import org.springframework.web.reactive.function.BodyInserters;
 import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
 
+@Slf4j
 @Component
 @RequiredArgsConstructor
 public class AligoSmsClient {
@@ -47,13 +50,21 @@ public AligoSendResponse sendSms(String phoneNumber, String message, String name
                 .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                 .body(BodyInserters.fromFormData(params))
                 .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));
+                        })
+                )
                 .bodyToMono(String.class)
-                .block(); // 동기로 변환
+                .block();
 
         try {
             return objectMapper.readValue(body, AligoSendResponse.class);
         } catch (Exception e) {
-            throw new AligoException(ErrorStatus.FAILED_TO_SEND_SMS);
+            log.error("Aligo 응답 파싱 실패. 원본 body: {}", body, e);
+            throw new AligoException(ErrorStatus.FAILED_TO_PARSE_ALIGO);
         }
     }
 }
diff --git a/src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java b/src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java
index 2e77e7b..0b90348 100644
--- a/src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java
+++ b/src/main/java/com/assu/server/infra/aligo/dto/AligoSendResponse.java
@@ -7,4 +7,7 @@ public class AligoSendResponse {
     private String result_code; // 성공 여부
     private String message;     // 결과 메시지
     private String msg_id;      // 메시지 ID
+    private String success_cnt; // 성공 개수
+    private String error_cnt;   // 에러 개수
+    private String msg_type;    // 메시지 타입
 }

From 6efa14444b29578ef5c5c61f19cf7226fb1b130f Mon Sep 17 00:00:00 2001
From: kimyw1018 
Date: Sun, 14 Sep 2025 17:59:21 +0900
Subject: [PATCH 175/270] =?UTF-8?q?[MOD/#98]=20=EC=97=94=EB=93=9C=ED=8F=AC?=
 =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/mapping/controller/StudentAdminController.java       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
index 4286805..9487feb 100644
--- a/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
+++ b/src/main/java/com/assu/server/domain/mapping/controller/StudentAdminController.java
@@ -14,7 +14,7 @@
 
 @RestController
 @RequiredArgsConstructor
-@RequestMapping("/dashBoard")
+@RequestMapping("/admin/dashBoard")
 public class StudentAdminController {
     private final StudentAdminService studentAdminService;
     @Operation(

From 59ee7194a5fdf0e8476bdf0bc98d2efc557ef659 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Sun, 14 Sep 2025 19:31:19 +0900
Subject: [PATCH 176/270] =?UTF-8?q?refactor/#38=20-=20ci=20=ED=95=B4?=
 =?UTF-8?q?=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/test/resources/application-test.yml | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 3969c2a..ac859d1 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -27,12 +27,6 @@ jwt:
     push:
       enabled: false
 
-  messaging:
-    rabbit:
-      enabled: false
-    push:
-      enabled: false
-
 cloud:
   aws:
     s3:

From d0b47b71db32d564f92e56fab77f506a2354a92f Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Sun, 14 Sep 2025 19:31:52 +0900
Subject: [PATCH 177/270] =?UTF-8?q?refactor/#38=20-=20=EC=B5=9C=EC=8B=A0?=
 =?UTF-8?q?=20application-test.yml?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/test/resources/application-test.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index ac859d1..8a3197b 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -21,6 +21,11 @@ jwt:
   access-valid-seconds: 3600
   refresh-valid-seconds: 1209600
 
+assu:
+  security:
+    school-crypto:
+      base64-key: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=" #"dummy-base64-key"를 Base64로 인코딩한 값
+
   messaging:
     rabbit:
       enabled: false

From 18f8ac9a8a681ba44ccd1b2379e7abbb65fc2564 Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Sun, 14 Sep 2025 20:13:43 +0900
Subject: [PATCH 178/270] =?UTF-8?q?[MOD/#24]=20=20-=20=EC=A0=9C=ED=9C=B4?=
 =?UTF-8?q?=20API=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/PartnershipController.java     |  77 ++++++++--
 .../converter/PartnershipConverter.java       |  77 ++++++----
 .../dto/PartnershipRequestDTO.java            |   8 +-
 .../dto/PartnershipResponseDTO.java           |  43 ++++++
 .../domain/partnership/entity/Paper.java      |   4 +
 .../repository/GoodsRepository.java           |   9 ++
 .../repository/PaperRepository.java           |   7 +
 .../service/PartnershipService.java           |  22 ++-
 .../service/PartnershipServiceImpl.java       | 143 +++++++++++++++---
 .../controller/SuggestionController.java      |   6 +-
 .../converter/SuggestionConverter.java        |  15 +-
 .../service/SuggestionServiceImpl.java        |  23 ++-
 .../server/global/config/SecurityConfig.java  |   3 +-
 13 files changed, 361 insertions(+), 76 deletions(-)

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 ff37634..d6e628f 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
@@ -46,18 +46,16 @@ public ResponseEntity> finalPa
 		return ResponseEntity.ok(BaseResponse.onSuccessWithoutData(SuccessStatus.USER_PAPER_REQUEST_SUCCESS));
 	}
 
-
+    @PatchMapping("/proposal")
     @Operation(
-            summary = "제휴 제안서 작성 API",
+            summary = "제휴 제안서 내용 수정 API",
             description = "제공 서비스 종류(SERVICE, DISCOUNT), 서비스 제공 기준(PRICE, HEADCOUNT), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성해주세요."
     )
-    @PostMapping("/proposal")
-    public BaseResponse writePartnership(
+    public BaseResponse updatePartnership(
             @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request,
             @AuthenticationPrincipal PrincipalDetails pd
     ){
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.writePartnershipAsPartner(request, memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.updatePartnership(request, pd.getId()));
     }
 
     @Operation(
@@ -76,8 +74,7 @@ public BaseResponse createM
             MultipartFile contractImage,
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createManualPartnership(request, memberId, contractImage));
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createManualPartnership(request, pd.getId(), contractImage));
     }
 
     @Operation(
@@ -89,8 +86,7 @@ public BaseResponse> li
             @RequestParam(name = "all", defaultValue = "false") boolean all,
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForAdmin(all, memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForAdmin(all, pd.getId()));
     }
 
     @Operation(
@@ -102,8 +98,7 @@ public BaseResponse> li
             @RequestParam(name = "all", defaultValue = "false") boolean all,
             @AuthenticationPrincipal PrincipalDetails pd
     ) {
-        Long memberId = pd.getMember().getId();
-        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForPartner(all, memberId));
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForPartner(all, pd.getId()));
     }
 
     @Operation(
@@ -129,4 +124,62 @@ public BaseResponse updatePartnershipS
         return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.updatePartnershipStatus(partnershipId, request));
     }
 
+    @PostMapping("/proposal/draft")
+    @Operation(
+            summary = "제휴 제안서 초안 생성 API",
+            description = "현재 로그인한 관리자(Admin)가 내용이 비어있는 제휴 제안서를 초안 상태로 생성합니다."
+    )
+    public BaseResponse createDraftPartnership(
+            @RequestBody PartnershipRequestDTO.CreateDraftRequestDTO request,
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createDraftPartnership(request, pd.getId()));
+    }
+
+    @DeleteMapping("/proposal/delete/{paperId}")
+    @Operation(
+            summary = "제휴 제안서 삭제 API",
+            description = "특정 제휴 제안서(paperId)와 관련된 모든 데이터를 삭제합니다."
+    )
+    public BaseResponse deletePartnership(
+            @PathVariable Long paperId
+    ) {
+        partnershipService.deletePartnership(paperId);
+        return BaseResponse.onSuccess(SuccessStatus._OK, null);
+    }
+
+    @GetMapping("/suspended")
+    @Operation(
+            summary = "대기 중인 제휴 계약서 조회 API",
+            description = "현재 로그인한 관리자(Admin)가 대기 중인 제휴 계약서를 모두 조회하여 리스트로 반환합니다."
+    )
+    public BaseResponse> suspendPartnership(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.getSuspendedPapers(pd.getId()));
+    }
+
+    @GetMapping("/check/admin")
+    @Operation(
+            summary = "관리자 채팅방 내 제휴 확인 API",
+            description = "현재 로그인한 관리자(Admin)가 파라미터로 받은 partnerId를 가진 상대 제휴업체(Partner)와 맺고 있는 제휴를 조회합니다. 비활성화되지 않은 가장 최근 제휴 1건을 조회합니다."
+    )
+    public BaseResponse checkAdminPartnership(
+            @RequestParam("partnerId") Long partnerId,
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.checkPartnershipWithPartner(pd.getId(), partnerId));
+    }
+
+    @GetMapping("/check/partner")
+    @Operation(
+            summary = "제휴업체 채팅방 내 제휴 확인 API",
+            description = "현재 로그인한 제휴업체(Partner)가 파라미터로 받은 AdminId를 가진 상대 관리자(Admin)과 맺고 있는 제휴를 조회합니다. 비활성화되지 않은 가장 최근 제휴 1건을 조회합니다."
+    )
+    public BaseResponse checkPartnerPartnership(
+            @RequestParam("adminId") Long adminId,
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.checkPartnershipWithAdmin(pd.getId(), adminId));
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index c8b8fb1..09679ac 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -53,6 +53,18 @@ public static Paper toPaperEntity(
                 .build();
     }
 
+	public static Paper toDraftPaperEntity(Admin admin, Partner partner, Store store) {
+		return Paper.builder()
+				.admin(admin)
+				.partner(partner)
+				.store(store)
+				.partnershipPeriodStart(null)
+				.partnershipPeriodEnd(null)
+				.isActivated(ActivationStatus.SUSPEND)
+				.contractImageKey(null)
+				.build();
+	}
+
     public static List toPaperContents(
             PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO,
             Paper paper
@@ -60,20 +72,17 @@ public static List toPaperContents(
         if (partnershipRequestDTO.getOptions() == null || partnershipRequestDTO.getOptions().isEmpty()) {
             return Collections.emptyList();
         }
-        List contents = new ArrayList<>(partnershipRequestDTO.getOptions().size());
-        for (var o : partnershipRequestDTO.getOptions()) {
-            PaperContent content = PaperContent.builder()
-                    .paper(paper)
-                    .criterionType(o.getCriterionType())
-                    .optionType(o.getOptionType())
-                    .people(o.getPeople())
-                    .cost(o.getCost())
-                    .category(o.getCategory())
-                    .discount(o.getDiscountRate())
-                    .build();
-            contents.add(content);
-        }
-        return contents;
+		return partnershipRequestDTO.getOptions().stream()
+				.map(optionDto -> PaperContent.builder()
+						.paper(paper) // 어떤 Paper에 속하는지 연결
+						.optionType(optionDto.getOptionType())
+						.criterionType(optionDto.getCriterionType())
+						.people(optionDto.getPeople())
+						.cost(optionDto.getCost())
+						.category(optionDto.getCategory())
+						.discount(optionDto.getDiscountRate()) // DTO의 discountRate를 Entity의 discount에 매핑
+						.build())
+				.toList();
     }
 
 
@@ -83,22 +92,20 @@ public static List> toGoodsBatches(
             PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO
     ) {
         if (partnershipRequestDTO == null || partnershipRequestDTO.getOptions().isEmpty()) {
-            return List.of();
-        }
-        List> batches = new ArrayList<>(partnershipRequestDTO.getOptions().size());
-        for (var o : partnershipRequestDTO.getOptions()) {
-            if (o.getGoods() == null || o.getGoods().isEmpty()) {
-                batches.add(List.of());
-                continue;
-            }
-            List goodsList = o.getGoods().stream()
-                    .map(g -> Goods.builder()
-                            .belonging(g.getGoodsName())
-                            .build())
-                    .collect(Collectors.toList());
-            batches.add(goodsList);
+            return Collections.emptyList();
         }
-        return batches;
+        return partnershipRequestDTO.getOptions().stream()
+				.map(optionDto -> {
+					if (optionDto.getGoods() == null || optionDto.getGoods().isEmpty()) {
+						return Collections.emptyList();
+					}
+					return optionDto.getGoods().stream()
+							.map(goodsDto -> Goods.builder()
+									.belonging(goodsDto.getGoodsName()) // DTO의 goodsName을 엔티티의 belonging에 매핑
+									.build())
+							.toList();
+				})
+				.toList();
     }
 
 
@@ -270,6 +277,7 @@ public static PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershi
                 .adminId(paper.getAdmin()    != null ? paper.getAdmin().getId()     : null)
                 .partnerId(paper.getPartner()!= null ? paper.getPartner().getId()   : null) // 수동등록이면 null
                 .storeId(paper.getStore()    != null ? paper.getStore().getId()     : null)
+				.isActivated(paper.getIsActivated())
                 .options(optionDTOS)
                 .build();
     }
@@ -283,4 +291,15 @@ public static List goodsResu
                         .build())
                 .toList();
     }
+
+	public static PartnershipResponseDTO.CreateDraftResponseDTO toCreateDraftResponseDTO(Paper paper) {
+		return PartnershipResponseDTO.CreateDraftResponseDTO.builder()
+				.paperId(paper.getId())
+				.build();
+	}
+
+	public static void updatePaperFromDto(Paper paper, PartnershipRequestDTO.WritePartnershipRequestDTO dto) {
+		paper.setPartnershipPeriodStart(dto.getPartnershipPeriodStart());
+		paper.setPartnershipPeriodEnd(dto.getPartnershipPeriodEnd());
+	}
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
index 1d9344c..924e84b 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java
@@ -21,9 +21,10 @@ public static class finalRequest{
         Long discount;
         List userIds;
     }
+
     @Getter
     public static class WritePartnershipRequestDTO {
-        private Long adminId; // 제안 학생회 아이디
+        private Long paperId; // 제휴 제안서 아이디
         private LocalDate partnershipPeriodStart;
         private LocalDate partnershipPeriodEnd;
         private List options; // 동적으로 받는 제안 항목
@@ -64,4 +65,9 @@ public static class ManualPartnershipRequestDTO {
         private LocalDate partnershipPeriodEnd;
         private List options;
     }
+
+    @Getter
+    public static class CreateDraftRequestDTO {
+        private Long partnerId; // 제안서를 보낼 제휴업체 ID
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
index 60ad89b..4bf7d2b 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.partnership.dto;
 
 
+import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partnership.entity.enums.CriterionType;
 import com.assu.server.domain.partnership.entity.enums.OptionType;
 import lombok.*;
@@ -12,6 +13,7 @@
 public class PartnershipResponseDTO {
 
     @Getter
+    @Setter
     @NoArgsConstructor
     @AllArgsConstructor
     @Builder
@@ -22,6 +24,7 @@ public static class WritePartnershipResponseDTO {
         private Long adminId;
         private Long partnerId;
         private Long storeId;
+        private ActivationStatus isActivated;
         private List options;
     }
 
@@ -76,4 +79,44 @@ public static class ManualPartnershipResponseDTO {
         private String contractImageUrl;
         private WritePartnershipResponseDTO partnership;
     }
+
+    @Getter
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class CreateDraftResponseDTO {
+        private Long paperId; // 생성된 빈 제안서의 ID
+    }
+
+    @Getter
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class SuspendedPaperDTO {
+        private Long paperId;
+        private String partnerName;
+        private LocalDateTime createdAt;
+    }
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class AdminPartnershipWithPartnerResponseDTO {
+        private Long paperId;
+        private boolean isPartnered; // 제휴 여부
+        private String status; // 제휴 상태
+    }
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class PartnerPartnershipWithAdminResponseDTO {
+        private Long paperId;
+        private boolean isPartnered; // 제휴 여부
+        private String status; // 제휴 상태
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
index 3c4c284..eea1810 100644
--- a/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
+++ b/src/main/java/com/assu/server/domain/partnership/entity/Paper.java
@@ -22,8 +22,12 @@ public class Paper extends BaseEntity {
 	private Long id;
 
 
+	@Setter
 	private LocalDate partnershipPeriodStart; //  LocalDate vs String
+
+	@Setter
 	private LocalDate partnershipPeriodEnd;
+
 	@Setter
     @Enumerated(EnumType.STRING)
 	private ActivationStatus isActivated;
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java
index bede045..cf961df 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/GoodsRepository.java
@@ -2,6 +2,15 @@
 
 import com.assu.server.domain.partnership.entity.Goods;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
 
 public interface GoodsRepository extends JpaRepository {
+
+    @Modifying
+    @Query("delete from Goods g where g.content.id in :contentIds")
+    void deleteAllByContentIds(@Param("contentIds") List contentIds);
 }
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
index f317790..4f9fdf3 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java
@@ -33,6 +33,13 @@ Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(
             Long adminId, Long partnerId, ActivationStatus isActivated
     );
 
+    boolean existsByAdmin_IdAndPartner_IdAndIsActivatedIn(Long adminId, Long partnerId, List statuses);
+    Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(Long adminId, Long partnerId, List statuses);
+
+    // Admin 기준 (SUSPEND)
+    @Query("select p from Paper p join fetch p.partner where p.isActivated = :status order by p.createdAt desc")
+    List findAllByIsActivatedWithPartner(@Param("status") ActivationStatus status);
+
     // Partner 기준 (ACTIVE)
     List findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Sort sort);
     Page  findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Pageable pageable);
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 fcc135a..2e58750 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
@@ -13,25 +13,39 @@
 
 public interface PartnershipService {
 
-    PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipAsPartner(
-            @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request,
+    // 제휴 제안서 수정
+    PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership(
+            PartnershipRequestDTO.WritePartnershipRequestDTO request,
             Long memberId
     );
     
     void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member);
-    
+
+    // 제휴업체/관리자 맺은 제휴 리스트
     List listPartnershipsForAdmin(boolean all, Long partnerId);
     List listPartnershipsForPartner(boolean all, Long adminId);
 
-
+    // 제휴 제안서 조회
     PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId);
+    List getSuspendedPapers(Long adminId);
 
+    // 제휴 상태 업데이트
     PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request);
 
+    // 제휴 수동 등록
     PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership(
             PartnershipRequestDTO.ManualPartnershipRequestDTO request,
             Long adminId,
             MultipartFile contractImage
     );
 
+    // 빈 제휴제안서 만들기
+    PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(PartnershipRequestDTO.CreateDraftRequestDTO request, Long adminId);
+
+    // 제휴 계약서 삭제
+    void deletePartnership(Long paperId);
+
+    // 채팅방 내 제휴 계약서 상태 확인
+    PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId); // 관리자가 조회
+    PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO 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 8a18d2f..9d91a82 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
@@ -88,7 +88,8 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe
     private final AmazonS3Manager amazonS3Manager;
 
     @Override
-    public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipAsPartner(
+    @Transactional
+    public PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership(
             PartnershipRequestDTO.WritePartnershipRequestDTO request,
             Long memberId
     ) {
@@ -96,37 +97,40 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershipAsPart
             throw new DatabaseException(ErrorStatus._BAD_REQUEST);
         }
 
+        Paper paper = paperRepository.findById(request.getPaperId())
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER));
+
         Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
 
-        Admin admin = adminRepository.findById(request.getAdminId())
+        Admin admin = adminRepository.findById(paper.getAdmin().getId())
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
         Store store = storeRepository.findByPartner(partner)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
 
-        return writePartnership(request, admin, partner, store);
-    }
-
-    public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(PartnershipRequestDTO.WritePartnershipRequestDTO request, Admin admin, Partner partner, Store store) {
+        PartnershipConverter.updatePaperFromDto(paper, request);
 
-        Paper paper = PartnershipConverter.toPaperEntity(request, admin, partner, store);
-        paper = paperRepository.save(paper);
+        List existingContents = paperContentRepository.findByPaperId(request.getPaperId());
+        if (!existingContents.isEmpty()) {
+            List contentIds = existingContents.stream().map(PaperContent::getId).toList();
+            goodsRepository.deleteAllByContentIds(contentIds);
+            paperContentRepository.deleteAll(existingContents);
+        }
 
-        List contents = PartnershipConverter.toPaperContents(request, paper);
-        contents = contents.isEmpty() ? contents : paperContentRepository.saveAll(contents);
+        List newContents = PartnershipConverter.toPaperContents(request, paper);
+        newContents = newContents.isEmpty() ? newContents : paperContentRepository.saveAll(newContents);
 
         List> requestGoodsBatches = PartnershipConverter.toGoodsBatches(request);
 
         List> attachedGoodsBatches = new ArrayList<>();
         List toPersist = new ArrayList<>();
 
-        for(int i = 0;i < contents.size();i++){
-            PaperContent content = contents.get(i);
+        for (int i = 0; i < newContents.size(); i++) {
+            PaperContent content = newContents.get(i);
             List batch = (requestGoodsBatches.size() > i) ? requestGoodsBatches.get(i) : Collections.emptyList();
-
-            List attached = new ArrayList<>(batch.size());
-            for(Goods g : batch){
+            List attached = new ArrayList<>();
+            for (Goods g : batch) {
                 Goods entity = Goods.builder()
                         .content(content)
                         .belonging(g.getBelonging())
@@ -136,11 +140,11 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO writePartnership(Partn
             }
             attachedGoodsBatches.add(attached);
         }
-
-        if(!toPersist.isEmpty()){
+        if (!toPersist.isEmpty()) {
             goodsRepository.saveAll(toPersist);
         }
-        return PartnershipConverter.writePartnershipResultDTO(paper, contents, attachedGoodsBatches);
+
+        return PartnershipConverter.writePartnershipResultDTO(paper, newContents, attachedGoodsBatches);
     }
 
     @Override
@@ -172,6 +176,7 @@ public List listPartnerships
     }
 
     @Override
+    @Transactional
     public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId) {
         Paper paper = paperRepository.findById(partnershipId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER));
@@ -185,6 +190,20 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long pa
         return PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches);
     }
 
+    @Override
+    @Transactional
+    public List getSuspendedPapers(Long adminId) {
+        List suspendedPapers = paperRepository.findAllByIsActivatedWithPartner(ActivationStatus.SUSPEND);
+
+        return suspendedPapers.stream()
+                .map(paper -> PartnershipResponseDTO.SuspendedPaperDTO.builder()
+                        .paperId(paper.getId())
+                        .partnerName(paper.getPartner().getName())
+                        .createdAt(paper.getCreatedAt())
+                        .build())
+                .toList();
+    }
+
     @Override
     @Transactional
     public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request) {
@@ -300,6 +319,94 @@ public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnersh
                 .build();
     }
 
+    @Override
+    @Transactional
+    public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(PartnershipRequestDTO.CreateDraftRequestDTO request, Long adminId) {
+        Admin admin = adminRepository.findById(adminId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
+        Partner partner = partnerRepository.findById(request.getPartnerId())
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
+        Store store = storeRepository.findByPartner(partner)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
+
+        Paper draftPaper = PartnershipConverter.toDraftPaperEntity(admin, partner, store);
+        paperRepository.save(draftPaper);
+
+        return PartnershipConverter.toCreateDraftResponseDTO(draftPaper);
+    }
+
+    @Override
+    @Transactional
+    public void deletePartnership(Long paperId) {
+        Paper paper = paperRepository.findById(paperId)
+                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER));
+
+        List contentsToDelete = paperContentRepository.findByPaperId(paperId);
+
+        if (contentsToDelete != null && !contentsToDelete.isEmpty()) {
+            List contentIds = contentsToDelete.stream().map(PaperContent::getId).toList();
+            goodsRepository.deleteAllByContentIds(contentIds);
+
+            paperContentRepository.deleteAll(contentsToDelete);
+        }
+
+        paperRepository.delete(paper);
+    }
+
+    @Override
+    @Transactional
+    public PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId) {
+        List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND);
+        boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses);
+
+        Long paperId = null;
+        String status = "NONE";
+
+        if (isPartnered) {
+            Optional latestActiveOrSuspendPaper = paperRepository
+                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(adminId, partnerId, targetStatuses);
+
+            if (latestActiveOrSuspendPaper.isPresent()) {
+                Paper paper = latestActiveOrSuspendPaper.get();
+                paperId = paper.getId();
+                status = paper.getIsActivated().name();
+            }
+        }
+
+        return PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO.builder()
+                .paperId(paperId)
+                .isPartnered(isPartnered)
+                .status(status)
+                .build();
+    }
+
+    @Override
+    @Transactional
+    public PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId) {
+        List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND);
+        boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses);
+
+        Long paperId = null;
+        String status = "NONE";
+
+        if (isPartnered) {
+            Optional latestActiveOrSuspendPaper = paperRepository
+                    .findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(adminId, partnerId, targetStatuses);
+
+            if (latestActiveOrSuspendPaper.isPresent()) {
+                Paper paper = latestActiveOrSuspendPaper.get();
+                paperId = paper.getId();
+                status = paper.getIsActivated().name();
+            }
+        }
+
+        return PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO.builder()
+                .paperId(paperId)
+                .isPartnered(isPartnered)
+                .status(status)
+                .build();
+    }
+
     private List buildPartnershipDTOs(List papers) {
         if (papers == null || papers.isEmpty()) return List.of();
 
diff --git a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
index 83b3d29..1a97324 100644
--- a/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
+++ b/src/main/java/com/assu/server/domain/suggestion/controller/SuggestionController.java
@@ -26,7 +26,7 @@ public class SuggestionController {
     @PostMapping
     @Operation(
             summary = "제휴 건의 API",
-            description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 관리자에게 제휴를 건의합니다.\n"
+            description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 현재 로그인한 학생(User)이 관리자에게 제휴를 건의합니다.\n"
     )
     public BaseResponse writeSuggestion(
             @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO,
@@ -38,7 +38,7 @@ public BaseResponse writeSugge
     @GetMapping("/admin")
     @Operation(
             summary = "제휴 건의대상 조회 API",
-            description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 현재 로그인한 학생(User)이 제휴를 건의할 수 있는 학생회(Admin)을 조회합니다.\n"
+            description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 현재 로그인한 학생(User)이 제휴를 건의할 수 있는 학생회(Admin)를 조회합니다.\n"
     )
     public BaseResponse getSuggestionAdmins(
             @AuthenticationPrincipal PrincipalDetails pd
@@ -49,7 +49,7 @@ public BaseResponse getSuggestionA
     @GetMapping("/list")
     @Operation(
             summary = "제휴 건의 조회 API",
-            description = "모든 제휴 건의를 조회합니다."
+            description = "[v1.0 (2025-09-03)](https://www.notion.so/_-24c1197c19ed8083bf8be4b6a6a43f18) 현재 로그인한 학생회(Admin)가 받은 모든 제휴 건의를 조회합니다."
     )
     public BaseResponse> getSuggestions(
             @AuthenticationPrincipal PrincipalDetails pd
diff --git a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
index b33e44a..f5ca591 100644
--- a/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
+++ b/src/main/java/com/assu/server/domain/suggestion/converter/SuggestionConverter.java
@@ -53,9 +53,14 @@ public static List toGetSuggesti
                 .collect(Collectors.toList());
     }
 
-//    public static SuggestionResponseDTO.GetSuggestionAdminsDTO GetSuggestionAdminsResultDTO(Student student) {
-//        return SuggestionResponseDTO.GetSuggestionAdminsDTO.builder()
-//                .adminId()
-//                .build()
-//    }
+    public static SuggestionResponseDTO.GetSuggestionAdminsDTO toGetSuggestionAdmins(Admin universityAdmin, Admin departmentAdmin, Admin majorAdmin) {
+        return SuggestionResponseDTO.GetSuggestionAdminsDTO.builder()
+                .adminId(universityAdmin != null ? universityAdmin.getId() : null)
+                .adminName(universityAdmin != null ? universityAdmin.getName() : null)
+                .departId(departmentAdmin != null ? departmentAdmin.getId() : null)
+                .departName(departmentAdmin != null ? departmentAdmin.getName() : null)
+                .majorId(majorAdmin != null ? majorAdmin.getId() : null)
+                .majorName(majorAdmin != null ? majorAdmin.getName() : null)
+                .build();
+    }
 }
diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
index 2a345ff..2995ff6 100644
--- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java
@@ -2,6 +2,7 @@
 
 import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.admin.repository.AdminRepository;
+import com.assu.server.domain.notification.service.NotificationCommandService;
 import com.assu.server.domain.suggestion.converter.SuggestionConverter;
 import com.assu.server.domain.suggestion.dto.SuggestionRequestDTO;
 import com.assu.server.domain.suggestion.dto.SuggestionResponseDTO;
@@ -11,6 +12,7 @@
 import com.assu.server.domain.user.repository.StudentRepository;
 import com.assu.server.global.apiPayload.code.status.ErrorStatus;
 import com.assu.server.global.exception.DatabaseException;
+import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
@@ -23,8 +25,10 @@ public class SuggestionServiceImpl implements SuggestionService {
     private final SuggestionRepository suggestionRepository;
     private final AdminRepository adminRepository;
     private final StudentRepository studentRepository;
+    private final NotificationCommandService notificationCommandService;
 
     @Override
+    @Transactional
     public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(SuggestionRequestDTO.WriteSuggestionRequestDTO request, Long userId) {
 
         Admin admin = adminRepository.findById(request.getAdminId())
@@ -35,6 +39,7 @@ public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(Suggesti
 
         Suggestion suggestion = SuggestionConverter.toSuggestionEntity(request, admin, student);
         suggestionRepository.save(suggestion);
+        notificationCommandService.sendPartnerSuggestion(suggestion.getAdmin().getId(), suggestion.getId());
 
         return SuggestionConverter.writeSuggestionResultDTO(suggestion);
     }
@@ -54,8 +59,8 @@ public SuggestionResponseDTO.GetSuggestionAdminsDTO getSuggestionAdmins(Long use
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT));
 
         List adminList = adminRepository.findMatchingAdmins(
-                student.getUniversity().toString(),
-                student.getDepartment().toString(),
+                student.getUniversity(),
+                student.getDepartment(),
                 student.getMajor()
         );
 
@@ -63,6 +68,18 @@ public SuggestionResponseDTO.GetSuggestionAdminsDTO getSuggestionAdmins(Long use
         Admin departmentAdmin = null;
         Admin majorAdmin = null;
 
-        return null;
+        for (Admin admin : adminList) {
+            if (admin.getMajor() != null) {
+                majorAdmin = admin;
+            }
+            else if (admin.getDepartment() != null) {
+                departmentAdmin = admin;
+            }
+            else {
+                universityAdmin = admin;
+            }
+        }
+
+        return SuggestionConverter.toGetSuggestionAdmins(universityAdmin, departmentAdmin, majorAdmin);
     }
 }
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 6226353..ac01dc5 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -38,7 +38,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/auth/admins/signup",
                                 "/auth/commons/login",
                                 "/auth/students/login",
-                                "/auth/students/ssu-verify"
+                                "/auth/students/ssu-verify",
+                                "/map/place" // 주소 입력용 장소 검색 API 제외ㅕ
                         ).permitAll()
 
                         // 나머지 요청은 JwtAuthFilter가 화이트리스트/보호자원 판별

From 15c610f74e565ecdc81d2260fba6bc7f1684737d Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Mon, 15 Sep 2025 18:45:02 +0900
Subject: [PATCH 179/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?=
 =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1=20api=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/chat/dto/ChatRequestDTO.java     | 2 +-
 .../assu/server/domain/chat/service/ChatServiceImpl.java    | 6 ++----
 .../server/domain/store/repository/StoreRepository.java     | 2 ++
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
index 2123afc..90798fc 100644
--- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
+++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
@@ -5,7 +5,7 @@
 public class ChatRequestDTO {
     @Getter
     public static class CreateChatRoomRequestDTO {
-        private Long storeId;
+        private Long adminId;
         private Long partnerId;
     }
 
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index e4ef31b..93a27f5 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -27,8 +27,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Slf4j
 @Service
@@ -52,14 +50,14 @@ public List getChatRoomList(Long memberId) {
     @Override
     public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId) {
 
-        Long storeId = request.getStoreId();
+        Long adminId = request.getAdminId();
         Long partnerId = request.getPartnerId();
 
         Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
         Partner partner = partnerRepository.findById(partnerId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-        Store store = storeRepository.findById(storeId)
+        Store store = storeRepository.findByPartnerId(partnerId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE));
 
 
diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
index 342595d..cfb67c4 100644
--- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
+++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java
@@ -120,4 +120,6 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), s.point)
     Optional findByName(String name);
     Optional findById(Long id);
 
+    Optional findByPartnerId(Long partnerId);
+
 }

From d349f3408141b620ed44997081452677660c855f Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Mon, 15 Sep 2025 19:58:41 +0900
Subject: [PATCH 180/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?=
 =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1=20api=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/chat/service/ChatServiceImpl.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index 93a27f5..cd8d672 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -53,7 +53,7 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C
         Long adminId = request.getAdminId();
         Long partnerId = request.getPartnerId();
 
-        Admin admin = adminRepository.findById(memberId)
+        Admin admin = adminRepository.findById(adminId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
         Partner partner = partnerRepository.findById(partnerId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));

From cdf1f1b1cbed0a838bf4b38aa2e4d454c009f668 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Mon, 15 Sep 2025 23:47:41 +1000
Subject: [PATCH 181/270] =?UTF-8?q?[FEAT/#101]=20-=20=ED=94=84=EB=A1=9C?=
 =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20?=
 =?UTF-8?q?=EB=B0=8F=20=EB=93=B1=EB=A1=9D=20api=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../inquiry/controller/InquiryController.java | 45 +++++++++++++--
 .../profileImage/ProfileImageResponse.java    | 12 ++++
 .../inquiry/service/ProfileImageService.java  |  7 +++
 .../service/ProfileImageServiceImpl.java      | 55 +++++++++++++++++++
 .../apiPayload/code/status/ErrorStatus.java   |  7 +++
 .../assu/server/infra/s3/AmazonS3Manager.java | 15 +++++
 6 files changed, 135 insertions(+), 6 deletions(-)
 create mode 100644 src/main/java/com/assu/server/domain/inquiry/dto/profileImage/ProfileImageResponse.java
 create mode 100644 src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java
 create mode 100644 src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index 00e9adf..b5c173e 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -1,29 +1,37 @@
 package com.assu.server.domain.inquiry.controller;
 
+import com.assu.server.domain.inquiry.dto.profileImage.ProfileImageResponse;
 import com.assu.server.domain.inquiry.dto.InquiryAnswerRequestDTO;
 import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO;
 import com.assu.server.domain.inquiry.dto.InquiryResponseDTO;
 import com.assu.server.domain.inquiry.service.InquiryService;
+import com.assu.server.domain.inquiry.service.ProfileImageService;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 
 import com.assu.server.global.util.PrincipalDetails;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.Map;
 
-@Tag(name = "Inquiry", description = "문의 API")
+@Tag(name = "MyPage", description = "마이페이지 API")
 @RestController
-@RequestMapping("/member/inquiries")
+@RequestMapping("/member")
 @RequiredArgsConstructor
 public class InquiryController {
 
     private final InquiryService inquiryService;
+    private final ProfileImageService profileImageService;
 
     @Operation(
             summary = "문의 생성 API",
@@ -31,7 +39,7 @@ public class InquiryController {
                     "- 문의를 생성하고 해당 문의의 id를 반환합니다.\n"+
                     "  - InquiryCreateRequestDTO: title, content, email\n"
     )
-    @PostMapping
+    @PostMapping("/inquiries")
     public BaseResponse create(
             @AuthenticationPrincipal PrincipalDetails pd,
             @RequestBody @Valid InquiryCreateRequestDTO req
@@ -48,7 +56,7 @@ public BaseResponse create(
                     "  - page: Request Param, Integer, 1 이상\n" +
                     "  - size: Request Param, Integer, default = 20"
     )
-    @GetMapping
+    @GetMapping("/inquiries")
     public BaseResponse> list(
             @AuthenticationPrincipal PrincipalDetails pd,
             @RequestParam(defaultValue = "all") String status, // all | waiting | answered
@@ -66,7 +74,7 @@ public BaseResponse> list(
                     "- 본인의 단건 문의를 상세 조회합니다.\n"+
                     "  - inquiry-id: Path Variable, Long\n"
     )
-    @GetMapping("/{inquiry-id}")
+    @GetMapping("/inquiries/{inquiry-id}")
     public BaseResponse get(
             @AuthenticationPrincipal PrincipalDetails pd,
             @PathVariable("inquiry-id") Long inquiryId
@@ -82,7 +90,7 @@ public BaseResponse get(
                     "- 문의에 답변을 저장하고 상태를 ANSWERED로 변경합니다.\n"+
                     "  - inquiry-id: Path Variable, Long\n"
     )
-    @PatchMapping("/{inquiry-id}/answer")
+    @PatchMapping("/inquiries/{inquiry-id}/answer")
     public BaseResponse answer(
             @PathVariable("inquiry-id") Long inquiryId,
             @RequestBody @Valid InquiryAnswerRequestDTO req
@@ -90,4 +98,29 @@ public BaseResponse answer(
         inquiryService.answer(inquiryId, req.getAnswer());
         return BaseResponse.onSuccess(SuccessStatus._OK, "The inquiry answered successfully. id=" + inquiryId);
     }
+
+    @Operation(
+            summary = "프로필 사진 업로드/교체 API",
+            description = "# [v1.0 (2025-09-15)](https://clumsy-seeder-416.notion.site/26f1197c19ed8031bc50e3571e8ea18f?source=copy_link)\n" +
+                    "- `multipart/form-data`로 프로필 이미지를 업로드합니다.\n" +
+                    "- 기존 이미지가 있으면 S3에서 삭제 후 새 이미지로 교체합니다.\n" +
+                    "- 성공 시 업로드된 이미지 key를 반환합니다."
+    )
+    @PutMapping(value = "/profile/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public BaseResponse uploadOrReplaceProfileImage(
+            @AuthenticationPrincipal PrincipalDetails pd,
+            @RequestPart("image")
+            @Parameter(
+                    description = "프로필 이미지 파일 (jpg/png/webp 등)",
+                    required = true,
+                    content = @Content(
+                            mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                            schema = @Schema(type = "string", format = "binary")
+                    )
+            )
+            MultipartFile image
+    ) {
+        String key = profileImageService.updateProfileImage(pd.getMemberId(), image);
+        return BaseResponse.onSuccess(SuccessStatus._OK, new ProfileImageResponse(key));
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/inquiry/dto/profileImage/ProfileImageResponse.java b/src/main/java/com/assu/server/domain/inquiry/dto/profileImage/ProfileImageResponse.java
new file mode 100644
index 0000000..e8867f2
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/inquiry/dto/profileImage/ProfileImageResponse.java
@@ -0,0 +1,12 @@
+package com.assu.server.domain.inquiry.dto.profileImage;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class ProfileImageResponse {
+    @Schema(description = "업로드된 프로필 이미지 URL")
+    private String url;
+}
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java
new file mode 100644
index 0000000..d19a5db
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java
@@ -0,0 +1,7 @@
+package com.assu.server.domain.inquiry.service;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public interface ProfileImageService {
+    String updateProfileImage(Long memberId, MultipartFile image);
+}
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java
new file mode 100644
index 0000000..3f79788
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java
@@ -0,0 +1,55 @@
+package com.assu.server.domain.inquiry.service;
+
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import com.assu.server.infra.s3.AmazonS3Manager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ProfileImageServiceImpl implements ProfileImageService{
+
+    private static final long MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
+    private static final String[] ALLOWED_EXT = {"jpg", "jpeg", "png", "webp"};
+
+    private final MemberRepository memberRepository;
+    private final AmazonS3Manager amazonS3Manager;
+
+    @Override
+    @Transactional
+    public String updateProfileImage(Long memberId, MultipartFile image) {
+        if (image == null || image.isEmpty()) {
+            throw new CustomAuthException(ErrorStatus.PROFILE_IMAGE_NOT_FOUND);
+        }
+
+        // 1) 멤버 조회
+        Member member = memberRepository.findById(memberId)
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+
+        // 2) 업로드 (generateKeyName + uploadFile 만 사용)
+        String keyPath = "members/" + member.getId() + "/profile/" + image.getOriginalFilename();
+        String keyName = amazonS3Manager.generateKeyName(keyPath);
+        String uploadedKey = amazonS3Manager.uploadFile(keyName, image); // S3에 올린 후 key 반환
+
+        // 3) 기존 파일 있으면 삭제 (기존 값이 key 라는 전제)
+        String oldKey = member.getProfileUrl();
+        if (oldKey != null && !oldKey.isBlank()) {
+            try { amazonS3Manager.deleteFile(oldKey); }
+            catch (Exception e) { log.warn("이전 프로필 삭제 실패 key={}", oldKey, e); }
+        }
+
+        // 4) DB 업데이트 (key 저장)
+        member.setProfileUrl(uploadedKey);
+
+        // 5) 호출자에 key 반환 (FE는 필요 시 presigned URL 생성해 사용)
+        return uploadedKey;
+    }
+}
+
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
index f0d331a..94ecae2 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java
@@ -98,6 +98,13 @@ public enum ErrorStatus implements BaseErrorCode {
     // 주소 에러
     NO_SUCH_ADDRESS(HttpStatus.NOT_FOUND, "ADDRESS_7001", "주소를 찾을 수 없습니다."),
 
+    // 프로필(Profile) 관련 에러
+    PROFILE_IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "PROFILE_5001", "프로필 이미지 업로드에 실패했습니다."),
+    PROFILE_IMAGE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "PROFILE_5002", "프로필 이미지 삭제에 실패했습니다."),
+    PROFILE_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "PROFILE_4001", "존재하지 않는 프로필 이미지입니다."),
+    PROFILE_IMAGE_INVALID_FORMAT(HttpStatus.BAD_REQUEST, "PROFILE_4002", "지원하지 않는 이미지 형식입니다."),
+    PROFILE_IMAGE_TOO_LARGE(HttpStatus.BAD_REQUEST, "PROFILE_4003", "허용된 크기를 초과한 이미지입니다."),
+
     ;
 
     private final HttpStatus httpStatus;
diff --git a/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java b/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
index 1d0d622..8ec2f5d 100644
--- a/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
+++ b/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
@@ -8,6 +8,7 @@
 import software.amazon.awssdk.core.ResponseBytes;
 import software.amazon.awssdk.core.sync.RequestBody;
 import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
 import software.amazon.awssdk.services.s3.model.GetObjectRequest;
 import software.amazon.awssdk.services.s3.model.GetObjectResponse;
 import software.amazon.awssdk.services.s3.model.PutObjectRequest;
@@ -135,4 +136,18 @@ public String generateKeyName(String path) {
         return path + '/' + UUID.randomUUID();
     }
 
+    public void deleteFile(String keyName) {
+        if (keyName == null || keyName.isBlank()) return;
+        try {
+            s3Client.deleteObject(DeleteObjectRequest.builder()
+                    .bucket(amazonConfig.getBucket())
+                    .key(keyName)
+                    .build());
+            log.debug("S3 삭제 완료 key={}", keyName);
+        } catch (Exception e) {
+            log.error("S3 파일 삭제 실패. key={}", keyName, e);
+            throw new RuntimeException("S3 delete failed", e);
+        }
+    }
+
 }

From 598482b124aa48be2bf1f0b5dc6c84b46260b446 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Tue, 16 Sep 2025 04:01:50 +0900
Subject: [PATCH 182/270] =?UTF-8?q?[FIX/#82]=20map=20=EB=A7=8C=20=ED=92=80?=
 =?UTF-8?q?=EC=96=B4=EC=84=9C=20=EC=A1=B0=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/assu/server/global/config/SecurityConfig.java     | 3 ++-
 1 file changed, 2 insertions(+), 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 8d7f72b..06b3642 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -41,7 +41,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/auth/admins/signup",
                                 "/auth/commons/login",
                                 "/auth/students/login",
-                                "/auth/students/ssu-verify"
+                                "/auth/students/ssu-verify",
+                                "/map/place"
                         ).permitAll()
                     .requestMatchers("/ws/**").permitAll()
                         // 나머지는 인증 필요

From 750e963b83c67e461235de5ae64d7c18260d0bf7 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Tue, 16 Sep 2025 23:19:40 +1000
Subject: [PATCH 183/270] =?UTF-8?q?[FEAT/#110]=20-=20=EC=A7=80=EB=8F=84=20?=
 =?UTF-8?q?=EB=B0=98=ED=99=98=EA=B0=92=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/admin/repository/AdminRepository.java     | 10 +---------
 .../server/domain/map/controller/MapController.java  |  2 +-
 .../assu/server/domain/map/dto/MapResponseDTO.java   |  4 ++++
 .../server/domain/map/service/MapServiceImpl.java    |  4 ++--
 .../domain/partner/repository/PartnerRepository.java | 12 +++---------
 5 files changed, 11 insertions(+), 21 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
index 2a43867..8a1eaee 100644
--- a/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
+++ b/src/main/java/com/assu/server/domain/admin/repository/AdminRepository.java
@@ -67,16 +67,8 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), a.point)
         select distinct a
         from Admin a
         where lower(a.name) like lower(concat('%', :keyword, '%'))
-          and exists (
-              select 1 from Paper pc
-              where pc.admin = a
-                and pc.partner.id = :partnerId
-                and pc.isActivated = :status
-          )
         """)
-    List searchPartneredByName(
-            @Param("partnerId") Long partnerId,
-            @Param("status") ActivationStatus status,
+    List searchAdminByKeyword(
             @Param("keyword") String keyword
     );
 
diff --git a/src/main/java/com/assu/server/domain/map/controller/MapController.java b/src/main/java/com/assu/server/domain/map/controller/MapController.java
index 86f948b..e22be08 100644
--- a/src/main/java/com/assu/server/domain/map/controller/MapController.java
+++ b/src/main/java/com/assu/server/domain/map/controller/MapController.java
@@ -47,7 +47,7 @@ public BaseResponse getLocations(
 
     @Operation(
             summary = "검색어 기반 장소 조회 API",
-            description = "검색어를 입력해주세요. (user → store 전체조회 / admin → 제휴중인 partner 조회 / partner → 제휴중인 admin 조회)"
+            description = "검색어를 입력해주세요. (user → store 전체조회 / admin → partner 전체조회 / partner → admin 전체조회)"
     )
     @GetMapping("/search")
     public BaseResponse getLocationsByKeyword(
diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
index 195dacd..8fc2b55 100644
--- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java
@@ -23,6 +23,7 @@ public static class PartnerMapResponseDTO {
         private LocalDate partnershipEndDate;
         private Double latitude;
         private Double longitude;
+        private String profileUrl;
     }
 
     @Getter
@@ -39,6 +40,7 @@ public static class AdminMapResponseDTO {
         private LocalDate partnershipEndDate;
         private Double latitude;
         private Double longitude;
+        private String profileUrl;
     }
 
     @Getter
@@ -61,6 +63,8 @@ public static class StoreMapResponseDTO {
         private boolean hasPartner;
         private Double latitude;
         private Double longitude;
+        private String profileUrl;
+
     }
 
     @Getter @NoArgsConstructor @AllArgsConstructor @Builder
diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index ae1bc8e..dd7a5e1 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -196,7 +196,7 @@ public List searchPartner(String keyword,
         Admin admin = adminRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
-        List partners = partnerRepository.searchPartneredByName(memberId, ActivationStatus.ACTIVE, keyword);
+        List partners = partnerRepository.searchPartnerByKeyword(keyword);
 
         return partners.stream().map(p -> {
                 Paper active = paperRepository
@@ -223,7 +223,7 @@ public List searchAdmin(String keyword, Long
         Partner partner = partnerRepository.findById(memberId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
 
-        List admins = adminRepository.searchPartneredByName(memberId, ActivationStatus.ACTIVE, keyword);
+        List admins = adminRepository.searchAdminByKeyword(keyword);
 
         return admins.stream().map(a -> {
             Paper active = paperRepository
diff --git a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java
index 2b5bb2f..df9aa42 100644
--- a/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java
+++ b/src/main/java/com/assu/server/domain/partner/repository/PartnerRepository.java
@@ -50,16 +50,10 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), p.point)
         select distinct p
         from Partner p
         where lower(p.name) like lower(concat('%', :keyword, '%'))
-          and exists (
-              select 1 from Paper pc
-              where pc.partner = p
-                and pc.admin.id = :adminId
-                and pc.isActivated = :status
-          )
         """)
-    List searchPartneredByName(
-            @Param("adminId") Long adminId,
-            @Param("status") ActivationStatus status,
+    List searchPartnerByKeyword(
             @Param("keyword") String keyword
     );
+
+
 }

From e4f29c3f517cf00ab73b79fe4768a0d85351cd4b Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 17 Sep 2025 00:09:18 +1000
Subject: [PATCH 184/270] =?UTF-8?q?[FEAT/#110]=20-=20=ED=94=84=EB=A1=9C?=
 =?UTF-8?q?=ED=95=84=20url=20=EB=B0=98=ED=99=98=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/map/service/MapServiceImpl.java    | 43 +++++++++++++------
 .../member/repository/MemberRepository.java   |  3 ++
 2 files changed, 32 insertions(+), 14 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index dd7a5e1..afa49c5 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -5,6 +5,8 @@
 import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.map.dto.MapRequestDTO;
 import com.assu.server.domain.map.dto.MapResponseDTO;
+import com.assu.server.domain.member.entity.Member;
+import com.assu.server.domain.member.repository.MemberRepository;
 import com.assu.server.domain.partner.entity.Partner;
 import com.assu.server.domain.partner.repository.PartnerRepository;
 import com.assu.server.domain.partnership.entity.Goods;
@@ -21,12 +23,14 @@
 import com.assu.server.global.exception.DatabaseException;
 import com.assu.server.global.exception.GeneralException;
 
+import com.assu.server.infra.s3.AmazonS3Manager;
 import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.Point;
 import org.springframework.stereotype.Service;
+import software.amazon.awssdk.services.s3.auth.scheme.internal.S3EndpointResolverAware;
 
 import java.util.List;
 import java.util.stream.Collectors;
@@ -42,11 +46,10 @@ public class MapServiceImpl implements MapService {
     private final PaperRepository paperRepository;
     private final GeometryFactory geometryFactory;
     private final GoodsRepository goodsRepository;
+    private final AmazonS3Manager amazonS3Manager;
 
     @Override
     public List getPartners(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) {
-        Admin admin = adminRepository.findById(memberId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
         String wkt = toWKT(viewport);
         List partners = partnerRepository.findAllWithinViewport(wkt);
@@ -55,6 +58,9 @@ public List getPartners(MapRequestDTO.View
             Paper active = paperRepository.findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(memberId, p.getId(), ActivationStatus.ACTIVE)
                     .orElse(null);
 
+            String key = (p.getMember() != null) ? p.getMember().getProfileUrl() : null;
+            String url = amazonS3Manager.generatePresignedUrl(key);
+
             return MapResponseDTO.PartnerMapResponseDTO.builder()
                     .partnerId(p.getId())
                     .name(p.getName())
@@ -65,16 +71,13 @@ public List getPartners(MapRequestDTO.View
                     .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
                     .latitude(p.getLatitude())
                     .longitude(p.getLongitude())
+                    .profileUrl(url)
                     .build();
         }).toList();
     }
 
     @Override
     public List getAdmins(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) {
-
-        Partner partner = partnerRepository.findById(memberId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-
         String wkt = toWKT(viewport);
         List admins = adminRepository.findAllWithinViewport(wkt);
 
@@ -82,6 +85,9 @@ public List getAdmins(MapRequestDTO.ViewOnMa
             Paper active = paperRepository.findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(a.getId(), memberId, ActivationStatus.ACTIVE)
                     .orElse(null);
 
+            String key = (a.getMember() != null) ? a.getMember().getProfileUrl() : null;
+            String url = amazonS3Manager.generatePresignedUrl(key);
+
             return MapResponseDTO.AdminMapResponseDTO.builder()
                     .adminId(a.getId())
                     .name(a.getName())
@@ -92,6 +98,7 @@ public List getAdmins(MapRequestDTO.ViewOnMa
                     .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
                     .latitude(a.getLatitude())
                     .longitude(a.getLongitude())
+                    .profileUrl(url)
                     .build();
         }).toList();
     }
@@ -109,6 +116,9 @@ public List getStores(MapRequestDTO.ViewOnMa
                         () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT)
                     );
 
+            String key = (s.getPartner() != null) ? s.getPartner().getMember().getProfileUrl() : null;
+            String url = amazonS3Manager.generatePresignedUrl(key);
+
             Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
                     .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
                     .orElse(null);
@@ -130,6 +140,7 @@ public List getStores(MapRequestDTO.ViewOnMa
                     .hasPartner(hasPartner)
                     .latitude(s.getLatitude())
                     .longitude(s.getLongitude())
+                    .profileUrl(url)
                     .build();
         }).toList();
     }
@@ -145,6 +156,9 @@ public List searchStores(String keyword) {
                         () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT)
                     );
 
+            String key = (s.getPartner() != null) ? s.getPartner().getMember().getProfileUrl() : null;
+            String url = amazonS3Manager.generatePresignedUrl(key);
+
             Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
                     .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
                     .orElse(null);
@@ -186,16 +200,13 @@ else if (content.getOptionType() == OptionType.SERVICE) {
                     .hasPartner(hasPartner)
                     .latitude(s.getLatitude())
                     .longitude(s.getLongitude())
+                    .profileUrl(url)
                     .build();
         }).toList();
     }
 
     @Override
     public List searchPartner(String keyword, Long memberId) {
-
-        Admin admin = adminRepository.findById(memberId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
-
         List partners = partnerRepository.searchPartnerByKeyword(keyword);
 
         return partners.stream().map(p -> {
@@ -203,6 +214,9 @@ public List searchPartner(String keyword,
                                     .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(memberId, p.getId(), ActivationStatus.ACTIVE)
                                     .orElse(null);
 
+            String key = (p.getMember() != null) ? p.getMember().getProfileUrl() : null;
+            String url = amazonS3Manager.generatePresignedUrl(key);
+
                 return MapResponseDTO.PartnerMapResponseDTO.builder()
                     .partnerId(p.getId())
                     .name(p.getName())
@@ -213,16 +227,13 @@ public List searchPartner(String keyword,
                     .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
                     .latitude(p.getLatitude())
                     .longitude(p.getLongitude())
+                    .profileUrl(url)
                     .build();
         }).toList();
     }
 
     @Override
     public List searchAdmin(String keyword, Long memberId) {
-
-        Partner partner = partnerRepository.findById(memberId)
-                .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER));
-
         List admins = adminRepository.searchAdminByKeyword(keyword);
 
         return admins.stream().map(a -> {
@@ -230,6 +241,9 @@ public List searchAdmin(String keyword, Long
                     .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(a.getId(), memberId, ActivationStatus.ACTIVE)
                     .orElse(null);
 
+            String key = (a.getMember() != null) ? a.getMember().getProfileUrl() : null;
+            String url = amazonS3Manager.generatePresignedUrl(key);
+
             return MapResponseDTO.AdminMapResponseDTO.builder()
                     .adminId(a.getId())
                     .name(a.getName())
@@ -240,6 +254,7 @@ public List searchAdmin(String keyword, Long
                     .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
                     .latitude(a.getLatitude())
                     .longitude(a.getLongitude())
+                    .profileUrl(url)
                     .build();
         }).toList();
     }
diff --git a/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java b/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
index 555a352..ac82f1e 100644
--- a/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
+++ b/src/main/java/com/assu/server/domain/member/repository/MemberRepository.java
@@ -4,6 +4,7 @@
 import java.util.List;
 import java.util.Optional;
 
+import com.assu.server.domain.admin.entity.Admin;
 import com.assu.server.domain.member.entity.Member;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
@@ -17,4 +18,6 @@ public interface MemberRepository extends JpaRepository {
     Optional findMemberById(Long id);
     
     List findByDeletedAtBefore(LocalDateTime deletedAt);
+
+    Member findByAdminProfile(Admin adminProfile);
 }

From 092378c07af88344d61cc1a9b8032cbe7e8f7c72 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Tue, 16 Sep 2025 23:15:36 +0900
Subject: [PATCH 185/270] =?UTF-8?q?refactor/#38=20-=20chatting=EC=9D=84=20?=
 =?UTF-8?q?=EC=9C=84=ED=95=9C=20SecurityConfig,=20WebSocketConfig=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/chat/config/WebSocketConfig.java       | 15 +++++++++------
 .../domain/chat/service/ChatServiceImpl.java      |  2 ++
 .../assu/server/global/config/SecurityConfig.java |  2 +-
 3 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
index cdb02cb..7e87bf5 100644
--- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
@@ -1,6 +1,7 @@
 package com.assu.server.domain.chat.config;
 
 import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.ChannelRegistration;
 import org.springframework.messaging.simp.config.MessageBrokerRegistry;
 import org.springframework.web.socket.config.annotation.*;
 
@@ -8,25 +9,27 @@
 @EnableWebSocketMessageBroker
 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
+
     @Override
     public void registerStompEndpoints(StompEndpointRegistry registry) {
-        registry.addEndpoint("/ws/chat")  // 클라이언트 WebSocket 연결 지점
+        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
                 .setAllowedOriginPatterns(
+                        "*",
+                        "https://assu.shop",
                         "http://localhost:63342",
                         "http://localhost:5173",     // Vite 기본
                         "http://localhost:3000",     // CRA/Next 기본
                         "http://127.0.0.1:*",
-                        "http://192.168.*.*:*")       // 같은 LAN의 실제 기기 테스트용
-                .withSockJS();             // fallback for old browsers
+                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
+                      // fallback for old browsers
 
-        // ✅ 모바일/안드로이드용 (네이티브 WebSocket)
-        registry.addEndpoint("/ws/chat-native")
-                .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅
     }
 
     @Override
     public void configureMessageBroker(MessageBrokerRegistry registry) {
         registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
         registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
+        registry.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
+        registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
     }
 }
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index cd8d672..f732a7f 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -95,6 +95,8 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM
         log.info("saved message start");
         Message saved = messageRepository.saveAndFlush(message);
         log.info("saved message middle");
+        log.info("REQ roomId={}, senderId={}, receiverId={}, message={}",
+                request.roomId(), request.senderId(), request.receiverId(), request.message());
         log.info("saved message id={}, roomId={}, senderId={}, receiverId={}",
                 saved.getId(), room.getId(), sender.getId(), receiver.getId());
 
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 8d7f72b..186af88 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -22,7 +22,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                         .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
 
                         // ✅ WebSocket 핸드셰이크 허용 (네이티브 + SockJS 모두 포함)
-                        .requestMatchers("/ws/**").permitAll()
+                        .requestMatchers("/ws","/ws/**").permitAll()
 
                         // Swagger 등 공개 리소스
                         .requestMatchers(

From 0514952c2dd9e350713b9b710f5c64c132e33d9f Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Tue, 16 Sep 2025 23:33:36 +0900
Subject: [PATCH 186/270] =?UTF-8?q?refactor/#38=20-=20chatting=EC=9D=84=20?=
 =?UTF-8?q?=EC=9C=84=ED=95=9C=20WebSocketConfig=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../config/CertifyWebSocketConfig.java        | 15 +++-
 .../domain/chat/config/WebSocketConfig.java   | 70 +++++++++----------
 2 files changed, 49 insertions(+), 36 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 85d7fc2..99a37f6 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -19,13 +19,26 @@ public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer
 	private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
 	@Override
 	public void configureMessageBroker(MessageBrokerRegistry config) {
+        config.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
+        config.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
 		config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
 		config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
 	}
 
 	@Override
 	public void registerStompEndpoints(StompEndpointRegistry registry) {
-		registry.addEndpoint("/ws").setAllowedOriginPatterns("*");          // 클라이언트 WebSocket 연결 주소
+        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
+                .setAllowedOriginPatterns(
+                        "*",
+                        "https://assu.shop",
+                        "http://localhost:63342",
+                        "http://localhost:5173",     // Vite 기본
+                        "http://localhost:3000",     // CRA/Next 기본
+                        "http://127.0.0.1:*",
+                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
+        // fallback for old browsers
+
+            // 클라이언트 WebSocket 연결 주소
 			// .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
 	}
 
diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
index 7e87bf5..6e3cebb 100644
--- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
@@ -1,35 +1,35 @@
-package com.assu.server.domain.chat.config;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.messaging.simp.config.ChannelRegistration;
-import org.springframework.messaging.simp.config.MessageBrokerRegistry;
-import org.springframework.web.socket.config.annotation.*;
-
-@Configuration
-@EnableWebSocketMessageBroker
-public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-
-
-    @Override
-    public void registerStompEndpoints(StompEndpointRegistry registry) {
-        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
-                .setAllowedOriginPatterns(
-                        "*",
-                        "https://assu.shop",
-                        "http://localhost:63342",
-                        "http://localhost:5173",     // Vite 기본
-                        "http://localhost:3000",     // CRA/Next 기본
-                        "http://127.0.0.1:*",
-                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
-                      // fallback for old browsers
-
-    }
-
-    @Override
-    public void configureMessageBroker(MessageBrokerRegistry registry) {
-        registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
-        registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
-        registry.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
-        registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
-    }
-}
+//package com.assu.server.domain.chat.config;
+//
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.messaging.simp.config.ChannelRegistration;
+//import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+//import org.springframework.web.socket.config.annotation.*;
+//
+//@Configuration
+//@EnableWebSocketMessageBroker
+//public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+//
+//
+//    @Override
+//    public void registerStompEndpoints(StompEndpointRegistry registry) {
+//        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
+//                .setAllowedOriginPatterns(
+//                        "*",
+//                        "https://assu.shop",
+//                        "http://localhost:63342",
+//                        "http://localhost:5173",     // Vite 기본
+//                        "http://localhost:3000",     // CRA/Next 기본
+//                        "http://127.0.0.1:*",
+//                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
+//                      // fallback for old browsers
+//
+//    }
+//
+//    @Override
+//    public void configureMessageBroker(MessageBrokerRegistry registry) {
+//        registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
+//        registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
+//        registry.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
+//        registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
+//    }
+//}

From dbdf6ba0c2755f96981660d540335850967a9cca Mon Sep 17 00:00:00 2001
From: BAEK0111 <143854581+BAEK0111@users.noreply.github.com>
Date: Wed, 17 Sep 2025 00:19:55 +0900
Subject: [PATCH 187/270] Revert "refactor/#38"

---
 .../config/CertifyWebSocketConfig.java        | 15 +---
 .../domain/chat/config/WebSocketConfig.java   | 70 +++++++++----------
 2 files changed, 36 insertions(+), 49 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 99a37f6..85d7fc2 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -19,26 +19,13 @@ public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer
 	private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
 	@Override
 	public void configureMessageBroker(MessageBrokerRegistry config) {
-        config.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
-        config.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
 		config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
 		config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
 	}
 
 	@Override
 	public void registerStompEndpoints(StompEndpointRegistry registry) {
-        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
-                .setAllowedOriginPatterns(
-                        "*",
-                        "https://assu.shop",
-                        "http://localhost:63342",
-                        "http://localhost:5173",     // Vite 기본
-                        "http://localhost:3000",     // CRA/Next 기본
-                        "http://127.0.0.1:*",
-                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
-        // fallback for old browsers
-
-            // 클라이언트 WebSocket 연결 주소
+		registry.addEndpoint("/ws").setAllowedOriginPatterns("*");          // 클라이언트 WebSocket 연결 주소
 			// .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
 	}
 
diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
index 6e3cebb..7e87bf5 100644
--- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
@@ -1,35 +1,35 @@
-//package com.assu.server.domain.chat.config;
-//
-//import org.springframework.context.annotation.Configuration;
-//import org.springframework.messaging.simp.config.ChannelRegistration;
-//import org.springframework.messaging.simp.config.MessageBrokerRegistry;
-//import org.springframework.web.socket.config.annotation.*;
-//
-//@Configuration
-//@EnableWebSocketMessageBroker
-//public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-//
-//
-//    @Override
-//    public void registerStompEndpoints(StompEndpointRegistry registry) {
-//        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
-//                .setAllowedOriginPatterns(
-//                        "*",
-//                        "https://assu.shop",
-//                        "http://localhost:63342",
-//                        "http://localhost:5173",     // Vite 기본
-//                        "http://localhost:3000",     // CRA/Next 기본
-//                        "http://127.0.0.1:*",
-//                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
-//                      // fallback for old browsers
-//
-//    }
-//
-//    @Override
-//    public void configureMessageBroker(MessageBrokerRegistry registry) {
-//        registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
-//        registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
-//        registry.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
-//        registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
-//    }
-//}
+package com.assu.server.domain.chat.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.ChannelRegistration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.config.annotation.*;
+
+@Configuration
+@EnableWebSocketMessageBroker
+public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {
+        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
+                .setAllowedOriginPatterns(
+                        "*",
+                        "https://assu.shop",
+                        "http://localhost:63342",
+                        "http://localhost:5173",     // Vite 기본
+                        "http://localhost:3000",     // CRA/Next 기본
+                        "http://127.0.0.1:*",
+                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
+                      // fallback for old browsers
+
+    }
+
+    @Override
+    public void configureMessageBroker(MessageBrokerRegistry registry) {
+        registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
+        registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
+        registry.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
+        registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
+    }
+}

From ad89a5e7365b615deb527b4ba69903c122ab3c0e Mon Sep 17 00:00:00 2001
From: BAEK0111 <143854581+BAEK0111@users.noreply.github.com>
Date: Wed, 17 Sep 2025 00:25:52 +0900
Subject: [PATCH 188/270] Revert "Develop"

---
 .../config/CertifyWebSocketConfig.java        | 15 +----
 .../domain/chat/config/WebSocketConfig.java   | 67 +++++++++----------
 .../domain/chat/service/ChatServiceImpl.java  |  2 -
 .../server/global/config/SecurityConfig.java  |  2 +-
 4 files changed, 34 insertions(+), 52 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 99a37f6..85d7fc2 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -19,26 +19,13 @@ public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer
 	private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
 	@Override
 	public void configureMessageBroker(MessageBrokerRegistry config) {
-        config.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
-        config.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
 		config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
 		config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
 	}
 
 	@Override
 	public void registerStompEndpoints(StompEndpointRegistry registry) {
-        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
-                .setAllowedOriginPatterns(
-                        "*",
-                        "https://assu.shop",
-                        "http://localhost:63342",
-                        "http://localhost:5173",     // Vite 기본
-                        "http://localhost:3000",     // CRA/Next 기본
-                        "http://127.0.0.1:*",
-                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
-        // fallback for old browsers
-
-            // 클라이언트 WebSocket 연결 주소
+		registry.addEndpoint("/ws").setAllowedOriginPatterns("*");          // 클라이언트 WebSocket 연결 주소
 			// .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
 	}
 
diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
index 6e3cebb..cdb02cb 100644
--- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
@@ -1,35 +1,32 @@
-//package com.assu.server.domain.chat.config;
-//
-//import org.springframework.context.annotation.Configuration;
-//import org.springframework.messaging.simp.config.ChannelRegistration;
-//import org.springframework.messaging.simp.config.MessageBrokerRegistry;
-//import org.springframework.web.socket.config.annotation.*;
-//
-//@Configuration
-//@EnableWebSocketMessageBroker
-//public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-//
-//
-//    @Override
-//    public void registerStompEndpoints(StompEndpointRegistry registry) {
-//        registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
-//                .setAllowedOriginPatterns(
-//                        "*",
-//                        "https://assu.shop",
-//                        "http://localhost:63342",
-//                        "http://localhost:5173",     // Vite 기본
-//                        "http://localhost:3000",     // CRA/Next 기본
-//                        "http://127.0.0.1:*",
-//                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용
-//                      // fallback for old browsers
-//
-//    }
-//
-//    @Override
-//    public void configureMessageBroker(MessageBrokerRegistry registry) {
-//        registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
-//        registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
-//        registry.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
-//        registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
-//    }
-//}
+package com.assu.server.domain.chat.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.config.annotation.*;
+
+@Configuration
+@EnableWebSocketMessageBroker
+public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {
+        registry.addEndpoint("/ws/chat")  // 클라이언트 WebSocket 연결 지점
+                .setAllowedOriginPatterns(
+                        "http://localhost:63342",
+                        "http://localhost:5173",     // Vite 기본
+                        "http://localhost:3000",     // CRA/Next 기본
+                        "http://127.0.0.1:*",
+                        "http://192.168.*.*:*")       // 같은 LAN의 실제 기기 테스트용
+                .withSockJS();             // fallback for old browsers
+
+        // ✅ 모바일/안드로이드용 (네이티브 WebSocket)
+        registry.addEndpoint("/ws/chat-native")
+                .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅
+    }
+
+    @Override
+    public void configureMessageBroker(MessageBrokerRegistry registry) {
+        registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
+        registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index f732a7f..cd8d672 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -95,8 +95,6 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM
         log.info("saved message start");
         Message saved = messageRepository.saveAndFlush(message);
         log.info("saved message middle");
-        log.info("REQ roomId={}, senderId={}, receiverId={}, message={}",
-                request.roomId(), request.senderId(), request.receiverId(), request.message());
         log.info("saved message id={}, roomId={}, senderId={}, receiverId={}",
                 saved.getId(), room.getId(), sender.getId(), receiver.getId());
 
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 73c9772..06b3642 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -22,7 +22,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                         .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
 
                         // ✅ WebSocket 핸드셰이크 허용 (네이티브 + SockJS 모두 포함)
-                        .requestMatchers("/ws","/ws/**").permitAll()
+                        .requestMatchers("/ws/**").permitAll()
 
                         // Swagger 등 공개 리소스
                         .requestMatchers(

From 8ceb131ec0bbbcf4223ec21015ffc251b3234d45 Mon Sep 17 00:00:00 2001
From: SJ Hwang 
Date: Wed, 17 Sep 2025 02:13:25 +0900
Subject: [PATCH 189/270] =?UTF-8?q?[MOD/#24]=20=20-=20=EC=B1=84=ED=8C=85?=
 =?UTF-8?q?=EB=B0=A9=20=EB=82=B4=20=EC=A0=9C=ED=9C=B4=20=ED=99=95=EC=9D=B8?=
 =?UTF-8?q?=20(=EC=A0=9C=ED=9C=B4=EC=97=85=EC=B2=B4)=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/partnership/service/PartnershipServiceImpl.java      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 dccdc76..b25201b 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
@@ -413,7 +413,7 @@ public PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO checkPartne
         Admin admin = adminRepository.findById(adminId)
                 .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN));
 
-        List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND);
+        List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND, ActivationStatus.BLANK);
         boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses);
 
         Long paperId = null;

From 70e3569fa7888364a230fe0e18211a89eb81aa09 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Wed, 17 Sep 2025 05:45:55 +0900
Subject: [PATCH 190/270] =?UTF-8?q?[FIX]=20logout=20delete=20=EB=A7=A4?=
 =?UTF-8?q?=ED=95=91=20->=20post=20=EB=A7=A4=ED=95=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/auth/controller/AuthController.java | 2 +-
 .../server/global/apiPayload/code/status/SuccessStatus.java    | 2 +-
 .../java/com/assu/server/global/config/SecurityConfig.java     | 3 ---
 3 files changed, 2 insertions(+), 5 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 848c2c3..b0e78e7 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
@@ -305,7 +305,7 @@ public BaseResponse refreshToken(
                     "- 처리: Refresh 무효화(선택), Access 블랙리스트 등록.\n" +
                     "- 성공 시 200(OK)."
     )
-    @DeleteMapping("/logout")
+    @PostMapping("/logout")
     public BaseResponse logout(
             @RequestHeader("Authorization")
             @Parameter(name = "Authorization", description = "Access Token. 형식: `Bearer `", required = true,
diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
index 4e185c4..aba56af 100644
--- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
+++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java
@@ -19,7 +19,7 @@ public enum SuccessStatus implements BaseCode {
     MEMBER_CREATED(HttpStatus.CREATED, "MEMBER_201", "성공적으로 생성되었습니다."),
 
     //인증 관련 성공
-    SEND_AUTH_NUMBER_SUCCESS(HttpStatus.OK, "AUTH_200", "성공적으로 조회되었습니다."),
+    SEND_AUTH_NUMBER_SUCCESS(HttpStatus.OK, "AUTH_200", "성공적으로 전송되었습니다."),
     VERIFY_AUTH_NUMBER_SUCCESS(HttpStatus.OK, "AUTH_201", "성공적으로 생성되었습니다."),
 
     //신고 성공
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 6226353..e0a8f2c 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -27,9 +27,6 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                                 "/swagger-resources/**", "/webjars/**"
                         ).permitAll()
 
-                        // 로그아웃은 인증 필요
-                        .requestMatchers("/auth/logout").authenticated()
-
                         .requestMatchers(// Auth (로그아웃 제외)
                                 "/auth/phone-verification/send",
                                 "/auth/phone-verification/verify",

From c35dc318426cdb0bc2d05d8cd06dcf55bf9757ec Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 17 Sep 2025 13:12:47 +1000
Subject: [PATCH 191/270] =?UTF-8?q?[FEAT/#110]=20-=20getStores=20=EC=A1=B0?=
 =?UTF-8?q?=ED=9A=8C=20=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/map/service/MapServiceImpl.java    | 41 ++++++++++++-------
 .../repository/PaperContentRepository.java    | 21 ++++++++++
 2 files changed, 48 insertions(+), 14 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index afa49c5..7b41b7b 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -105,29 +105,42 @@ public List getAdmins(MapRequestDTO.ViewOnMa
 
     @Override
     public List getStores(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) {
-        String wkt = toWKT(viewport);
-        List stores = storeRepository.findAllWithinViewport(wkt);
+        final String wkt = toWKT(viewport);
 
-        return stores.stream().map(s -> {
-            boolean hasPartner = (s.getPartner() != null);
+        // 1) 뷰포트 내 매장 조회
+        final List stores = storeRepository.findAllWithinViewport(wkt);
 
-            PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId())
-                    .orElseThrow(
-                        () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT)
-                    );
+        // 2) 매장별 content는 "있으면 사용, 없으면 null" 전략
+        return stores.stream().map(s -> {
+            final boolean hasPartner = (s.getPartner() != null);
 
-            String key = (s.getPartner() != null) ? s.getPartner().getMember().getProfileUrl() : null;
-            String url = amazonS3Manager.generatePresignedUrl(key);
+            // 2-1) 유효한 paper_content만 조회 (없으면 null 허용)
+            final PaperContent content = paperContentRepository
+                    .findLatestValidByStoreId(s.getId())
+                    .orElse(null);
 
-            Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
+            // 2-2) admin 정보 (null-safe)
+            final Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
                     .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null)
                     .orElse(null);
 
-            Admin admin = adminRepository.findById(adminId).orElse(null);
+            String adminName = null;
+            if (adminId != null) {
+                final Admin admin = adminRepository.findById(adminId).orElse(null);
+                adminName = (admin != null ? admin.getName() : null);
+            }
+
+            // 2-3) S3 presigned URL (키가 없으면 null)
+            final String key = (s.getPartner() != null && s.getPartner().getMember() != null)
+                    ? s.getPartner().getMember().getProfileUrl()
+                    : null;
+            final String profileUrl = (key != null ? amazonS3Manager.generatePresignedUrl(key) : null);
+
+            // 2-4) DTO 빌드 (content null 허용)
             return MapResponseDTO.StoreMapResponseDTO.builder()
                     .storeId(s.getId())
                     .adminId(adminId)
-                    .adminName(admin.getName())
+                    .adminName(adminName)
                     .name(s.getName())
                     .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress())
                     .rate(s.getRate())
@@ -140,7 +153,7 @@ public List getStores(MapRequestDTO.ViewOnMa
                     .hasPartner(hasPartner)
                     .latitude(s.getLatitude())
                     .longitude(s.getLongitude())
-                    .profileUrl(url)
+                    .profileUrl(profileUrl)
                     .build();
         }).toList();
     }
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
index 9c478c3..2ffc299 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
@@ -31,4 +31,25 @@ public interface PaperContentRepository extends JpaRepository findAllByOnePaperIdInFetchGoods(@Param("paperIds") Long paperIds);
 
     Optional findById(Long id);
+
+    @Query("""
+        SELECT pc FROM PaperContent pc
+        JOIN pc.paper p
+        WHERE p.store.id = :storeId
+          AND p.isActivated = com.assu.server.domain.paper.enums.IsActivated.ACTIVE
+          AND CURRENT_DATE BETWEEN p.partnershipPeriodStart AND p.partnershipPeriodEnd
+          AND (
+                (pc.optionType = com.assu.server.domain.paper.enums.OptionType.SERVICE AND
+                    (
+                      (pc.criterionType = com.assu.server.domain.paper.enums.CriterionType.PRICE AND pc.cost IS NOT NULL)
+                      OR
+                      (pc.criterionType = com.assu.server.domain.paper.enums.CriterionType.HEADCOUNT AND pc.cost IS NOT NULL AND pc.people IS NOT NULL)
+                    )
+                )
+                OR
+                (pc.optionType = com.assu.server.domain.paper.enums.OptionType.DISCOUNT AND pc.discount IS NOT NULL)
+              )
+        ORDER BY pc.id DESC
+        """)
+    Optional findLatestValidByStoreId(@Param("storeId") Long storeId);
 }

From 1d52e478223d9742c4a00a8473ad5d7767a61f1e Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 17 Sep 2025 13:29:58 +1000
Subject: [PATCH 192/270] =?UTF-8?q?[FEAT/#110]=20-=20=EC=BF=BC=EB=A6=AC=20?=
 =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/map/service/MapServiceImpl.java    | 12 ++++++--
 .../repository/PaperContentRepository.java    | 28 ++++++++++++++-----
 2 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index 7b41b7b..6212e3d 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -12,6 +12,7 @@
 import com.assu.server.domain.partnership.entity.Goods;
 import com.assu.server.domain.partnership.entity.Paper;
 import com.assu.server.domain.partnership.entity.PaperContent;
+import com.assu.server.domain.partnership.entity.enums.CriterionType;
 import com.assu.server.domain.partnership.entity.enums.OptionType;
 import com.assu.server.domain.partnership.repository.GoodsRepository;
 import com.assu.server.domain.partnership.repository.PaperContentRepository;
@@ -115,9 +116,14 @@ public List getStores(MapRequestDTO.ViewOnMa
             final boolean hasPartner = (s.getPartner() != null);
 
             // 2-1) 유효한 paper_content만 조회 (없으면 null 허용)
-            final PaperContent content = paperContentRepository
-                    .findLatestValidByStoreId(s.getId())
-                    .orElse(null);
+            final PaperContent content = paperContentRepository.findLatestValidByStoreId(
+                    s.getId(),
+                    ActivationStatus.ACTIVE,
+                    OptionType.SERVICE,
+                    OptionType.DISCOUNT,
+                    CriterionType.PRICE,
+                    CriterionType.HEADCOUNT
+            ).orElse(null);
 
             // 2-2) admin 정보 (null-safe)
             final Long adminId = paperRepository.findTopPaperByStoreId(s.getId())
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
index 2ffc299..15bfa4c 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
@@ -1,6 +1,9 @@
 package com.assu.server.domain.partnership.repository;
 
+import com.assu.server.domain.common.enums.ActivationStatus;
 import com.assu.server.domain.partnership.entity.PaperContent;
+import com.assu.server.domain.partnership.entity.enums.CriterionType;
+import com.assu.server.domain.partnership.entity.enums.OptionType;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
@@ -36,20 +39,31 @@ public interface PaperContentRepository extends JpaRepository findLatestValidByStoreId(@Param("storeId") Long storeId);
+    Optional findLatestValidByStoreId(
+            @Param("storeId") Long storeId,
+            @Param("active") ActivationStatus active,
+            @Param("service") OptionType service,
+            @Param("discount") OptionType discount,
+            @Param("price") CriterionType price,
+            @Param("headcount") CriterionType headcount
+    );
 }

From 3c8223986aebc5886e6de34714e35451cf218eea Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Wed, 17 Sep 2025 15:05:56 +1000
Subject: [PATCH 193/270] =?UTF-8?q?[BUG/#110]=20-=20=EC=A4=91=EB=B3=B5=20?=
 =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/map/service/MapServiceImpl.java    | 12 ++--
 .../repository/PaperContentRepository.java    | 59 ++++++++++---------
 2 files changed, 36 insertions(+), 35 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index 6212e3d..d9b0d68 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -116,13 +116,13 @@ public List getStores(MapRequestDTO.ViewOnMa
             final boolean hasPartner = (s.getPartner() != null);
 
             // 2-1) 유효한 paper_content만 조회 (없으면 null 허용)
-            final PaperContent content = paperContentRepository.findLatestValidByStoreId(
+            final PaperContent content = paperContentRepository.findLatestValidByStoreIdNative(
                     s.getId(),
-                    ActivationStatus.ACTIVE,
-                    OptionType.SERVICE,
-                    OptionType.DISCOUNT,
-                    CriterionType.PRICE,
-                    CriterionType.HEADCOUNT
+                    ActivationStatus.ACTIVE.name(),
+                    OptionType.SERVICE.name(),
+                    OptionType.DISCOUNT.name(),
+                    CriterionType.PRICE.name(),
+                    CriterionType.HEADCOUNT.name()
             ).orElse(null);
 
             // 2-2) admin 정보 (null-safe)
diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
index 15bfa4c..8f4336a 100644
--- a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
+++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java
@@ -35,35 +35,36 @@ public interface PaperContentRepository extends JpaRepository findById(Long id);
 
-    @Query("""
-        SELECT pc FROM PaperContent pc
-        JOIN pc.paper p
-        WHERE p.store.id = :storeId
-          AND p.isActivated = :active
-          AND (
-                (p.partnershipPeriodStart IS NULL OR p.partnershipPeriodEnd IS NULL)
-                OR
-                (CURRENT_DATE BETWEEN p.partnershipPeriodStart AND p.partnershipPeriodEnd)
-              )
-          AND (
-                (pc.optionType = :service AND
-                    (
-                      (pc.criterionType = :price AND pc.cost IS NOT NULL)
-                      OR
-                      (pc.criterionType = :headcount AND pc.cost IS NOT NULL AND pc.people IS NOT NULL)
-                    )
-                )
-                OR
-                (pc.optionType = :discount AND pc.discount IS NOT NULL)
-              )
-        ORDER BY pc.id DESC
-        """)
-    Optional findLatestValidByStoreId(
+    @Query(value = """
+SELECT pc.*
+FROM paper_content pc
+JOIN paper p ON p.id = pc.paper_id
+WHERE p.store_id = :storeId
+  AND p.is_activated = :active
+  AND CURRENT_DATE BETWEEN p.partnership_period_start AND p.partnership_period_end
+  AND (
+       (pc.option_type = :service AND
+         ((pc.criterion_type = :price AND pc.cost IS NOT NULL)
+       OR  (pc.criterion_type = :headcount AND pc.cost IS NOT NULL AND pc.people IS NOT NULL)))
+    OR (pc.option_type = :discount AND pc.discount IS NOT NULL)
+  )
+ORDER BY
+  CASE pc.option_type
+    WHEN :service THEN 0 ELSE 1 END,              -- SERVICE 우선
+  CASE pc.criterion_type
+    WHEN :price THEN 0
+    WHEN :headcount THEN 1
+    ELSE 2 END,                                   -- PRICE > HEADCOUNT > 기타
+  pc.updated_at DESC,
+  pc.id DESC
+LIMIT 1
+""", nativeQuery = true)
+    Optional findLatestValidByStoreIdNative(
             @Param("storeId") Long storeId,
-            @Param("active") ActivationStatus active,
-            @Param("service") OptionType service,
-            @Param("discount") OptionType discount,
-            @Param("price") CriterionType price,
-            @Param("headcount") CriterionType headcount
+            @Param("active") String active,            // ActivationStatus.ACTIVE.name()
+            @Param("service") String service,          // OptionType.SERVICE.name()
+            @Param("discount") String discount,        // OptionType.DISCOUNT.name()
+            @Param("price") String price,              // CriterionType.PRICE.name()
+            @Param("headcount") String headcount       // CriterionType.HEADCOUNT.name()
     );
 }

From a492b713a2e131eb0e667abe4a6fe94733821445 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Wed, 17 Sep 2025 21:17:40 +0900
Subject: [PATCH 194/270] =?UTF-8?q?refactor/#38=20-=20chatting=EC=9D=84=20?=
 =?UTF-8?q?=EC=9C=84=ED=95=9C=20WebSocketConfig=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/chat/config/WebSocketConfig.java  | 5 ++---
 .../java/com/assu/server/global/config/SecurityConfig.java   | 2 +-
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
index cdb02cb..7544a9e 100644
--- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
@@ -16,11 +16,10 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
                         "http://localhost:5173",     // Vite 기본
                         "http://localhost:3000",     // CRA/Next 기본
                         "http://127.0.0.1:*",
-                        "http://192.168.*.*:*")       // 같은 LAN의 실제 기기 테스트용
-                .withSockJS();             // fallback for old browsers
+                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용// fallback for old browsers
 
         // ✅ 모바일/안드로이드용 (네이티브 WebSocket)
-        registry.addEndpoint("/ws/chat-native")
+        registry.addEndpoint("/ws")
                 .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅
     }
 
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 06b3642..864c02f 100644
--- a/src/main/java/com/assu/server/global/config/SecurityConfig.java
+++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java
@@ -22,7 +22,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF
                         .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
 
                         // ✅ WebSocket 핸드셰이크 허용 (네이티브 + SockJS 모두 포함)
-                        .requestMatchers("/ws/**").permitAll()
+                        .requestMatchers("/ws/**","/ws").permitAll()
 
                         // Swagger 등 공개 리소스
                         .requestMatchers(

From 3349a211502eae0fafb1e096e29fcebf9910036b Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Thu, 18 Sep 2025 00:44:03 +1000
Subject: [PATCH 195/270] =?UTF-8?q?[FEAT/#128]=20-=20=ED=94=84=EB=A1=9C?=
 =?UTF-8?q?=ED=95=84=20=EC=82=AC=EC=A7=84=20=EC=A1=B0=ED=9A=8C=20API=20?=
 =?UTF-8?q?=EA=B0=9C=EB=B0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../inquiry/controller/InquiryController.java | 14 ++++++++++++
 .../inquiry/service/ProfileImageService.java  |  1 +
 .../service/ProfileImageServiceImpl.java      | 22 +++++++++++++++++++
 .../assu/server/infra/s3/AmazonS3Manager.java |  4 ++--
 4 files changed, 39 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index b5c173e..a8a6807 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -123,4 +123,18 @@ public BaseResponse uploadOrReplaceProfileImage(
         String key = profileImageService.updateProfileImage(pd.getMemberId(), image);
         return BaseResponse.onSuccess(SuccessStatus._OK, new ProfileImageResponse(key));
     }
+
+    @Operation(
+            summary = "프로필 이미지 조회 API",
+            description = "# [v1.0 (2025-09-18)](/2711197c19ed8039bbe2c48380c9f4c8?source=copy_link)\n" +
+                    "- 로그인한 사용자의 프로필 이미지 presigned URL을 반환합니다.\n" +
+                    "- URL은 일정 시간 동안만 유효합니다."
+    )
+    @GetMapping("/profile/image")
+    public BaseResponse getProfileImage(
+            @AuthenticationPrincipal PrincipalDetails pd
+    ) {
+        String url = profileImageService.getProfileImageUrl(pd.getMemberId());
+        return BaseResponse.onSuccess(SuccessStatus._OK, new ProfileImageResponse(url));
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java
index d19a5db..09a2282 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageService.java
@@ -4,4 +4,5 @@
 
 public interface ProfileImageService {
     String updateProfileImage(Long memberId, MultipartFile image);
+    String getProfileImageUrl(Long memberId);
 }
diff --git a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java
index 3f79788..79fee6f 100644
--- a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java
@@ -51,5 +51,27 @@ public String updateProfileImage(Long memberId, MultipartFile image) {
         // 5) 호출자에 key 반환 (FE는 필요 시 presigned URL 생성해 사용)
         return uploadedKey;
     }
+
+    @Override
+    @Transactional(readOnly = true)
+    public String getProfileImageUrl(Long memberId) {
+        Member member = memberRepository.findById(memberId)
+                .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER));
+
+        String keyOrUrl = member.getProfileUrl();
+        if (keyOrUrl == null || keyOrUrl.isBlank()) {
+            throw new CustomAuthException(ErrorStatus.PROFILE_IMAGE_NOT_FOUND);
+        }
+
+        if (keyOrUrl.startsWith("http://") || keyOrUrl.startsWith("https://")) {
+            return keyOrUrl;
+        }
+
+        String presigned = amazonS3Manager.generatePresignedUrl(keyOrUrl);
+        if (presigned == null) {
+            throw new CustomAuthException(ErrorStatus.PROFILE_IMAGE_NOT_FOUND);
+        }
+        return presigned;
+    }
 }
 
diff --git a/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java b/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
index 8ec2f5d..6c875a4 100644
--- a/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
+++ b/src/main/java/com/assu/server/infra/s3/AmazonS3Manager.java
@@ -71,7 +71,7 @@ public String uploadFile(String keyName, byte[] fileBytes, String contentType) {
     }
 
 
-    // FE로 url을 보내기 위해 사용하는 메서드
+    // 그대로 사용
     public String generatePresignedUrl(String keyName) {
         if (keyName == null || keyName.isBlank()) return null;
 
@@ -81,7 +81,7 @@ public String generatePresignedUrl(String keyName) {
                 .build();
 
         GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
-                .signatureDuration(Duration.ofMinutes(10))
+                .signatureDuration(Duration.ofMinutes(10)) // 유효기간 10분
                 .getObjectRequest(getObjectRequest)
                 .build();
 

From e1bec634ae4e7dc894e62816f962c620141b7f43 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Thu, 18 Sep 2025 00:56:21 +1000
Subject: [PATCH 196/270] =?UTF-8?q?[FEAT/#128]=20-=20=EB=A7=81=ED=81=AC=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../server/domain/inquiry/controller/InquiryController.java     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
index a8a6807..93d7f58 100644
--- a/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
+++ b/src/main/java/com/assu/server/domain/inquiry/controller/InquiryController.java
@@ -126,7 +126,7 @@ public BaseResponse uploadOrReplaceProfileImage(
 
     @Operation(
             summary = "프로필 이미지 조회 API",
-            description = "# [v1.0 (2025-09-18)](/2711197c19ed8039bbe2c48380c9f4c8?source=copy_link)\n" +
+            description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2711197c19ed8039bbe2c48380c9f4c8?source=copy_link)\n" +
                     "- 로그인한 사용자의 프로필 이미지 presigned URL을 반환합니다.\n" +
                     "- URL은 일정 시간 동안만 유효합니다."
     )

From fa21828b509ff72bd0be92ff2a191ea396a6a59f Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 18 Sep 2025 03:51:05 +0900
Subject: [PATCH 197/270] =?UTF-8?q?[FEAT/#131]=20=EC=A0=84=ED=99=94?=
 =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B0=8F=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?=
 =?UTF-8?q?=EC=A4=91=EB=B3=B5=EA=B2=80=EC=82=AC=20API=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../auth/controller/AuthController.java       | 32 +++++++++++++++
 .../verification/VerificationRequestDTO.java  | 32 +++++++++++++++
 .../auth/service/VerificationService.java     | 11 +++++
 .../auth/service/VerificationServiceImpl.java | 40 +++++++++++++++++++
 4 files changed, 115 insertions(+)
 create mode 100644 src/main/java/com/assu/server/domain/auth/dto/verification/VerificationRequestDTO.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/VerificationService.java
 create mode 100644 src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java

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 b0e78e7..7351921 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
@@ -13,6 +13,7 @@
 import com.assu.server.domain.auth.dto.ssu.USaintAuthResponse;
 import com.assu.server.domain.auth.service.*;
 import com.assu.server.domain.user.entity.enums.University;
+import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO;
 import com.assu.server.global.apiPayload.BaseResponse;
 import com.assu.server.global.apiPayload.code.status.SuccessStatus;
 import io.swagger.v3.oas.annotations.Operation;
@@ -41,6 +42,7 @@ public class AuthController {
     private final LogoutService logoutService;
     private final SSUAuthService ssuAuthService;
     private final WithdrawalService withdrawalService;
+    private final VerificationService verificationService;
 
     @Operation(
             summary = "휴대폰 인증번호 발송 API",
@@ -82,6 +84,36 @@ public BaseResponse checkAuthNumber(
         return BaseResponse.onSuccess(SuccessStatus.VERIFY_AUTH_NUMBER_SUCCESS, null);
     }
 
+    @Operation(summary = "전화번호 중복 체크 API", description = "# [v1.0 (2025-01-15)]\n" +
+                    "- 입력한 전화번호가 이미 가입된 사용자가 있는지 확인합니다.\n" +
+                    "- 중복된 전화번호가 있으면 에러를 반환합니다.\n" +
+                    "\n**Request Body:**\n" +
+                    "  - `phoneNumber` (String, required): 확인할 전화번호 (010XXXXXXXX 형식)\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 사용 가능 메시지 반환\n" +
+                    "  - 중복 시 404(NOT_FOUND)와 에러 메시지 반환")
+    @PostMapping("/phone-verification/check")
+    public BaseResponse checkPhoneNumberAvailability(
+            @RequestBody @Valid VerificationRequestDTO.PhoneVerificationCheckRequest request) {
+        verificationService.checkPhoneNumberAvailability(request);
+        return BaseResponse.onSuccess(SuccessStatus._OK, null);
+    }
+
+    @Operation(summary = "이메일 중복 체크 API", description = "# [v1.0 (2025-01-15)]\n" +
+                    "- 입력한 이메일이 이미 가입된 사용자가 있는지 확인합니다.\n" +
+                    "- 중복된 이메일이 있으면 에러를 반환합니다.\n" +
+                    "\n**Request Body:**\n" +
+                    "  - `email` (String, required): 확인할 이메일 주소\n" +
+                    "\n**Response:**\n" +
+                    "  - 성공 시 200(OK)과 사용 가능 메시지 반환\n" +
+                    "  - 중복 시 404(NOT_FOUND)와 에러 메시지 반환")
+    @PostMapping("/email-verification/check")
+    public BaseResponse checkEmailAvailability(
+            @RequestBody @Valid VerificationRequestDTO.EmailVerificationCheckRequest request) {
+        verificationService.checkEmailAvailability(request);
+        return BaseResponse.onSuccess(SuccessStatus._OK, null);
+    }
+
     @Operation(
             summary = "학생 회원가입 API",
             description = "# [v1.2 (2025-09-13)](https://clumsy-seeder-416.notion.site/2241197c19ed81129c85cf5bbe1f7971)\n" +
diff --git a/src/main/java/com/assu/server/domain/auth/dto/verification/VerificationRequestDTO.java b/src/main/java/com/assu/server/domain/auth/dto/verification/VerificationRequestDTO.java
new file mode 100644
index 0000000..5ef713e
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/dto/verification/VerificationRequestDTO.java
@@ -0,0 +1,32 @@
+package com.assu.server.domain.auth.dto.verification;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+public class VerificationRequestDTO {
+
+    @Builder
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class PhoneVerificationCheckRequest {
+        @NotBlank(message = "전화번호는 필수입니다.")
+        @Pattern(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다.")
+        private String phoneNumber;
+    }
+
+    @Builder
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class EmailVerificationCheckRequest {
+        @NotBlank(message = "이메일은 필수입니다.")
+        @Email(message = "올바른 이메일 형식이 아닙니다.")
+        private String email;
+    }
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java b/src/main/java/com/assu/server/domain/auth/service/VerificationService.java
new file mode 100644
index 0000000..aaf2823
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/VerificationService.java
@@ -0,0 +1,11 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO;
+
+public interface VerificationService {
+    void checkPhoneNumberAvailability(
+            VerificationRequestDTO.PhoneVerificationCheckRequest request);
+
+    void checkEmailAvailability(
+            VerificationRequestDTO.EmailVerificationCheckRequest request);
+}
diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java
new file mode 100644
index 0000000..f3020e3
--- /dev/null
+++ b/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java
@@ -0,0 +1,40 @@
+package com.assu.server.domain.auth.service;
+
+import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO;
+import com.assu.server.domain.auth.dto.verification.VerificationResponseDTO;
+import com.assu.server.domain.auth.exception.CustomAuthException;
+import com.assu.server.domain.auth.repository.CommonAuthRepository;
+import com.assu.server.domain.member.repository.MemberRepository;
+import com.assu.server.global.apiPayload.code.status.ErrorStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class VerificationServiceImpl implements VerificationService {
+
+    private final MemberRepository memberRepository;
+    private final CommonAuthRepository commonAuthRepository;
+
+    @Override
+    public void checkPhoneNumberAvailability(
+            VerificationRequestDTO.PhoneVerificationCheckRequest request) {
+
+        boolean exists = memberRepository.existsByPhoneNum(request.getPhoneNumber());
+
+        if (exists) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_PHONE);
+        }
+    }
+
+    @Override
+    public void checkEmailAvailability(
+            VerificationRequestDTO.EmailVerificationCheckRequest request) {
+
+        boolean exists = commonAuthRepository.existsByEmail(request.getEmail());
+
+        if (exists) {
+            throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL);
+        }
+    }
+}

From fa5ac781ba68b096b76bf4c8bd41270405308686 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 18 Sep 2025 03:51:24 +0900
Subject: [PATCH 198/270] =?UTF-8?q?[FEAT/#131]=20=EC=A0=84=ED=99=94?=
 =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B0=8F=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?=
 =?UTF-8?q?=EC=A4=91=EB=B3=B5=EA=B2=80=EC=82=AC=20API=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assu/server/domain/auth/service/VerificationServiceImpl.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java
index f3020e3..85cc43e 100644
--- a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java
@@ -1,7 +1,6 @@
 package com.assu.server.domain.auth.service;
 
 import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO;
-import com.assu.server.domain.auth.dto.verification.VerificationResponseDTO;
 import com.assu.server.domain.auth.exception.CustomAuthException;
 import com.assu.server.domain.auth.repository.CommonAuthRepository;
 import com.assu.server.domain.member.repository.MemberRepository;

From 9fa77c65a9967d648fbc9a32d6fe0814f352e381 Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 18 Sep 2025 03:52:15 +0900
Subject: [PATCH 199/270] =?UTF-8?q?[FIX]=20=ED=81=AC=EB=A1=A4=EB=A7=81=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 과정/학적 -> 과정/학기로 수정
- 학기중에는 해당 부분의 워딩이 바뀌는 것으로 추정됨
---
 .../com/assu/server/domain/auth/service/SSUAuthServiceImpl.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
index 5c22bdf..616cb2d 100644
--- a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
@@ -149,7 +149,7 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) {
                         }
                     }
                 }
-                case "과정/학적" -> usaintAuthResponse.setEnrollmentStatus(strong.text());
+                case "과정/학기" -> usaintAuthResponse.setEnrollmentStatus(strong.text());
                 case "학년/학기" -> usaintAuthResponse.setYearSemester(strong.text());
             }
         }

From 76fff2f2938e81ba7c2e10d386aba3e62b35cb7b Mon Sep 17 00:00:00 2001
From: 2ghrms 
Date: Thu, 18 Sep 2025 04:01:31 +0900
Subject: [PATCH 200/270] =?UTF-8?q?[FEAT/#131]=20Major=20=EB=B0=8F=20Depar?=
 =?UTF-8?q?tment=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../auth/service/SSUAuthServiceImpl.java      | 65 +++++++++++++++++-
 .../domain/user/entity/enums/Department.java  | 10 ++-
 .../domain/user/entity/enums/Major.java       | 66 ++++++++++++++++++-
 3 files changed, 135 insertions(+), 6 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
index 616cb2d..515d7cf 100644
--- a/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/auth/service/SSUAuthServiceImpl.java
@@ -136,6 +136,63 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) {
 
                     // 매핑된 Enum 값 저장
                     switch (majorStr) {
+                        // 인문대학
+                        case "기독교학과" -> usaintAuthResponse.setMajor(Major.CHRISTIAN_STUDIES);
+                        case "국어국문학과" -> usaintAuthResponse.setMajor(Major.KOREAN_LITERATURE);
+                        case "영어영문학과" -> usaintAuthResponse.setMajor(Major.ENGLISH_LITERATURE);
+                        case "독어독문학과" -> usaintAuthResponse.setMajor(Major.GERMAN_LITERATURE);
+                        case "불어불문학과" -> usaintAuthResponse.setMajor(Major.FRENCH_LITERATURE);
+                        case "중어중문학과" -> usaintAuthResponse.setMajor(Major.CHINESE_LITERATURE);
+                        case "일어일문학과" -> usaintAuthResponse.setMajor(Major.JAPANESE_LITERATURE);
+                        case "철학과" -> usaintAuthResponse.setMajor(Major.PHILOSOPHY);
+                        case "사학과" -> usaintAuthResponse.setMajor(Major.HISTORY);
+                        case "예술창작학부" -> usaintAuthResponse.setMajor(Major.CREATIVE_ARTS);
+                        case "스포츠학부" -> usaintAuthResponse.setMajor(Major.SPORTS);
+
+                        // 자연과학대학
+                        case "수학과" -> usaintAuthResponse.setMajor(Major.MATHEMATICS);
+                        case "화학과" -> usaintAuthResponse.setMajor(Major.CHEMISTRY);
+                        case "의생명시스템학부" -> usaintAuthResponse.setMajor(Major.BIOMEDICAL_SYSTEMS);
+                        case "물리학과" -> usaintAuthResponse.setMajor(Major.PHYSICS);
+                        case "정보통계ㆍ보험수리학과" -> usaintAuthResponse.setMajor(Major.STATISTICS_ACTUARIAL);
+
+                        // 법과대학
+                        case "법학과" -> usaintAuthResponse.setMajor(Major.LAW);
+                        case "국제법무학과" -> usaintAuthResponse.setMajor(Major.INTERNATIONAL_LAW);
+
+                        // 사회과학대학
+                        case "사회복지학부" -> usaintAuthResponse.setMajor(Major.SOCIAL_WELFARE);
+                        case "정치외교학과" -> usaintAuthResponse.setMajor(Major.POLITICAL_SCIENCE);
+                        case "언론홍보학과" -> usaintAuthResponse.setMajor(Major.MEDIA_COMMUNICATION);
+                        case "행정학부" -> usaintAuthResponse.setMajor(Major.PUBLIC_ADMINISTRATION);
+                        case "정보사회학과" -> usaintAuthResponse.setMajor(Major.INFORMATION_SOCIETY);
+                        case "평생교육학과" -> usaintAuthResponse.setMajor(Major.LIFELONG_EDUCATION);
+
+                        // 경제통상대학
+                        case "경제학과" -> usaintAuthResponse.setMajor(Major.ECONOMICS);
+                        case "금융경제학과" -> usaintAuthResponse.setMajor(Major.FINANCIAL_ECONOMICS);
+                        case "글로벌통상학과" -> usaintAuthResponse.setMajor(Major.GLOBAL_TRADE);
+                        case "국제무역학과" -> usaintAuthResponse.setMajor(Major.INTERNATIONAL_TRADE);
+
+                        // 경영대학
+                        case "경영학부" -> usaintAuthResponse.setMajor(Major.BUSINESS_ADMINISTRATION);
+                        case "회계학과" -> usaintAuthResponse.setMajor(Major.ACCOUNTING);
+                        case "벤처경영학과" -> usaintAuthResponse.setMajor(Major.VENTURE_MANAGEMENT);
+                        case "복지경영학과" -> usaintAuthResponse.setMajor(Major.WELFARE_MANAGEMENT);
+                        case "벤처중소기업학과" -> usaintAuthResponse.setMajor(Major.VENTURE_SME);
+                        case "금융학부" -> usaintAuthResponse.setMajor(Major.FINANCE);
+                        case "혁신경영학과" -> usaintAuthResponse.setMajor(Major.INNOVATION_MANAGEMENT);
+                        case "회계세무학과" -> usaintAuthResponse.setMajor(Major.ACCOUNTING_TAX);
+
+                        // 공과대학
+                        case "화학공학과" -> usaintAuthResponse.setMajor(Major.CHEMICAL_ENGINEERING);
+                        case "전기공학부" -> usaintAuthResponse.setMajor(Major.ELECTRICAL_ENGINEERING);
+                        case "건축학부" -> usaintAuthResponse.setMajor(Major.ARCHITECTURE);
+                        case "산업ㆍ정보시스템공학과" -> usaintAuthResponse.setMajor(Major.INDUSTRIAL_INFO_SYSTEMS);
+                        case "기계공학부" -> usaintAuthResponse.setMajor(Major.MECHANICAL_ENGINEERING);
+                        case "신소재공학과" -> usaintAuthResponse.setMajor(Major.MATERIALS_SCIENCE);
+
+                        // IT대학
                         case "컴퓨터학부" -> usaintAuthResponse.setMajor(Major.COM);
                         case "소프트웨어학부" -> usaintAuthResponse.setMajor(Major.SW);
                         case "글로벌미디어학부" -> usaintAuthResponse.setMajor(Major.GM);
@@ -143,6 +200,10 @@ public USaintAuthResponse uSaintAuth(USaintAuthRequest uSaintAuthRequest) {
                         case "AI융합학부" -> usaintAuthResponse.setMajor(Major.AI);
                         case "전자정보공학부" -> usaintAuthResponse.setMajor(Major.EE);
                         case "정보보호학과" -> usaintAuthResponse.setMajor(Major.IP);
+
+                        // 자유전공학부
+                        case "자유전공학부" -> usaintAuthResponse.setMajor(Major.LIBERAL_ARTS);
+
                         default -> {
                             log.debug("{} is not a supported major.", majorStr);
                             throw new CustomAuthException(ErrorStatus.SSU_SAINT_UNSUPPORTED_MAJOR);
@@ -164,8 +225,8 @@ private ResponseEntity requestUSaintSSO(String sToken, String sIdno) {
                 .uri(url)
                 .header("Cookie", "sToken=" + sToken + "; sIdno=" + sIdno)
                 .retrieve()
-                .toEntity(String.class)   // ResponseEntity 전체 반환 (body + header 포함)
-                .block();                 // 동기 방식
+                .toEntity(String.class) // ResponseEntity 전체 반환 (body + header 포함)
+                .block(); // 동기 방식
     }
 
     private ResponseEntity requestUSaintPortal(StringBuilder cookie) {
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Department.java b/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
index 668c9dd..37b91f9 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Department.java
@@ -1,7 +1,15 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum Department {
-    IT("IT대학");
+    HUMANITIES("인문대학"),
+    NATURAL_SCIENCE("자연과학대학"),
+    LAW("법과대학"),
+    SOCIAL_SCIENCE("사회과학대학"),
+    ECONOMICS("경제통상대학"),
+    BUSINESS("경영대학"),
+    ENGINEERING("공과대학"),
+    IT("IT대학"),
+    LIBERAL_ARTS("자유전공학부");
 
     private final String displayName;
 
diff --git a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
index 2fbf076..302babe 100644
--- a/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
+++ b/src/main/java/com/assu/server/domain/user/entity/enums/Major.java
@@ -1,13 +1,73 @@
 package com.assu.server.domain.user.entity.enums;
 
 public enum Major {
+    // 인문대학
+    CHRISTIAN_STUDIES(Department.HUMANITIES, "기독교학과"),
+    KOREAN_LITERATURE(Department.HUMANITIES, "국어국문학과"),
+    ENGLISH_LITERATURE(Department.HUMANITIES, "영어영문학과"),
+    GERMAN_LITERATURE(Department.HUMANITIES, "독어독문학과"),
+    FRENCH_LITERATURE(Department.HUMANITIES, "불어불문학과"),
+    CHINESE_LITERATURE(Department.HUMANITIES, "중어중문학과"),
+    JAPANESE_LITERATURE(Department.HUMANITIES, "일어일문학과"),
+    PHILOSOPHY(Department.HUMANITIES, "철학과"),
+    HISTORY(Department.HUMANITIES, "사학과"),
+    CREATIVE_ARTS(Department.HUMANITIES, "예술창작학부"),
+    SPORTS(Department.HUMANITIES, "스포츠학부"),
+
+    // 자연과학대학
+    MATHEMATICS(Department.NATURAL_SCIENCE, "수학과"),
+    CHEMISTRY(Department.NATURAL_SCIENCE, "화학과"),
+    BIOMEDICAL_SYSTEMS(Department.NATURAL_SCIENCE, "의생명시스템학부"),
+    PHYSICS(Department.NATURAL_SCIENCE, "물리학과"),
+    STATISTICS_ACTUARIAL(Department.NATURAL_SCIENCE, "정보통계ㆍ보험수리학과"),
+
+    // 법과대학
+    LAW(Department.LAW, "법학과"),
+    INTERNATIONAL_LAW(Department.LAW, "국제법무학과"),
+
+    // 사회과학대학
+    SOCIAL_WELFARE(Department.SOCIAL_SCIENCE, "사회복지학부"),
+    POLITICAL_SCIENCE(Department.SOCIAL_SCIENCE, "정치외교학과"),
+    MEDIA_COMMUNICATION(Department.SOCIAL_SCIENCE, "언론홍보학과"),
+    PUBLIC_ADMINISTRATION(Department.SOCIAL_SCIENCE, "행정학부"),
+    INFORMATION_SOCIETY(Department.SOCIAL_SCIENCE, "정보사회학과"),
+    LIFELONG_EDUCATION(Department.SOCIAL_SCIENCE, "평생교육학과"),
+
+    // 경제통상대학
+    ECONOMICS(Department.ECONOMICS, "경제학과"),
+    FINANCIAL_ECONOMICS(Department.ECONOMICS, "금융경제학과"),
+    GLOBAL_TRADE(Department.ECONOMICS, "글로벌통상학과"),
+    INTERNATIONAL_TRADE(Department.ECONOMICS, "국제무역학과"),
+
+    // 경영대학
+    BUSINESS_ADMINISTRATION(Department.BUSINESS, "경영학부"),
+    ACCOUNTING(Department.BUSINESS, "회계학과"),
+    VENTURE_MANAGEMENT(Department.BUSINESS, "벤처경영학과"),
+    WELFARE_MANAGEMENT(Department.BUSINESS, "복지경영학과"),
+    VENTURE_SME(Department.BUSINESS, "벤처중소기업학과"),
+    FINANCE(Department.BUSINESS, "금융학부"),
+    INNOVATION_MANAGEMENT(Department.BUSINESS, "혁신경영학과"),
+    ACCOUNTING_TAX(Department.BUSINESS, "회계세무학과"),
+
+    // 공과대학
+    CHEMICAL_ENGINEERING(Department.ENGINEERING, "화학공학과"),
+    ELECTRICAL_ENGINEERING(Department.ENGINEERING, "전기공학부"),
+    ARCHITECTURE(Department.ENGINEERING, "건축학부"),
+    INDUSTRIAL_INFO_SYSTEMS(Department.ENGINEERING, "산업ㆍ정보시스템공학과"),
+    MECHANICAL_ENGINEERING(Department.ENGINEERING, "기계공학부"),
+    MATERIALS_SCIENCE(Department.ENGINEERING, "신소재공학과"),
+
+    // IT대학
     SW(Department.IT, "소프트웨어학부"),
-    GM(Department.IT, "글로벌미디어학과"),
+    GM(Department.IT, "글로벌미디어학부"),
     COM(Department.IT, "컴퓨터학부"),
     EE(Department.IT, "전자정보공학부"),
     IP(Department.IT, "정보보호학과"),
-    AI(Department.IT, "AI융합학과"),
-    MB(Department.IT, "미디어경영학과");
+    AI(Department.IT, "AI융합학부"),
+    MB(Department.IT, "미디어경영학과"),
+
+    // 자유전공학부
+    LIBERAL_ARTS(Department.LIBERAL_ARTS, "자유전공학부");
 
     private final Department department;
     private final String displayName;

From 693e912a8fbf616d23c153d4b201cddfc00f3847 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Thu, 18 Sep 2025 22:48:19 +0900
Subject: [PATCH 201/270] =?UTF-8?q?refactor/#38=20-=20=EB=A9=94=EC=8B=9C?=
 =?UTF-8?q?=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=20messageId=20?=
 =?UTF-8?q?=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20-=20?=
 =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20=EB=82=98=EA=B0=80=EA=B8=B0=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/chat/converter/ChatConverter.java     |  1 +
 .../domain/chat/service/ChatServiceImpl.java     | 16 ++++++++++++++--
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
index 70fe10c..6082b4c 100644
--- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
+++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java
@@ -63,6 +63,7 @@ public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO reque
 
     public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message message) {
         return ChatResponseDTO.SendMessageResponseDTO.builder()
+                .messageId(message.getId())
                 .roomId(message.getChattingRoom().getId())
                 .senderId(message.getSender().getId())
                 .receiverId(message.getReceiver().getId())
diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
index cd8d672..df09fde 100644
--- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java
@@ -162,9 +162,21 @@ public ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomI
             isLeftSuccessfully = true;
             chatRepository.save(chattingRoom);
         } else if(memberCount == 1) {
-            isRoomDeleted = true;
+            if (isAdmin) {
+                chattingRoom.setAdmin(null);
+            } else if (isPartner) {
+                chattingRoom.setPartner(null);
+            }
+            chattingRoom.updateMemberCount(0);
             isLeftSuccessfully = true;
-            chatRepository.delete(chattingRoom);
+
+            // ✅ 방에 아무도 안 남았을 때만 삭제
+            if (chattingRoom.getAdmin() == null && chattingRoom.getPartner() == null) {
+                isRoomDeleted = true;
+                chatRepository.delete(chattingRoom);
+            } else {
+                chatRepository.save(chattingRoom);
+            }
 
         } else if(memberCount == 0) {
             throw new DatabaseException(ErrorStatus.NO_MEMBER);

From 048dc36125e5e1c5e685c4dc89240374c2ede67c Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 19 Sep 2025 11:55:38 +1000
Subject: [PATCH 202/270] =?UTF-8?q?[FEAT/#128]=20-=20=EC=B6=94=EC=B2=9C=20?=
 =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20=ED=94=84=EB=A1=9C=ED=95=84=20?=
 =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B0=98=ED=99=98=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/assu/server/domain/admin/dto/AdminResponseDTO.java  | 1 +
 .../com/assu/server/domain/admin/service/AdminServiceImpl.java   | 1 +
 .../com/assu/server/domain/partner/dto/PartnerResponseDTO.java   | 1 +
 .../assu/server/domain/partner/service/PartnerServiceImpl.java   | 1 +
 4 files changed, 4 insertions(+)

diff --git a/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java b/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java
index 3157770..13d0a26 100644
--- a/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java
@@ -16,5 +16,6 @@ public static class RandomPartnerResponseDTO {
         private String partnerAddress;
         private String partnerDetailAddress;
         private String partnerName;
+        private String partnerUrl;
     }
 }
diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
index 824d562..f0c5208 100644
--- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java
@@ -57,6 +57,7 @@ public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long admin
                 .partnerName(picked.getName())
                 .partnerAddress(picked.getAddress())
                 .partnerDetailAddress(picked.getDetailAddress())
+                .partnerUrl(picked.getMember().getProfileUrl())
                 .build();
     }
 
diff --git a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java
index 517e8c5..82254bd 100644
--- a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java
@@ -26,5 +26,6 @@ public static class AdminLiteDTO {
         private String adminAddress;
         private String adminDetailAddress;
         private String adminName;
+        private String adminUrl;
     }
 }
diff --git a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
index 03ee1d1..265c0ea 100644
--- a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java
@@ -47,6 +47,7 @@ public PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(Long partnerId)
                         .adminAddress(a.getOfficeAddress())
                         .adminDetailAddress(a.getDetailAddress())
                         .adminName(a.getName())
+                        .adminUrl(a.getMember().getProfileUrl())
                         .build())
                 .collect(Collectors.toList());
 

From 04e2d70f7f4fc00ce09b6f5a4b38170dd04ef7d6 Mon Sep 17 00:00:00 2001
From: MiN <81948815+leesumin0526@users.noreply.github.com>
Date: Fri, 19 Sep 2025 22:35:48 +1000
Subject: [PATCH 203/270] =?UTF-8?q?[FEAT/#136]=20-=20=EC=A7=80=EB=8F=84=20?=
 =?UTF-8?q?=EB=B0=98=ED=99=98=EA=B0=92=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/assu/server/domain/map/service/MapServiceImpl.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
index d9b0d68..eeeee0c 100644
--- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
+++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java
@@ -240,7 +240,7 @@ public List searchPartner(String keyword,
                     .partnerId(p.getId())
                     .name(p.getName())
                     .address(p.getAddress() != null ? p.getAddress() : p.getDetailAddress())
-                    .isPartnered(true)
+                    .isPartnered(active != null)
                     .partnershipId(active != null ? active.getId() : null)
                     .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
                     .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)
@@ -267,7 +267,7 @@ public List searchAdmin(String keyword, Long
                     .adminId(a.getId())
                     .name(a.getName())
                     .address(a.getOfficeAddress() != null ? a.getOfficeAddress() : a.getDetailAddress())
-                    .isPartnered(true)
+                    .isPartnered(active != null)
                     .partnershipId(active != null ? active.getId() : null)
                     .partnershipStartDate(active != null ? active.getPartnershipPeriodStart() : null)
                     .partnershipEndDate(active != null ? active.getPartnershipPeriodEnd() : null)

From ce42c8ee3ebd06da5249f18b2ee252c4f656e9dc Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Fri, 19 Sep 2025 22:39:06 +0900
Subject: [PATCH 204/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?=
 =?UTF-8?q?=EB=B0=A9=20=EC=9D=B4=EB=A6=84=20=EC=84=A4=EC=A0=95=20&=20?=
 =?UTF-8?q?=EB=82=98=EA=B0=80=EA=B8=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?=
 =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../domain/chat/repository/ChatRepository.java       | 12 ++++++------
 .../partnership/converter/PartnershipConverter.java  |  2 ++
 .../partnership/dto/PartnershipResponseDTO.java      |  3 +++
 3 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java
index c3270c4..30b93c1 100644
--- a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java
+++ b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java
@@ -30,20 +30,20 @@ SELECT MAX(m2.createdAt)
      AND m.receiver.id = :memberId
      AND m.isRead = false),
      CASE
-         WHEN pm.id   IS NULL AND am.id = :memberId THEN NULL
-         WHEN am.id   IS NULL AND pm.id = :memberId THEN NULL
+         WHEN pm.id   IS NULL AND am.id = :memberId THEN -1
+         WHEN am.id   IS NULL AND pm.id = :memberId THEN -1
          WHEN pm.id = :memberId THEN a.id
          ELSE p.id
        END,
      CASE
-         WHEN pm.id   IS NULL AND am.id = :memberId THEN NULL
-         WHEN am.id   IS NULL AND pm.id = :memberId THEN NULL
+         WHEN pm.id   IS NULL AND am.id = :memberId THEN -1
+         WHEN am.id   IS NULL AND pm.id = :memberId THEN -1
          WHEN pm.id = :memberId THEN a.name
          ELSE p.name
        END,
      CASE
-         WHEN pm.id   IS NULL AND am.id = :memberId THEN NULL
-         WHEN am.id   IS NULL AND pm.id = :memberId THEN NULL
+         WHEN pm.id   IS NULL AND am.id = :memberId THEN -1
+         WHEN am.id   IS NULL AND pm.id = :memberId THEN -1
          WHEN pm.id = :memberId THEN am.profileUrl
          ELSE pm.profileUrl
        END
diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
index 1cf7bac..c2f0a26 100644
--- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
+++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java
@@ -279,6 +279,8 @@ public static PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershi
                 .adminId(paper.getAdmin()    != null ? paper.getAdmin().getId()     : null)
                 .partnerId(paper.getPartner()!= null ? paper.getPartner().getId()   : null) // 수동등록이면 null
                 .storeId(paper.getStore()    != null ? paper.getStore().getId()     : null)
+                .storeName(paper.getStore().getName())
+                .adminName(paper.getAdmin().getName())
 				.isActivated(paper.getIsActivated())
                 .options(optionDTOS)
                 .build();
diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
index 2483b94..2229ca2 100644
--- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java
@@ -24,6 +24,9 @@ public static class WritePartnershipResponseDTO {
         private Long adminId;
         private Long partnerId;
         private Long storeId;
+        private String storeName;
+        private String adminName;
+        private Boolean activated;
         private ActivationStatus isActivated;
         private List options;
     }

From a81c42ff7c25323e339bcb6c88071792b95c90f1 Mon Sep 17 00:00:00 2001
From: BAEK0111 
Date: Sat, 20 Sep 2025 00:43:27 +0900
Subject: [PATCH 205/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?=
 =?UTF-8?q?=EB=B0=A9=20=EC=B5=9C=EC=8B=A0=EC=88=9C=20=EC=A0=95=EB=A0=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../assu/server/domain/chat/repository/ChatRepository.java    | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java
index 30b93c1..8cdeb46 100644
--- a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java
+++ b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java
@@ -55,6 +55,10 @@ SELECT MAX(m2.createdAt)
       LEFT JOIN a.member  am
       WHERE pm.id = :memberId
       OR am.id = :memberId
+      ORDER BY
+             (SELECT MAX(m.createdAt)
+              FROM Message m
+              WHERE m.chattingRoom.id = r.id) DESC
     """)
     List findChattingRoomsByMemberId(@Param("memberId") Long memberId);
 

From 3cb25049a845b86fa28c8c801d817b6f037bd7dd Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Sun, 21 Sep 2025 20:12:58 +0900
Subject: [PATCH 206/270] =?UTF-8?q?[FIX/#109]=20=EC=8A=A4=ED=85=9C?=
 =?UTF-8?q?=ED=94=84=20=EC=A1=B0=ED=9A=8C=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?=
 =?UTF-8?q?=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/PartnershipServiceImpl.java       | 26 ++++++++++++-------
 1 file changed, 17 insertions(+), 9 deletions(-)

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 f70a5f8..3aa18f7 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
@@ -57,8 +57,13 @@ public class PartnershipServiceImpl implements PartnershipService {
     private final NotificationCommandService notificationService;
 
 
+    @Override
+    @Transactional
 	public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){
 
+        Student requestStudent = studentRepository.findById(member.getId()).orElseThrow(
+            () -> new GeneralException(ErrorStatus.NO_SUCH_STUDENT) // 혹은 적절한 예외 처리
+        );
 
 		List usages = new ArrayList<>();
 
@@ -67,8 +72,9 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe
         );
         Long paperId = content.getPaper().getId();
 		// 1) 요청한 member 본인
-		usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile(), paperId));
-        member.getStudentProfile().setStamp();
+		usages.add(PartnershipConverter.toPartnershipUsage(dto, requestStudent, paperId));
+        requestStudent.setStamp();
+        System.out.println("update 된 stamp : "+requestStudent.getStamp());
 
 		List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList());
 		// 2) dto의 userIds에 있는 다른 사용자들
@@ -85,13 +91,15 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe
             () -> new GeneralException(ErrorStatus.NO_SUCH_STORE)
         );
         Partner partner = store.getPartner();
-        if (partner != null) {
-            Long partnerId = partner.getId();
-            notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent());
-            partnershipUsageRepository.saveAll(usages);
-        } else {
-            throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER);
-        }
+        System.out.println("✨partnerId✨ = "+partner.getId());
+        // if (partner != null) {
+        //     Long partnerId = partner.getId();
+        //     System.out.println("알림 요청이 들어갑니다.");
+        //     notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent());
+        //     partnershipUsageRepository.saveAll(usages);
+        // } else {
+        //     throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER);
+        // }
 	}
 
 

From 5a5623fa14f4e7826c7b15840ffe39ed1c5e19a9 Mon Sep 17 00:00:00 2001
From: eeeeeaaan 
Date: Mon, 22 Sep 2025 17:18:05 +0900
Subject: [PATCH 207/270] =?UTF-8?q?[FIX/#109]=20=EC=8B=A4=EC=8B=9C?=
 =?UTF-8?q?=EA=B0=84=20=EC=9D=B8=EC=A6=9D=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?=
 =?UTF-8?q?=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../CertificationSessionManager.java          | 104 +++++-
 .../config/CertifyWebSocketConfig.java        |  48 +--
 .../config/StompAuthChannelInterceptor.java   |  57 ++-
 .../GroupCertificationController.java         |  44 ++-
 .../dto/CertificationProgressResponseDTO.java |   9 -
 .../service/CertificationServiceImpl.java     |   8 +-
 .../domain/chat/config/WebSocketConfig.java   |  80 +++-
 src/main/resources/certify-test.html          | 349 ++++++++++++++++--
 8 files changed, 571 insertions(+), 128 deletions(-)

diff --git a/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java
index 2b36a58..9110833 100644
--- a/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java
+++ b/src/main/java/com/assu/server/domain/certification/component/CertificationSessionManager.java
@@ -4,33 +4,119 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
-import org.springframework.stereotype.Component;
 
+import org.springframework.stereotype.Component;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+//
+// @Slf4j // ⭐️ SLF4j 로그 사용을 위해 추가
+// @Component
+// public class CertificationSessionManager {
+// 	private final Map> sessionUserMap = new ConcurrentHashMap<>();
+//
+// 	public void openSession(Long sessionId) {
+// 		sessionUserMap.put(sessionId, ConcurrentHashMap.newKeySet());
+// 		// ⭐️ 로그 추가
+// 		log.info("✅ New certification session opened. SessionID: {}", sessionId);
+// 	}
+//
+// 	public void addUserToSession(Long sessionId, Long userId) {
+// 		Set users = sessionUserMap.computeIfAbsent(sessionId, k -> {
+// 			log.warn("Attempted to add user to a non-existent session. Creating new set for SessionID: {}", k);
+// 			return ConcurrentHashMap.newKeySet();
+// 		});
+//
+// 		boolean isAdded = users.add(userId);
+//
+// 		// ⭐️ 요청하신 멤버 추가 확인 로그
+// 		if (isAdded) {
+// 			log.info("👤 User added to session. SessionID: {}, UserID: {}. Current participants: {}",
+// 				sessionId, userId, users.size());
+// 		} else {
+// 			log.info("👤 User already in session. SessionID: {}, UserID: {}. Current participants: {}",
+// 				sessionId, userId, users.size());
+// 		}
+// 	}
+//
+// 	public int getCurrentUserCount(Long sessionId) {
+// 		return sessionUserMap.getOrDefault(sessionId, Set.of()).size();
+// 	}
+//
+// 	public boolean hasUser(Long sessionId, Long userId) {
+// 		return sessionUserMap.getOrDefault(sessionId, Set.of()).contains(userId);
+// 	}
+//
+// 	public List snapshotUserIds(Long sessionId) {
+// 		return List.copyOf(sessionUserMap.getOrDefault(sessionId, Set.of()));
+// 	}
+//
+//
+//
+// 	public void removeSession(Long sessionId) {
+// 		sessionUserMap.remove(sessionId);
+// 		// ⭐️ 로그 추가
+// 		log.info("❌ Certification session removed. SessionID: {}", sessionId);
+// 	}
+// }
 @Component
+@RequiredArgsConstructor
 public class CertificationSessionManager {
-	private final Map> sessionUserMap = new ConcurrentHashMap<>();
+
+	// RedisTemplate을 주입받습니다.
+	private final StringRedisTemplate redisTemplate;
+
+	// 세션 ID를 위한 KEY를 만드는 헬퍼 메서드
+	private String getKey(Long sessionId) {
+		return "certification:session:" + sessionId;
+	}
 
 	public void openSession(Long sessionId) {
-		sessionUserMap.put(sessionId, ConcurrentHashMap.newKeySet());
+		String key = getKey(sessionId);
+		// 세션을 연다는 것은 키를 만드는 것과 같습니다.
+		// addUserToSession에서 자동으로 키가 생성되므로 이 메서드는 비워두거나,
+		// 만료 시간 설정 등 초기화 로직을 넣을 수 있습니다.
+		// 예: 10분 후 만료
+		redisTemplate.expire(key, 10, TimeUnit.MINUTES);
 	}
 
 	public void addUserToSession(Long sessionId, Long userId) {
-		sessionUserMap.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(userId);
+		String key = getKey(sessionId);
+		// Redis의 Set 자료구조에 userId를 추가합니다.
+		redisTemplate.opsForSet().add(key, String.valueOf(userId));
 	}
 
 	public int getCurrentUserCount(Long sessionId) {
-		return sessionUserMap.getOrDefault(sessionId, Set.of()).size();
+		String key = getKey(sessionId);
+		// Redis Set의 크기를 반환합니다.
+		Long size = redisTemplate.opsForSet().size(key);
+		return size != null ? size.intValue() : 0;
 	}
 
 	public boolean hasUser(Long sessionId, Long userId) {
-		return sessionUserMap.getOrDefault(sessionId, Set.of()).contains(userId);
+		String key = getKey(sessionId);
+		// Redis Set에 해당 멤버가 있는지 확인합니다.
+		return redisTemplate.opsForSet().isMember(key, String.valueOf(userId));
 	}
+
 	public List snapshotUserIds(Long sessionId) {
-		return List.copyOf(sessionUserMap.getOrDefault(sessionId, Set.of()));
+		String key = getKey(sessionId);
+		// Redis Set의 모든 멤버를 가져옵니다.
+		Set members = redisTemplate.opsForSet().members(key);
+		if (members == null) {
+			return List.of();
+		}
+		return members.stream()
+			.map(Long::valueOf)
+			.collect(Collectors.toList());
 	}
 
 	public void removeSession(Long sessionId) {
-		sessionUserMap.remove(sessionId);
+		String key = getKey(sessionId);
+		// 세션 키 자체를 삭제합니다.
+		redisTemplate.delete(key);
 	}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
index 85d7fc2..56da642 100644
--- a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java
@@ -11,27 +11,27 @@
 
 import lombok.RequiredArgsConstructor;
 
-@EnableWebSocketMessageBroker
-@Configuration
-@RequiredArgsConstructor
-public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer {
-
-	private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
-	@Override
-	public void configureMessageBroker(MessageBrokerRegistry config) {
-		config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
-		config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
-	}
-
-	@Override
-	public void registerStompEndpoints(StompEndpointRegistry registry) {
-		registry.addEndpoint("/ws").setAllowedOriginPatterns("*");          // 클라이언트 WebSocket 연결 주소
-			// .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
-	}
-
-	@Override
-	public void configureClientInboundChannel(ChannelRegistration registration) {
-		registration.interceptors(stompAuthChannelInterceptor);
-	}
-
-}
+// @EnableWebSocketMessageBroker
+// @Configuration
+// @RequiredArgsConstructor
+// public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer {
+//
+// 	private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
+// 	@Override
+// 	public void configureMessageBroker(MessageBrokerRegistry config) {
+// 		config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
+// 		config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
+// 	}
+//
+// 	@Override
+// 	public void registerStompEndpoints(StompEndpointRegistry registry) {
+// 		registry.addEndpoint("/ws-certify").setAllowedOriginPatterns("*");          // 클라이언트 WebSocket 연결 주소
+// 			// .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
+// 	}
+//
+// 	@Override
+// 	public void configureClientInboundChannel(ChannelRegistration registration) {
+// 		registration.interceptors(stompAuthChannelInterceptor);
+// 	}
+//
+// }
diff --git a/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
index b5da450..20e3367 100644
--- a/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
+++ b/src/main/java/com/assu/server/domain/certification/config/StompAuthChannelInterceptor.java
@@ -15,38 +15,61 @@
 public class StompAuthChannelInterceptor implements ChannelInterceptor {
 
 	private final JwtUtil jwtUtil;
-
 	@Override
 	public Message preSend(Message message, MessageChannel channel) {
 		StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
-		log.info("StompCommand: {}", accessor.getCommand()); // StompCommand 로그 추가
 
 		if (StompCommand.CONNECT.equals(accessor.getCommand())) {
-			log.info("CONNECT command received.");
-			// 프론트에서 connect 시 Authorization 헤더 넣어야 함
 			String authHeader = accessor.getFirstNativeHeader("Authorization");
-			log.info("Authorization Header: {}", authHeader); // Authorization 헤더 로그 추가
 
 			if (authHeader != null && authHeader.startsWith("Bearer ")) {
 				String token = jwtUtil.getTokenFromHeader(authHeader);
-				log.info("Extracted Token: {}", token); // 추출된 토큰 로그 추가
-
-				// JwtUtil 이용해서 Authentication 복원
 				Authentication authentication = jwtUtil.getAuthentication(token);
-				log.info("Authentication restored: {}", authentication); // 복원된 인증 정보 로그 추가
 
-				// WebSocket 세션에 Authentication(UserPrincipal) 저장
+				// ⭐️ 이 부분을 수정
 				accessor.setUser(authentication);
-				log.info("User principal set on accessor.");
-			} else {
-				log.warn("Authorization header is missing or not in Bearer format.");
+
+				// ⭐️ 추가: 메시지 헤더에도 Authentication 정보 저장
+				accessor.setHeader(StompHeaderAccessor.USER_HEADER, authentication);
+
+				log.info("Authentication set: {}", authentication);
 			}
-		} else if (StompCommand.SEND.equals(accessor.getCommand())) {
-			// SEND 명령어에 대한 로그 추가 (메시지 전송 시)
-			Object payload = message.getPayload();
-			log.info("SEND command received. Destination: {}, Payload: {}", accessor.getDestination(), payload);
 		}
 
 		return message;
 	}
+
+	// @Override
+	// public Message preSend(Message message, MessageChannel channel) {
+	// 	StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
+	// 	log.info("StompCommand: {}", accessor.getCommand()); // StompCommand 로그 추가
+	//
+	// 	if (StompCommand.CONNECT.equals(accessor.getCommand())) {
+	// 		log.info("CONNECT command received.");
+	// 		// 프론트에서 connect 시 Authorization 헤더 넣어야 함
+	// 		String authHeader = accessor.getFirstNativeHeader("Authorization");
+	// 		log.info("Authorization Header: {}", authHeader); // Authorization 헤더 로그 추가
+	//
+	// 		if (authHeader != null && authHeader.startsWith("Bearer ")) {
+	// 			String token = jwtUtil.getTokenFromHeader(authHeader);
+	// 			log.info("Extracted Token: {}", token); // 추출된 토큰 로그 추가
+	//
+	// 			// JwtUtil 이용해서 Authentication 복원
+	// 			Authentication authentication = jwtUtil.getAuthentication(token);
+	// 			log.info("Authentication restored: {}", authentication); // 복원된 인증 정보 로그 추가
+	//
+	// 			// WebSocket 세션에 Authentication(UserPrincipal) 저장
+	// 			accessor.setUser(authentication);
+	// 			log.info("User principal set on accessor.");
+	// 		} else {
+	// 			log.warn("Authorization header is missing or not in Bearer format.");
+	// 		}
+	// 	} else if (StompCommand.SEND.equals(accessor.getCommand())) {
+	// 		// SEND 명령어에 대한 로그 추가 (메시지 전송 시)
+	// 		Object payload = message.getPayload();
+	// 		log.info("SEND command received. Destination: {}, Payload: {}", accessor.getDestination(), payload);
+	// 	}
+	//
+	// 	return message;
+	// }
 }
\ No newline at end of file
diff --git a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java
index ee919c5..3114836 100644
--- a/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java
+++ b/src/main/java/com/assu/server/domain/certification/controller/GroupCertificationController.java
@@ -1,10 +1,16 @@
 package com.assu.server.domain.certification.controller;
 
+import java.security.Principal;
+
 import org.springframework.messaging.handler.annotation.MessageMapping;
 import org.springframework.messaging.handler.annotation.Payload;
 import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
 
 import com.assu.server.domain.certification.dto.GroupSessionRequest;
 import com.assu.server.domain.certification.service.CertificationService;
@@ -17,32 +23,32 @@
 @Slf4j
 @Controller // STOMP 메시지 처리를 위한 컨트롤러
 @RequiredArgsConstructor
+@Component
+@RequestMapping("/app")
 public class GroupCertificationController {
 
 	private final CertificationService certificationService;
 
 	@MessageMapping("/certify")
-	public void certifyGroup(@Payload GroupSessionRequest dto, SimpMessageHeaderAccessor headerAccessor) {
-		try {
-			log.info("### SUCCESS ### 인증 요청 메시지 수신 - adminId: {}, sessionId: {}", dto.getAdminId(), dto.getSessionId());
-
-			// Authentication에서 Member 정보 추출
-			Authentication auth = (Authentication) headerAccessor.getUser();
-			if (auth != null && auth.getPrincipal() instanceof PrincipalDetails) {
-				PrincipalDetails principalDetails = (PrincipalDetails) auth.getPrincipal();
-				// 실제 비즈니스 로직 호출
-				certificationService.handleCertification(dto, principalDetails.getMember());
-				log.info("### SUCCESS ### 그룹 인증 처리 완료");
+	public void certifyGroup(@Payload GroupSessionRequest dto,
+		Principal principal) {
+		if (principal instanceof UsernamePasswordAuthenticationToken) {
+			UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken)principal;
+			PrincipalDetails principalDetails = (PrincipalDetails)auth.getPrincipal();
+
+			try {
+				log.info("### SUCCESS ### 인증 요청 메시지 수신 - user: {}, adminId: {}, sessionId: {}",
+					principalDetails.getUsername(), dto.getAdminId(), dto.getSessionId());
+
+				// 헤더를 직접 다룰 필요 없이, 바로 principalDetails 객체를 사용
+				if (principalDetails != null) {
+					certificationService.handleCertification(dto, principalDetails.getMember());
+					log.info("### SUCCESS ### 그룹 인증 처리 완료");
+				}
+			} catch (Exception e) {
+				log.error("### ERROR ### 인증 처리 실패", e);
 			}
-		} catch (Exception e) {
-			log.error("### ERROR ### 인증 처리 실패", e);
 		}
 	}
 
-	// @MessageMapping("/certify")
-	// public void certifyGroup(SimpMessageHeaderAccessor headerAccessor) {
-	// 	log.info("### DEBUG ### 메서드 진입!");
-	// 	log.info("### DEBUG ### User: {}", headerAccessor.getUser());
-	// 	log.info("### DEBUG ### SessionId: {}", headerAccessor.getSessionId());
-	// }
 }
diff --git a/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java b/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java
index 4221fe9..510a1b8 100644
--- a/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java
+++ b/src/main/java/com/assu/server/domain/certification/dto/CertificationProgressResponseDTO.java
@@ -37,13 +37,4 @@ public class CertificationProgressResponseDTO {
 	private Integer count;
 	private String message;
 	private List userIds;
-
-	// 생성자들
-	public static CertificationProgressResponseDTO progress(int count) {
-		return new CertificationProgressResponseDTO("progress", count, null, null);
-	}
-
-	public static CertificationProgressResponseDTO completed(String message, List userIds) {
-		return new CertificationProgressResponseDTO("completed", userIds.size(), message, userIds);
-	}
 }
\ No newline at end of file
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 01946fc..79f6526 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
@@ -110,7 +110,8 @@ public void handleCertification(GroupSessionRequest dto, Member member) {
 		boolean isDoubledUser= sessionManager.hasUser(sessionId, userId);
 		if(isDoubledUser) {
 			messagingTemplate.convertAndSend("/certification/progress/"+sessionId,
-				new CertificationProgressResponseDTO("progress", 0,"doubled member", null));
+				new CertificationProgressResponseDTO("progress", null,
+					"doubled member", sessionManager.snapshotUserIds(sessionId)));
 			throw new GeneralException(ErrorStatus.DOUBLE_CERTIFIED_USER);
 		}
 
@@ -127,11 +128,8 @@ public void handleCertification(GroupSessionRequest dto, Member member) {
 				new CertificationProgressResponseDTO("completed", currentCertifiedNumber, "인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId)));
 		} else {
 			messagingTemplate.convertAndSend("/certification/progress/" + sessionId,
-				new CertificationProgressResponseDTO("progress", currentCertifiedNumber, null, null));
+				new CertificationProgressResponseDTO("progress", currentCertifiedNumber, null, sessionManager.snapshotUserIds(sessionId)));
 		}
-
-
-
 	}
 
 	@Override
diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
index ff1ba85..1e2c06a 100644
--- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
+++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java
@@ -1,16 +1,62 @@
+// package com.assu.server.domain.chat.config;
+//
+// import org.springframework.context.annotation.Configuration;
+// import org.springframework.messaging.simp.config.ChannelRegistration;
+// import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+// import org.springframework.web.socket.config.annotation.*;
+//
+// @Configuration
+// @EnableWebSocketMessageBroker
+// public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+//
+//     @Override
+//     public void registerStompEndpoints(StompEndpointRegistry registry) {
+//         registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
+//                 .setAllowedOriginPatterns(
+//                         "*",
+//                         "https://assu.shop",
+//                         "http://localhost:63342",
+//                         "http://localhost:5173",     // Vite 기본
+//                         "http://localhost:3000",     // CRA/Next 기본
+//                         "http://127.0.0.1:*",
+//                         "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용// fallback for old browsers
+//         // 같은 LAN의 실제 기기 테스트용
+//                       // fallback for old browsers
+//
+//         // ✅ 모바일/안드로이드용 (네이티브 WebSocket)
+//         registry.addEndpoint("/ws")
+//                 .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅
+//     }
+//
+//     @Override
+//     public void configureMessageBroker(MessageBrokerRegistry registry) {
+//         registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
+//         registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
+//     }
+// }
 package com.assu.server.domain.chat.config;
 
 import org.springframework.context.annotation.Configuration;
 import org.springframework.messaging.simp.config.ChannelRegistration;
 import org.springframework.messaging.simp.config.MessageBrokerRegistry;
-import org.springframework.web.socket.config.annotation.*;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+import com.assu.server.domain.certification.config.StompAuthChannelInterceptor;
+
+import lombok.RequiredArgsConstructor;
 
 @Configuration
 @EnableWebSocketMessageBroker
+@RequiredArgsConstructor
 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
+    private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
+
     @Override
     public void registerStompEndpoints(StompEndpointRegistry registry) {
+
         registry.addEndpoint("/ws")  // 클라이언트 WebSocket 연결 지점
                 .setAllowedOriginPatterns(
                         "*",
@@ -19,18 +65,32 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
                         "http://localhost:5173",     // Vite 기본
                         "http://localhost:3000",     // CRA/Next 기본
                         "http://127.0.0.1:*",
-                        "http://192.168.*.*:*");       // 같은 LAN의 실제 기기 테스트용// fallback for old browsers
-        // 같은 LAN의 실제 기기 테스트용
-                      // fallback for old browsers
+                        "http://192.168.*.*:*");
+        // 채팅용 엔드포인트
+
+        // 인증용 엔드포인트
+        registry.addEndpoint("/ws-certify")
+            .setAllowedOriginPatterns("*");
+
+        registry.addEndpoint("/ws-certify/sock")
+            .setAllowedOriginPatterns("*")
+            .withSockJS(); // ⬅️ SockJS를 반드시 포함해야 합니다.
 
-        // ✅ 모바일/안드로이드용 (네이티브 WebSocket)
-        registry.addEndpoint("/ws")
-                .setAllowedOriginPatterns("*"); // wss 사용 시 TLS 세팅
     }
 
     @Override
     public void configureMessageBroker(MessageBrokerRegistry registry) {
-        registry.setApplicationDestinationPrefixes("/pub"); // 클라이언트가 보내는 prefix
-        registry.enableSimpleBroker("/sub"); // 서버가 보내는 prefix
+        // 채팅용
+        registry.setApplicationDestinationPrefixes("/pub");
+        registry.enableSimpleBroker("/sub");
+
+        // 인증용 추가
+        registry.setApplicationDestinationPrefixes("/pub", "/app"); // 둘 다 추가
+        registry.enableSimpleBroker("/sub", "/certification"); // 둘 다 추가
+    }
+
+    @Override
+    public void configureClientInboundChannel(ChannelRegistration registration) {
+        registration.interceptors(stompAuthChannelInterceptor);
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/resources/certify-test.html b/src/main/resources/certify-test.html
index fa33109..90116f6 100644
--- a/src/main/resources/certify-test.html
+++ b/src/main/resources/certify-test.html
@@ -1,56 +1,335 @@
 
 
 
-    
-    Certify Test
-    
-    
+    
+    
+    WebSocket 인증 테스트 - 인증자
+    
+    
+    
 
 
-

WebSocket Certify Test

+
+

🔐 WebSocket 인증 테스트 - 인증자

-
-
-
- - -
+
+

📋 서버 설정

+
+ + +
-

+        
+ + +
+
- \ No newline at end of file From 29ef96f587657742fa05d4d5a5d965d42422bbe8 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Mon, 22 Sep 2025 18:40:31 +0900 Subject: [PATCH 208/270] =?UTF-8?q?[FIX/#109]=20url=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B0=94=EA=BE=B8=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/assu/server/domain/chat/config/WebSocketConfig.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java index 1e2c06a..8026ce3 100644 --- a/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java +++ b/src/main/java/com/assu/server/domain/chat/config/WebSocketConfig.java @@ -72,9 +72,6 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws-certify") .setAllowedOriginPatterns("*"); - registry.addEndpoint("/ws-certify/sock") - .setAllowedOriginPatterns("*") - .withSockJS(); // ⬅️ SockJS를 반드시 포함해야 합니다. } From a9d0169ced05fd2c479fe2c6fd70b4235a2d8191 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Mon, 22 Sep 2025 22:44:10 +0900 Subject: [PATCH 209/270] =?UTF-8?q?[fix/#143]=20usage=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CertificationServiceImpl.java | 2 +- .../service/PartnershipServiceImpl.java | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 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 79f6526..65f9244 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 @@ -71,7 +71,7 @@ public CertificationResponseDTO.getSessionIdResponse getSessionId( sessionManager.openSession(sessionId); // 세션 생성 직후 만료 시간을 5분으로 설정 - timeoutManager.scheduleTimeout(sessionId, Duration.ofMinutes(100));// TODO: 나중에 5분으로 변경 + timeoutManager.scheduleTimeout(sessionId, Duration.ofMinutes(10));// TODO: 나중에 5분으로 변경 // 세션 여는 대표자는 제일 먼저 인증 sessionManager.addUserToSession(sessionId, userId); 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 a7c936a..f4e0a21 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 @@ -91,15 +91,14 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) ); Partner partner = store.getPartner(); - System.out.println("✨partnerId✨ = "+partner.getId()); - // if (partner != null) { - // Long partnerId = partner.getId(); - // System.out.println("알림 요청이 들어갑니다."); - // notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); - // partnershipUsageRepository.saveAll(usages); - // } else { - // throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); - // } + if (partner != null) { + Long partnerId = partner.getId(); + System.out.println("알림 요청이 들어갑니다."); + notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); + partnershipUsageRepository.saveAll(usages); + } else { + throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); + } } From acd7ba5de4025bcba31ddcfbb80917130b79a3c1 Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Tue, 23 Sep 2025 01:40:18 +0900 Subject: [PATCH 210/270] =?UTF-8?q?[MOD/#24]=20=20-=20update=20=ED=95=A0?= =?UTF-8?q?=EB=95=8C=EB=A7=88=EB=8B=A4=20SUSPEND=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=20-=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BB=A8=EB=B2=84=ED=84=B0=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../converter/PartnershipConverter.java | 16 +- .../service/PartnershipServiceImpl.java | 782 +++++++++--------- 2 files changed, 392 insertions(+), 406 deletions(-) diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java index c2f0a26..7823df4 100644 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -38,21 +38,6 @@ public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalReq .partnershipContent(dto.getPartnershipContent()) .build(); } - public static Paper toPaperEntity( - PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO, - Admin admin, - Partner partner, - Store store - ) { - return Paper.builder() - .partnershipPeriodStart(partnershipRequestDTO.getPartnershipPeriodStart()) - .partnershipPeriodEnd(partnershipRequestDTO.getPartnershipPeriodEnd()) - .isActivated(ActivationStatus.SUSPEND) - .admin(admin) - .store(store) - .partner(partner) - .build(); - } public static Paper toDraftPaperEntity(Admin admin, Partner partner, Store store) { return Paper.builder() @@ -305,5 +290,6 @@ public static PartnershipResponseDTO.CreateDraftResponseDTO toCreateDraftRespons public static void updatePaperFromDto(Paper paper, PartnershipRequestDTO.WritePartnershipRequestDTO dto) { paper.setPartnershipPeriodStart(dto.getPartnershipPeriodStart()); paper.setPartnershipPeriodEnd(dto.getPartnershipPeriodEnd()); + paper.setIsActivated(ActivationStatus.SUSPEND); } } 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 b25201b..10cb3f0 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 @@ -1,474 +1,474 @@ -package com.assu.server.domain.partnership.service; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import org.springframework.stereotype.Service; - -import com.assu.server.domain.member.entity.Member; -import com.assu.server.domain.notification.repository.NotificationRepository; -import com.assu.server.domain.notification.service.NotificationCommandService; -import com.assu.server.domain.partnership.converter.PartnershipConverter; -import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; -import com.assu.server.domain.user.entity.PartnershipUsage; -import com.assu.server.domain.user.entity.Student; -import com.assu.server.domain.user.repository.PartnershipUsageRepository; -import com.assu.server.domain.user.repository.StudentRepository; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import com.assu.server.domain.admin.entity.Admin; -import com.assu.server.domain.admin.repository.AdminRepository; -import com.assu.server.domain.common.enums.ActivationStatus; -import com.assu.server.domain.partner.entity.Partner; -import com.assu.server.domain.partner.repository.PartnerRepository; -import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; -import com.assu.server.domain.partnership.entity.Goods; -import com.assu.server.domain.partnership.entity.Paper; -import com.assu.server.domain.partnership.entity.PaperContent; -import com.assu.server.domain.partnership.repository.GoodsRepository; -import com.assu.server.domain.partnership.repository.PaperContentRepository; -import com.assu.server.domain.partnership.repository.PaperRepository; -import com.assu.server.domain.store.entity.Store; -import com.assu.server.domain.store.repository.StoreRepository; -import com.assu.server.global.apiPayload.code.status.ErrorStatus; -import com.assu.server.global.exception.DatabaseException; -import com.assu.server.global.exception.GeneralException; -import com.assu.server.infra.s3.AmazonS3Manager; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.web.multipart.MultipartFile; -import java.time.LocalDateTime; -import java.util.*; -import java.util.stream.Collectors; - - - -@Service -@Transactional -@RequiredArgsConstructor -public class PartnershipServiceImpl implements PartnershipService { - - private final PartnershipUsageRepository partnershipUsageRepository; - private final StudentRepository studentRepository; - private final PaperContentRepository contentRepository; - private final NotificationCommandService notificationService; - - - public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ - - - List usages = new ArrayList<>(); - - PaperContent content = contentRepository.findById(dto.getContentId()).orElseThrow( - () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) - ); - Long paperId = content.getPaper().getId(); - // 1) 요청한 member 본인 - usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile(), paperId)); - member.getStudentProfile().setStamp(); - - List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList()); - // 2) dto의 userIds에 있는 다른 사용자들 - for (Long userId : userIds) { - if(userId != member.getId()){ - Student student = studentRepository.getReferenceById(userId); - usages.add(PartnershipConverter.toPartnershipUsage(dto, student, paperId)); - student.setStamp(); - } + package com.assu.server.domain.partnership.service; + + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + import java.util.Optional; + + import org.springframework.stereotype.Service; + + import com.assu.server.domain.member.entity.Member; + import com.assu.server.domain.notification.repository.NotificationRepository; + import com.assu.server.domain.notification.service.NotificationCommandService; + import com.assu.server.domain.partnership.converter.PartnershipConverter; + import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; + import com.assu.server.domain.user.entity.PartnershipUsage; + import com.assu.server.domain.user.entity.Student; + import com.assu.server.domain.user.repository.PartnershipUsageRepository; + import com.assu.server.domain.user.repository.StudentRepository; + + import jakarta.transaction.Transactional; + import lombok.RequiredArgsConstructor; + import com.assu.server.domain.admin.entity.Admin; + import com.assu.server.domain.admin.repository.AdminRepository; + import com.assu.server.domain.common.enums.ActivationStatus; + import com.assu.server.domain.partner.entity.Partner; + import com.assu.server.domain.partner.repository.PartnerRepository; + import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; + import com.assu.server.domain.partnership.entity.Goods; + import com.assu.server.domain.partnership.entity.Paper; + import com.assu.server.domain.partnership.entity.PaperContent; + import com.assu.server.domain.partnership.repository.GoodsRepository; + import com.assu.server.domain.partnership.repository.PaperContentRepository; + import com.assu.server.domain.partnership.repository.PaperRepository; + import com.assu.server.domain.store.entity.Store; + import com.assu.server.domain.store.repository.StoreRepository; + import com.assu.server.global.apiPayload.code.status.ErrorStatus; + import com.assu.server.global.exception.DatabaseException; + import com.assu.server.global.exception.GeneralException; + import com.assu.server.infra.s3.AmazonS3Manager; + import org.springframework.data.domain.PageRequest; + import org.springframework.data.domain.Sort; + import org.springframework.web.multipart.MultipartFile; + import java.time.LocalDateTime; + import java.util.*; + import java.util.stream.Collectors; + + + + @Service + @Transactional + @RequiredArgsConstructor + public class PartnershipServiceImpl implements PartnershipService { - } - - Store store = storeRepository.findById(dto.getStoreId()).orElseThrow( - () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) - ); - Partner partner = store.getPartner(); - if (partner != null) { - Long partnerId = partner.getId(); - notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); - partnershipUsageRepository.saveAll(usages); - } else { - throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); - } - } + private final PartnershipUsageRepository partnershipUsageRepository; + private final StudentRepository studentRepository; + private final PaperContentRepository contentRepository; + private final NotificationCommandService notificationService; + public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ - private final PaperRepository paperRepository; - private final PaperContentRepository paperContentRepository; - private final GoodsRepository goodsRepository; - private final AdminRepository adminRepository; - private final PartnerRepository partnerRepository; - private final StoreRepository storeRepository; + List usages = new ArrayList<>(); - private final AmazonS3Manager amazonS3Manager; + PaperContent content = contentRepository.findById(dto.getContentId()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) + ); + Long paperId = content.getPaper().getId(); + // 1) 요청한 member 본인 + usages.add(PartnershipConverter.toPartnershipUsage(dto, member.getStudentProfile(), paperId)); + member.getStudentProfile().setStamp(); - @Override - @Transactional - public PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership( - PartnershipRequestDTO.WritePartnershipRequestDTO request, - Long memberId - ) { - if (request == null || memberId == null) { - throw new DatabaseException(ErrorStatus._BAD_REQUEST); - } + List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList()); + // 2) dto의 userIds에 있는 다른 사용자들 + for (Long userId : userIds) { + if(userId != member.getId()){ + Student student = studentRepository.getReferenceById(userId); + usages.add(PartnershipConverter.toPartnershipUsage(dto, student, paperId)); + student.setStamp(); + } - Paper paper = paperRepository.findById(request.getPaperId()) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); + } - Partner partner = partnerRepository.findById(memberId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + Store store = storeRepository.findById(dto.getStoreId()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) + ); + Partner partner = store.getPartner(); + if (partner != null) { + Long partnerId = partner.getId(); + notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); + partnershipUsageRepository.saveAll(usages); + } else { + throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); + } + } - Admin admin = adminRepository.findById(paper.getAdmin().getId()) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - Store store = storeRepository.findByPartner(partner) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - PartnershipConverter.updatePaperFromDto(paper, request); + private final PaperRepository paperRepository; + private final PaperContentRepository paperContentRepository; + private final GoodsRepository goodsRepository; - List existingContents = paperContentRepository.findByPaperId(request.getPaperId()); - if (!existingContents.isEmpty()) { - List contentIds = existingContents.stream().map(PaperContent::getId).toList(); - goodsRepository.deleteAllByContentIds(contentIds); - paperContentRepository.deleteAll(existingContents); - } + private final AdminRepository adminRepository; + private final PartnerRepository partnerRepository; + private final StoreRepository storeRepository; - List newContents = PartnershipConverter.toPaperContents(request, paper); - newContents = newContents.isEmpty() ? newContents : paperContentRepository.saveAll(newContents); + private final AmazonS3Manager amazonS3Manager; - List> requestGoodsBatches = PartnershipConverter.toGoodsBatches(request); + @Override + @Transactional + public PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership( + PartnershipRequestDTO.WritePartnershipRequestDTO request, + Long memberId + ) { + if (request == null || memberId == null) { + throw new DatabaseException(ErrorStatus._BAD_REQUEST); + } - List> attachedGoodsBatches = new ArrayList<>(); - List toPersist = new ArrayList<>(); + Paper paper = paperRepository.findById(request.getPaperId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); - for (int i = 0; i < newContents.size(); i++) { - PaperContent content = newContents.get(i); - List batch = (requestGoodsBatches.size() > i) ? requestGoodsBatches.get(i) : Collections.emptyList(); - List attached = new ArrayList<>(); - for (Goods g : batch) { - Goods entity = Goods.builder() - .content(content) - .belonging(g.getBelonging()) - .build(); - attached.add(entity); - toPersist.add(entity); - } - attachedGoodsBatches.add(attached); - } - if (!toPersist.isEmpty()) { - goodsRepository.saveAll(toPersist); - } + Partner partner = partnerRepository.findById(memberId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); - return PartnershipConverter.writePartnershipResultDTO(paper, newContents, attachedGoodsBatches); - } + Admin admin = adminRepository.findById(paper.getAdmin().getId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - @Override - public List listPartnershipsForAdmin(boolean all, Long adminId) { - Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); - List papers = all - ? paperRepository.findByAdmin_IdAndIsActivated(adminId, ActivationStatus.ACTIVE, sort) - : paperRepository.findByAdmin_IdAndIsActivated(adminId, ActivationStatus.ACTIVE, PageRequest.of(0, 2, sort)).getContent(); + Store store = storeRepository.findByPartner(partner) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - papers = papers.stream() - .filter(p -> p.getStore() != null) - .toList(); + PartnershipConverter.updatePaperFromDto(paper, request); - return buildPartnershipDTOs(papers); - } + List existingContents = paperContentRepository.findByPaperId(request.getPaperId()); + if (!existingContents.isEmpty()) { + List contentIds = existingContents.stream().map(PaperContent::getId).toList(); + goodsRepository.deleteAllByContentIds(contentIds); + paperContentRepository.deleteAll(existingContents); + } - @Override - public List listPartnershipsForPartner(boolean all, Long partnerId) { - Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); - List papers = all - ? paperRepository.findByPartner_IdAndIsActivated(partnerId, ActivationStatus.ACTIVE, sort) - : paperRepository.findByPartner_IdAndIsActivated(partnerId, ActivationStatus.ACTIVE, PageRequest.of(0, 2, sort)).getContent(); + List newContents = PartnershipConverter.toPaperContents(request, paper); + newContents = newContents.isEmpty() ? newContents : paperContentRepository.saveAll(newContents); - papers = papers.stream() - .filter(p -> p.getAdmin() != null) - .toList(); + List> requestGoodsBatches = PartnershipConverter.toGoodsBatches(request); - return buildPartnershipDTOs(papers); - } + List> attachedGoodsBatches = new ArrayList<>(); + List toPersist = new ArrayList<>(); - @Override - @Transactional - public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId) { - Paper paper = paperRepository.findById(partnershipId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); + for (int i = 0; i < newContents.size(); i++) { + PaperContent content = newContents.get(i); + List batch = (requestGoodsBatches.size() > i) ? requestGoodsBatches.get(i) : Collections.emptyList(); + List attached = new ArrayList<>(); + for (Goods g : batch) { + Goods entity = Goods.builder() + .content(content) + .belonging(g.getBelonging()) + .build(); + attached.add(entity); + toPersist.add(entity); + } + attachedGoodsBatches.add(attached); + } + if (!toPersist.isEmpty()) { + goodsRepository.saveAll(toPersist); + } - List contents = paperContentRepository.findAllByOnePaperIdInFetchGoods(partnershipId); + return PartnershipConverter.writePartnershipResultDTO(paper, newContents, attachedGoodsBatches); + } - List> goodsBatches = contents.stream() - .map(pc -> pc.getGoods() == null ? Collections.emptyList() : pc.getGoods()) - .toList(); + @Override + public List listPartnershipsForAdmin(boolean all, Long adminId) { + Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); + List papers = all + ? paperRepository.findByAdmin_IdAndIsActivated(adminId, ActivationStatus.ACTIVE, sort) + : paperRepository.findByAdmin_IdAndIsActivated(adminId, ActivationStatus.ACTIVE, PageRequest.of(0, 2, sort)).getContent(); - return PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches); - } + papers = papers.stream() + .filter(p -> p.getStore() != null) + .toList(); - @Override - @Transactional - public List getSuspendedPapers(Long adminId) { - List suspendedPapers = paperRepository.findAllByIsActivatedWithPartner(ActivationStatus.SUSPEND); - - return suspendedPapers.stream() - .map(paper -> PartnershipResponseDTO.SuspendedPaperDTO.builder() - .paperId(paper.getId()) - .partnerName(paper.getPartner().getName()) - .createdAt(paper.getCreatedAt()) - .build()) - .toList(); - } + return buildPartnershipDTOs(papers); + } - @Override - @Transactional - public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request) { - Paper paper = paperRepository.findById(partnershipId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); + @Override + public List listPartnershipsForPartner(boolean all, Long partnerId) { + Sort sort = Sort.by(Sort.Direction.DESC, "createdAt"); + List papers = all + ? paperRepository.findByPartner_IdAndIsActivated(partnerId, ActivationStatus.ACTIVE, sort) + : paperRepository.findByPartner_IdAndIsActivated(partnerId, ActivationStatus.ACTIVE, PageRequest.of(0, 2, sort)).getContent(); + + papers = papers.stream() + .filter(p -> p.getAdmin() != null) + .toList(); - if(request == null || request.getStatus() == null){ - throw new DatabaseException(ErrorStatus._BAD_REQUEST); + return buildPartnershipDTOs(papers); } - ActivationStatus prev = paper.getIsActivated(); - ActivationStatus next = parseStatus(request.getStatus()); + @Override + @Transactional + public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId) { + Paper paper = paperRepository.findById(partnershipId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); - paper.setIsActivated(next); + List contents = paperContentRepository.findAllByOnePaperIdInFetchGoods(partnershipId); - return PartnershipResponseDTO.UpdateResponseDTO.builder() - .partnershipId(paper.getId()) - .prevStatus(prev == null ? null : prev.name()) - .newStatus(next.name()) - .changedAt(LocalDateTime.now()) - .build(); - } + List> goodsBatches = contents.stream() + .map(pc -> pc.getGoods() == null ? Collections.emptyList() : pc.getGoods()) + .toList(); - @Override - @Transactional - public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership( - PartnershipRequestDTO.ManualPartnershipRequestDTO request, - Long adminId, - MultipartFile contractImage) { + return PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches); + } - if (request == null || adminId == null) - throw new DatabaseException(ErrorStatus._BAD_REQUEST); + @Override + @Transactional + public List getSuspendedPapers(Long adminId) { + List suspendedPapers = paperRepository.findAllByIsActivatedWithPartner(ActivationStatus.SUSPEND); + + return suspendedPapers.stream() + .map(paper -> PartnershipResponseDTO.SuspendedPaperDTO.builder() + .paperId(paper.getId()) + .partnerName(paper.getPartner().getName()) + .createdAt(paper.getCreatedAt()) + .build()) + .toList(); + } - Admin admin = adminRepository.findById(adminId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + @Override + @Transactional + public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request) { + Paper paper = paperRepository.findById(partnershipId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); - String address = pickDisplayAddress(request.getSelectedPlace().getRoadAddress(), request.getSelectedPlace().getAddress()); + if(request == null || request.getStatus() == null){ + throw new DatabaseException(ErrorStatus._BAD_REQUEST); + } - Store store = storeRepository - .findByNameAndAddressAndDetailAddress(request.getStoreName(), address, request.getStoreDetailAddress()) - .orElse(null); + ActivationStatus prev = paper.getIsActivated(); + ActivationStatus next = parseStatus(request.getStatus()); - boolean created = false; - boolean reactivated = false; + paper.setIsActivated(next); - if (store == null) { - store = Store.builder() - .name(request.getStoreName()) - .address(address) - .detailAddress(request.getStoreDetailAddress()) - .rate(0) - .isActivate(ActivationStatus.SUSPEND) + return PartnershipResponseDTO.UpdateResponseDTO.builder() + .partnershipId(paper.getId()) + .prevStatus(prev == null ? null : prev.name()) + .newStatus(next.name()) + .changedAt(LocalDateTime.now()) .build(); - store = storeRepository.save(store); - created = true; - } else if (store.getIsActivate() == ActivationStatus.INACTIVE) { - store.setIsActivate(ActivationStatus.SUSPEND); - reactivated = true; } - Paper paper = PartnershipConverter.toPaperForManual( - admin, store, - request.getPartnershipPeriodStart(), - request.getPartnershipPeriodEnd(), - ActivationStatus.SUSPEND - ); - paper = paperRepository.save(paper); + @Override + @Transactional + public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership( + PartnershipRequestDTO.ManualPartnershipRequestDTO request, + Long adminId, + MultipartFile contractImage) { - if (contractImage != null && !contractImage.isEmpty()) { - try { - String keyName = amazonS3Manager.generateKeyName("contract-images"); - amazonS3Manager.uploadFile(keyName, contractImage); - paper.updateContractImageKey(keyName); - paperRepository.save(paper); - } catch (Exception e) { - throw new DatabaseException(ErrorStatus.IMAGE_UPLOAD_FAILED); - } - } + if (request == null || adminId == null) + throw new DatabaseException(ErrorStatus._BAD_REQUEST); - List savedContents = new ArrayList<>(); - if (request.getOptions() != null && !request.getOptions().isEmpty()) { - List contents = PartnershipConverter.toPaperContentsForManual(request.getOptions(), paper); - savedContents = paperContentRepository.saveAll(contents); + Admin admin = adminRepository.findById(adminId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - List toPersist = new ArrayList<>(); - for (int i = 0; i < savedContents.size(); i++) { - var opt = request.getOptions().get(i); - var content = savedContents.get(i); - var batch = PartnershipConverter.toGoodsForContent(opt, content); - if (!batch.isEmpty()) toPersist.addAll(batch); + String address = pickDisplayAddress(request.getSelectedPlace().getRoadAddress(), request.getSelectedPlace().getAddress()); + + Store store = storeRepository + .findByNameAndAddressAndDetailAddress(request.getStoreName(), address, request.getStoreDetailAddress()) + .orElse(null); + + boolean created = false; + boolean reactivated = false; + + if (store == null) { + store = Store.builder() + .name(request.getStoreName()) + .address(address) + .detailAddress(request.getStoreDetailAddress()) + .rate(0) + .isActivate(ActivationStatus.SUSPEND) + .build(); + store = storeRepository.save(store); + created = true; + } else if (store.getIsActivate() == ActivationStatus.INACTIVE) { + store.setIsActivate(ActivationStatus.SUSPEND); + reactivated = true; } - if (!toPersist.isEmpty()) goodsRepository.saveAll(toPersist); - } - List contentsWithGoods = paperContentRepository.findAllByOnePaperIdInFetchGoods(paper.getId()); - List> goodsBatches = contentsWithGoods.stream() - .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods()) - .toList(); - - var partnership = PartnershipConverter.writePartnershipResultDTO(paper, contentsWithGoods, goodsBatches); - - String url = (paper.getContractImageKey() == null) - ? null - :amazonS3Manager.generatePresignedUrl(paper.getContractImageKey()); - - return PartnershipResponseDTO.ManualPartnershipResponseDTO.builder() - .storeId(store.getId()) - .storeCreated(created) - .storeActivated(reactivated) - .status(store.getIsActivate() == null ? null : store.getIsActivate().name()) - .contractImageUrl(url) - .partnership(partnership) - .build(); - } + Paper paper = PartnershipConverter.toPaperForManual( + admin, store, + request.getPartnershipPeriodStart(), + request.getPartnershipPeriodEnd(), + ActivationStatus.SUSPEND + ); + paper = paperRepository.save(paper); + + if (contractImage != null && !contractImage.isEmpty()) { + try { + String keyName = amazonS3Manager.generateKeyName("contract-images"); + amazonS3Manager.uploadFile(keyName, contractImage); + paper.updateContractImageKey(keyName); + paperRepository.save(paper); + } catch (Exception e) { + throw new DatabaseException(ErrorStatus.IMAGE_UPLOAD_FAILED); + } + } - @Override - @Transactional - public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(PartnershipRequestDTO.CreateDraftRequestDTO request, Long adminId) { - Admin admin = adminRepository.findById(adminId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - Partner partner = partnerRepository.findById(request.getPartnerId()) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); - Store store = storeRepository.findByPartner(partner) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - - Paper draftPaper = PartnershipConverter.toDraftPaperEntity(admin, partner, store); - paperRepository.save(draftPaper); - - return PartnershipConverter.toCreateDraftResponseDTO(draftPaper); - } + List savedContents = new ArrayList<>(); + if (request.getOptions() != null && !request.getOptions().isEmpty()) { + List contents = PartnershipConverter.toPaperContentsForManual(request.getOptions(), paper); + savedContents = paperContentRepository.saveAll(contents); + + List toPersist = new ArrayList<>(); + for (int i = 0; i < savedContents.size(); i++) { + var opt = request.getOptions().get(i); + var content = savedContents.get(i); + var batch = PartnershipConverter.toGoodsForContent(opt, content); + if (!batch.isEmpty()) toPersist.addAll(batch); + } + if (!toPersist.isEmpty()) goodsRepository.saveAll(toPersist); + } - @Override - @Transactional - public void deletePartnership(Long paperId) { - Paper paper = paperRepository.findById(paperId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); + List contentsWithGoods = paperContentRepository.findAllByOnePaperIdInFetchGoods(paper.getId()); + List> goodsBatches = contentsWithGoods.stream() + .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods()) + .toList(); - List contentsToDelete = paperContentRepository.findByPaperId(paperId); + var partnership = PartnershipConverter.writePartnershipResultDTO(paper, contentsWithGoods, goodsBatches); - if (contentsToDelete != null && !contentsToDelete.isEmpty()) { - List contentIds = contentsToDelete.stream().map(PaperContent::getId).toList(); - goodsRepository.deleteAllByContentIds(contentIds); + String url = (paper.getContractImageKey() == null) + ? null + :amazonS3Manager.generatePresignedUrl(paper.getContractImageKey()); - paperContentRepository.deleteAll(contentsToDelete); + return PartnershipResponseDTO.ManualPartnershipResponseDTO.builder() + .storeId(store.getId()) + .storeCreated(created) + .storeActivated(reactivated) + .status(store.getIsActivate() == null ? null : store.getIsActivate().name()) + .contractImageUrl(url) + .partnership(partnership) + .build(); } - paperRepository.delete(paper); - } + @Override + @Transactional + public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(PartnershipRequestDTO.CreateDraftRequestDTO request, Long adminId) { + Admin admin = adminRepository.findById(adminId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + Partner partner = partnerRepository.findById(request.getPartnerId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + Store store = storeRepository.findByPartner(partner) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - @Override - @Transactional - public PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId) { + Paper draftPaper = PartnershipConverter.toDraftPaperEntity(admin, partner, store); + paperRepository.save(draftPaper); - Partner partner = partnerRepository.findById(partnerId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + return PartnershipConverter.toCreateDraftResponseDTO(draftPaper); + } - List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND); - boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses); + @Override + @Transactional + public void deletePartnership(Long paperId) { + Paper paper = paperRepository.findById(paperId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); - Long paperId = null; - String status = "NONE"; + List contentsToDelete = paperContentRepository.findByPaperId(paperId); - if (isPartnered) { - Optional latestActiveOrSuspendPaper = paperRepository - .findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(adminId, partnerId, targetStatuses); + if (contentsToDelete != null && !contentsToDelete.isEmpty()) { + List contentIds = contentsToDelete.stream().map(PaperContent::getId).toList(); + goodsRepository.deleteAllByContentIds(contentIds); - if (latestActiveOrSuspendPaper.isPresent()) { - Paper paper = latestActiveOrSuspendPaper.get(); - paperId = paper.getId(); - status = paper.getIsActivated().name(); + paperContentRepository.deleteAll(contentsToDelete); } + + paperRepository.delete(paper); } - return PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO.builder() - .paperId(paperId) - .isPartnered(isPartnered) - .status(status) - .partnerId(partner.getId()) - .partnerName(partner.getName()) - .partnerAddress(partner.getAddress()) - .build(); - } + @Override + @Transactional + public PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId) { - @Override - @Transactional - public PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId) { + Partner partner = partnerRepository.findById(partnerId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); - Admin admin = adminRepository.findById(adminId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND); + boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses); - List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND, ActivationStatus.BLANK); - boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses); + Long paperId = null; + String status = "NONE"; - Long paperId = null; - String status = "NONE"; + if (isPartnered) { + Optional latestActiveOrSuspendPaper = paperRepository + .findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(adminId, partnerId, targetStatuses); - if (isPartnered) { - Optional latestActiveOrSuspendPaper = paperRepository - .findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(adminId, partnerId, targetStatuses); - - if (latestActiveOrSuspendPaper.isPresent()) { - Paper paper = latestActiveOrSuspendPaper.get(); - paperId = paper.getId(); - status = paper.getIsActivated().name(); + if (latestActiveOrSuspendPaper.isPresent()) { + Paper paper = latestActiveOrSuspendPaper.get(); + paperId = paper.getId(); + status = paper.getIsActivated().name(); + } } + + return PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO.builder() + .paperId(paperId) + .isPartnered(isPartnered) + .status(status) + .partnerId(partner.getId()) + .partnerName(partner.getName()) + .partnerAddress(partner.getAddress()) + .build(); } - return PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO.builder() - .paperId(paperId) - .isPartnered(isPartnered) - .status(status) - .adminId(admin.getId()) - .adminName(admin.getName()) - .adminAddress(admin.getOfficeAddress()) - .build(); - } + @Override + @Transactional + public PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId) { - private List buildPartnershipDTOs(List papers) { - if (papers == null || papers.isEmpty()) return List.of(); + Admin admin = adminRepository.findById(adminId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - List paperIds = papers.stream().map(Paper::getId).toList(); - List allContents = paperContentRepository.findAllByPaperIdInFetchGoods(paperIds); + List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND, ActivationStatus.BLANK); + boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses); - Map> byPaperId = allContents.stream() - .collect(Collectors.groupingBy(pc -> pc.getPaper().getId())); + Long paperId = null; + String status = "NONE"; - List result = new ArrayList<>(papers.size()); - for (Paper p : papers) { - List contents = byPaperId.getOrDefault(p.getId(), List.of()); - List> goodsBatches = contents.stream() - .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods()) - .toList(); - result.add(PartnershipConverter.writePartnershipResultDTO(p, contents, goodsBatches)); + if (isPartnered) { + Optional latestActiveOrSuspendPaper = paperRepository + .findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(adminId, partnerId, targetStatuses); + + if (latestActiveOrSuspendPaper.isPresent()) { + Paper paper = latestActiveOrSuspendPaper.get(); + paperId = paper.getId(); + status = paper.getIsActivated().name(); + } + } + + return PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO.builder() + .paperId(paperId) + .isPartnered(isPartnered) + .status(status) + .adminId(admin.getId()) + .adminName(admin.getName()) + .adminAddress(admin.getOfficeAddress()) + .build(); } - return result; - } - private ActivationStatus parseStatus(String raw) { - try { - return ActivationStatus.valueOf(raw.trim().toUpperCase()); - } catch (Exception e) { - throw new DatabaseException(ErrorStatus._BAD_REQUEST); + private List buildPartnershipDTOs(List papers) { + if (papers == null || papers.isEmpty()) return List.of(); + + List paperIds = papers.stream().map(Paper::getId).toList(); + List allContents = paperContentRepository.findAllByPaperIdInFetchGoods(paperIds); + + Map> byPaperId = allContents.stream() + .collect(Collectors.groupingBy(pc -> pc.getPaper().getId())); + + List result = new ArrayList<>(papers.size()); + for (Paper p : papers) { + List contents = byPaperId.getOrDefault(p.getId(), List.of()); + List> goodsBatches = contents.stream() + .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods()) + .toList(); + result.add(PartnershipConverter.writePartnershipResultDTO(p, contents, goodsBatches)); + } + return result; + } + + private ActivationStatus parseStatus(String raw) { + try { + return ActivationStatus.valueOf(raw.trim().toUpperCase()); + } catch (Exception e) { + throw new DatabaseException(ErrorStatus._BAD_REQUEST); + } } - } - private String pickDisplayAddress(String road, String jibun) { - return (road != null && !road.isBlank()) ? road : jibun; + private String pickDisplayAddress(String road, String jibun) { + return (road != null && !road.isBlank()) ? road : jibun; + } } -} From f9b59b6c61a899252bf39a0dfc0c4f4c516d8b4a Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Tue, 23 Sep 2025 10:17:30 +0900 Subject: [PATCH 211/270] =?UTF-8?q?[fix/#143]=20usage=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=ED=95=B4=EC=A0=9C=20=EC=8A=A4=ED=85=9C?= =?UTF-8?q?=ED=94=84=20=EC=A4=91=EB=B3=B5=EC=B2=98=EB=A6=AC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PartnershipServiceImpl.java | 102 ++++++++++++------ 1 file changed, 68 insertions(+), 34 deletions(-) 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 f4e0a21..be75e23 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 @@ -59,47 +59,81 @@ public class PartnershipServiceImpl implements PartnershipService { @Override @Transactional - public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ - - Student requestStudent = studentRepository.findById(member.getId()).orElseThrow( - () -> new GeneralException(ErrorStatus.NO_SUCH_STUDENT) // 혹은 적절한 예외 처리 - ); - - List usages = new ArrayList<>(); - + public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member) { + // 1. 제휴 내용(PaperContent) 조회 PaperContent content = contentRepository.findById(dto.getContentId()).orElseThrow( () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) ); Long paperId = content.getPaper().getId(); - // 1) 요청한 member 본인 - usages.add(PartnershipConverter.toPartnershipUsage(dto, requestStudent, paperId)); - requestStudent.setStamp(); - System.out.println("update 된 stamp : "+requestStudent.getStamp()); - - List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList()); - // 2) dto의 userIds에 있는 다른 사용자들 - for (Long userId : userIds) { - if(userId != member.getId()){ - Student student = studentRepository.getReferenceById(userId); - usages.add(PartnershipConverter.toPartnershipUsage(dto, student, paperId)); + + // 2. 중복을 허용하지 않는 Set을 사용하여 모든 사용자 ID를 수집 + Set uniqueUserIds = new HashSet<>(); + // 요청자 본인 ID 추가 + uniqueUserIds.add(member.getId()); + // DTO에 포함된 사용자 ID들 추가 (null일 경우 무시) + if (dto.getUserIds() != null) { + uniqueUserIds.addAll(dto.getUserIds()); + } + + // 3. 모든 학생 정보를 DB에서 한 번의 쿼리로 조회 (N+1 문제 해결) + List studentsToUpdate = studentRepository.findAllById(uniqueUserIds); + + // 4. 조회된 학생들에 대해 PartnershipUsage 생성 및 스탬프 업데이트 + List usages = studentsToUpdate.stream() + .map(student -> { student.setStamp(); - } + System.out.println("스탬프 업데이트 - 학생 ID: " + student.getId() + ", 현재 스탬프: " + student.getStamp()); + return PartnershipConverter.toPartnershipUsage(dto, student, paperId); + }) + .collect(Collectors.toList()); - } + // 5. 생성된 모든 Usage 기록을 한 번에 저장 + partnershipUsageRepository.saveAll(usages); - Store store = storeRepository.findById(dto.getStoreId()).orElseThrow( - () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) - ); - Partner partner = store.getPartner(); - if (partner != null) { - Long partnerId = partner.getId(); - System.out.println("알림 요청이 들어갑니다."); - notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); - partnershipUsageRepository.saveAll(usages); - } else { - throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); - } - } + // @Transactional 환경에서는 studentsToUpdate의 변경 사항(스탬프)이 자동으로 DB에 반영됩니다. + } + // public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ + // + // Student requestStudent = studentRepository.findById(member.getId()).orElseThrow( + // () -> new GeneralException(ErrorStatus.NO_SUCH_STUDENT) // 혹은 적절한 예외 처리 + // ); + // + // List usages = new ArrayList<>(); + // + // PaperContent content = contentRepository.findById(dto.getContentId()).orElseThrow( + // () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) + // ); + // Long paperId = content.getPaper().getId(); + // // 1) 요청한 member 본인 + // usages.add(PartnershipConverter.toPartnershipUsage(dto, requestStudent, paperId)); + // requestStudent.setStamp(); + // System.out.println("update 된 stamp : "+requestStudent.getStamp()); + // + // List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList()); + // // 2) dto의 userIds에 있는 다른 사용자들 + // for (Long userId : userIds) { + // if(userId != member.getId()){ + // Student student = studentRepository.getReferenceById(userId); + // usages.add(PartnershipConverter.toPartnershipUsage(dto, student, paperId)); + // student.setStamp(); + // } + // + // } + // partnershipUsageRepository.saveAll(usages); + // + // // Store store = storeRepository.findById(dto.getStoreId()).orElseThrow( + // // () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) + // // ); + // // Partner partner = store.getPartner(); + // // if (partner != null) { + // // Long partnerId = partner.getId(); + // // System.out.println("알림 요청이 들어갑니다."); + // // notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); + // // + // // } else { + // // throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); + // // } + // } From b23d69d3882b9ca786f9b0ebf62174f73b78d6d3 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Tue, 23 Sep 2025 10:27:12 +0900 Subject: [PATCH 212/270] =?UTF-8?q?[fix/#143]=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/partnership/service/PartnershipServiceImpl.java | 1 - 1 file changed, 1 deletion(-) 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 be75e23..8418f44 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 @@ -82,7 +82,6 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe List usages = studentsToUpdate.stream() .map(student -> { student.setStamp(); - System.out.println("스탬프 업데이트 - 학생 ID: " + student.getId() + ", 현재 스탬프: " + student.getStamp()); return PartnershipConverter.toPartnershipUsage(dto, student, paperId); }) .collect(Collectors.toList()); From 0e5c4ae91506ffc5ca582161b0548ea57c6c30f1 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 00:49:51 +0900 Subject: [PATCH 213/270] =?UTF-8?q?[CHORE/#149]=20AuthController=20?= =?UTF-8?q?=EB=B2=84=EC=A0=80=EB=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/domain/auth/controller/AuthController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 7351921..d046fca 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 @@ -84,7 +84,8 @@ public BaseResponse checkAuthNumber( return BaseResponse.onSuccess(SuccessStatus.VERIFY_AUTH_NUMBER_SUCCESS, null); } - @Operation(summary = "전화번호 중복 체크 API", description = "# [v1.0 (2025-01-15)]\n" + + @Operation(summary = "전화번호 중복 체크 API", + description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2551197c19ed808a9757f7f0fc4cf09b?source=copy_link)\n" + "- 입력한 전화번호가 이미 가입된 사용자가 있는지 확인합니다.\n" + "- 중복된 전화번호가 있으면 에러를 반환합니다.\n" + "\n**Request Body:**\n" + @@ -99,7 +100,8 @@ public BaseResponse checkPhoneNumberAvailability( return BaseResponse.onSuccess(SuccessStatus._OK, null); } - @Operation(summary = "이메일 중복 체크 API", description = "# [v1.0 (2025-01-15)]\n" + + @Operation(summary = "이메일 중복 체크 API", + description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2551197c19ed802d8f6dd373dd045f3a?source=copy_link)\n" + "- 입력한 이메일이 이미 가입된 사용자가 있는지 확인합니다.\n" + "- 중복된 이메일이 있으면 에러를 반환합니다.\n" + "\n**Request Body:**\n" + From 4e3bf3adbd897196c2b9072df540a89e615a878a Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 01:38:18 +0900 Subject: [PATCH 214/270] =?UTF-8?q?[CHORE/#149]=20ErrorStatus=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/partnership/service/PaperQueryServiceImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java index 48d9b91..8cef7ca 100644 --- a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java +++ b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java @@ -19,7 +19,6 @@ import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.domain.user.entity.Student; -import com.assu.server.domain.user.entity.enums.Major; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.GeneralException; @@ -41,7 +40,7 @@ public PaperResponseDTO.partnershipContent getStorePaperContent(Long storeId, Me // 역할이 학생이 아닌 경우 : 이미 type별로 ui를 분기 시켜놔서 그럴일 없을 것 같긴 하지만 혹시 몰라서 처리함 if(member.getRole() != UserRole.STUDENT) - throw new GeneralException(ErrorStatus.NO_STUENT_TYPE); + throw new GeneralException(ErrorStatus.NO_STUDENT_TYPE); Student student = member.getStudentProfile(); From 36b3ec09221e70918054ae4fad487b373f733ca3 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 01:46:05 +0900 Subject: [PATCH 215/270] =?UTF-8?q?[CHORE/#149]=20ErrorStatus=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/global/apiPayload/code/status/ErrorStatus.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index 94ecae2..c5e811c 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -43,7 +43,7 @@ public enum ErrorStatus implements BaseErrorCode { // 멤버 에러 NO_SUCH_MEMBER(HttpStatus.NOT_FOUND,"MEMBER_4001","존재하지 않는 멤버 ID입니다."), - NO_STUENT_TYPE(HttpStatus.BAD_REQUEST, "MEMBER4002", "학생 타입이 아닌 멤버입니다."), + NO_STUDENT_TYPE(HttpStatus.BAD_REQUEST, "MEMBER4002", "학생 타입이 아닌 멤버입니다."), NO_SUCH_ADMIN(HttpStatus.NOT_FOUND,"MEMBER_4002","존재하지 않는 admin ID 입니다."), NO_SUCH_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4003","존재하지 않는 partner ID 입니다."), From 814f99b6b6dd2d724478ccdcc9d3459f4bdb0158 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:23:09 +0900 Subject: [PATCH 216/270] =?UTF-8?q?[FEAT/#149]=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/report/exception/ReportException.java | 10 ++++++++++ .../global/apiPayload/code/status/ErrorStatus.java | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/assu/server/domain/report/exception/ReportException.java diff --git a/src/main/java/com/assu/server/domain/report/exception/ReportException.java b/src/main/java/com/assu/server/domain/report/exception/ReportException.java new file mode 100644 index 0000000..8df2612 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/exception/ReportException.java @@ -0,0 +1,10 @@ +package com.assu.server.domain.report.exception; + +import com.assu.server.global.apiPayload.code.BaseErrorCode; +import com.assu.server.global.exception.GeneralException; + +public class ReportException extends GeneralException { + public ReportException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index c5e811c..f21a23a 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -105,13 +105,21 @@ public enum ErrorStatus implements BaseErrorCode { PROFILE_IMAGE_INVALID_FORMAT(HttpStatus.BAD_REQUEST, "PROFILE_4002", "지원하지 않는 이미지 형식입니다."), PROFILE_IMAGE_TOO_LARGE(HttpStatus.BAD_REQUEST, "PROFILE_4003", "허용된 크기를 초과한 이미지입니다."), + // Suggestion 관련 에러 + NO_SUCH_SUGGESTION(HttpStatus.NOT_FOUND, "SUGGESTION_4001", "존재하지 않는 건의글입니다."), + + // 신고(Report) 관련 에러 + REPORT_DUPLICATE(HttpStatus.CONFLICT, "REPORT_4001", "이미 신고한 대상입니다."), + REPORT_SELF_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REPORT_4002", "자신을 신고할 수 없습니다."), + REVIEW_REPORT_SELF_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REPORT_4003", "자신의 리뷰를 신고할 수 없습니다."), + SUGGESTION_REPORT_SELF_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REPORT_4004", "자신의 건의글을 신고할 수 없습니다."), + INVALID_REPORT_TYPE(HttpStatus.BAD_REQUEST, "REPORT_4005", "유효하지 않은 신고 타입입니다."), ; private final HttpStatus httpStatus; private final String code; private final String message; - @Override public ErrorReasonDTO getReasonHttpStatus() { return ErrorReasonDTO.builder() From a725d8a7c8360fb63aad9450948e9bfc623e9944 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:23:21 +0900 Subject: [PATCH 217/270] =?UTF-8?q?[FEAT/#149]=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=8B=A0=EA=B3=A0=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/repository/ReviewRepository.java | 52 ++++++++++++++++--- .../review/service/ReviewServiceImpl.java | 32 +++++++++--- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java index 8648241..97ea6fb 100644 --- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java @@ -2,6 +2,7 @@ import com.assu.server.domain.review.entity.Review; import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.common.entity.enums.ReportedStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -13,17 +14,54 @@ public interface ReviewRepository extends JpaRepository { @Query(""" - SELECT r - FROM Review r - WHERE r.student.id = :memberId - ORDER BY r.createdAt DESC -""") + SELECT r + FROM Review r + WHERE r.student.id = :memberId + AND r.status = :status + AND r.student.status = :studentStatus + ORDER BY r.createdAt DESC + """) + Page findByMemberIdAndStatusAndStudentStatus( + @Param("memberId") Long memberId, + @Param("status") ReportedStatus status, + @Param("studentStatus") ReportedStatus studentStatus, + Pageable pageable + ); + + @Query(""" + SELECT r + FROM Review r + WHERE r.student.id = :memberId + ORDER BY r.createdAt DESC + """) Page findByMemberId(@Param("memberId") Long memberId, Pageable pageable); - // List findByStoreId(Long storeId); - Page findByStoreIdOrderByCreatedAtDesc(Long id, Pageable pageable);//최신순 정렬 + @Query(""" + SELECT r + FROM Review r + WHERE r.store.id = :storeId + AND r.status = :status + AND r.student.status = :studentStatus + ORDER BY r.createdAt DESC + """) + Page findByStoreIdAndStatusAndStudentStatus( + @Param("storeId") Long storeId, + @Param("status") ReportedStatus status, + @Param("studentStatus") ReportedStatus studentStatus, + Pageable pageable + ); + + Page findByStoreIdOrderByCreatedAtDesc(Long id, Pageable pageable);// 최신순 정렬 + Page findByStoreId(Long id, Pageable pageable); + @Query("SELECT AVG(r.rate) FROM Review r WHERE r.store.id = :storeId AND r.status = :status AND r.student.status = :studentStatus") + Float standardScoreWithStatus( + @Param("storeId") Long storeId, + @Param("status") ReportedStatus status, + @Param("studentStatus") ReportedStatus studentStatus + ); + @Query("SELECT AVG(r.rate) FROM Review r WHERE r.store.id = :storeId") Float standardScore(Long storeId); diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index df6694a..7256cfa 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -14,6 +14,7 @@ import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.domain.user.repository.PartnershipUsageRepository; import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.domain.common.entity.enums.ReportedStatus; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; @@ -41,7 +42,6 @@ public class ReviewServiceImpl implements ReviewService { private final AmazonS3Manager amazonS3Manager; private final PartnershipUsageRepository partnershipUsageRepository; - @Override public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.WriteReviewRequestDTO request, Long memberId, List reviewImages) { // createReview 메서드 호출로 통합 @@ -97,7 +97,9 @@ private Review createReview(Long memberId, Long storeId, ReviewRequestDTO.WriteR } return reviewRepository.save(review); - } private String generateReviewImageKeyName(Long memberId, Long reviewId, int imageIndex) { + } + + private String generateReviewImageKeyName(Long memberId, Long reviewId, int imageIndex) { LocalDateTime now = LocalDateTime.now(); String year = String.valueOf(now.getYear()); String month = String.format("%02d", now.getMonthValue()); @@ -128,7 +130,12 @@ public Page checkPartnerReview(Long me Store store = storeRepository.findByPartner(partner) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - Page reviews = reviewRepository.findByStoreId(store.getId(), pageable); + // 신고되지 않은 리뷰와 신고되지 않은 학생이 작성한 리뷰만 조회 + Page reviews = reviewRepository.findByStoreIdAndStatusAndStudentStatus( + store.getId(), + ReportedStatus.NORMAL, + ReportedStatus.NORMAL, + pageable); for (Review review : reviews) { updateReviewImageUrls(review); @@ -146,6 +153,7 @@ public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) { .build(); } + private void updateReviewImageUrls(Review review) { for (ReviewPhoto reviewPhoto : review.getImageList()) { if (reviewPhoto.getKeyName() != null) { @@ -162,7 +170,13 @@ public Page checkStoreReview(Long stor pageable = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort()); Store store = storeRepository.findById(storeId).orElseThrow( () -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - Page reviews = reviewRepository.findByStoreId(store.getId(), pageable); + + // 신고되지 않은 리뷰와 신고되지 않은 학생이 작성한 리뷰만 조회 + Page reviews = reviewRepository.findByStoreIdAndStatusAndStudentStatus( + store.getId(), + ReportedStatus.NORMAL, + ReportedStatus.NORMAL, + pageable); for (Review review : reviews) { updateReviewImageUrls(review); @@ -174,7 +188,8 @@ public Page checkStoreReview(Long stor @Override @Transactional public ReviewResponseDTO.StandardScoreResponseDTO standardScore(Long storeId) { - Float score = reviewRepository.standardScore(storeId); + // 신고되지 않은 리뷰와 신고되지 않은 학생이 작성한 리뷰만으로 평균 계산 + Float score = reviewRepository.standardScoreWithStatus(storeId, ReportedStatus.NORMAL, ReportedStatus.NORMAL); if(score == null){ score = 0f; } @@ -191,7 +206,12 @@ public ReviewResponseDTO.StandardScoreResponseDTO myStoreAverage(Long memberId) Store store = storeRepository.findByPartner(partner) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - Float score = reviewRepository.standardScore(store.getId()); + // 신고되지 않은 리뷰와 신고되지 않은 학생이 작성한 리뷰만으로 평균 계산 + Float score = reviewRepository.standardScoreWithStatus(store.getId(), ReportedStatus.NORMAL, + ReportedStatus.NORMAL); + if (score == null) { + score = 0f; + } System.out.println(store.getId()); return ReviewResponseDTO.StandardScoreResponseDTO .builder() From 376663815fdf16665df0c47732c3e1dcf0a29966 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:23:31 +0900 Subject: [PATCH 218/270] =?UTF-8?q?[FEAT/#149]=20=EA=B1=B4=EC=9D=98?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20API=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/SuggestionRepository.java | 28 +++++++++++++++---- .../service/SuggestionServiceImpl.java | 4 ++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java b/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java index b712598..146338b 100644 --- a/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java +++ b/src/main/java/com/assu/server/domain/suggestion/repository/SuggestionRepository.java @@ -1,6 +1,7 @@ package com.assu.server.domain.suggestion.repository; import com.assu.server.domain.suggestion.entity.Suggestion; +import com.assu.server.domain.common.entity.enums.ReportedStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,11 +11,26 @@ public interface SuggestionRepository extends JpaRepository { @Query(""" - select s - from Suggestion s - join fetch s.student st - where s.admin.id = :adminId - order by s.createdAt desc - """) + select s + from Suggestion s + join fetch s.student st + where s.admin.id = :adminId + AND s.status = :status + AND s.student.status = :studentStatus + order by s.createdAt desc + """) + List findAllSuggestionsWithStatus( + @Param("adminId") Long adminId, + @Param("status") ReportedStatus status, + @Param("studentStatus") ReportedStatus studentStatus + ); + + @Query(""" + select s + from Suggestion s + join fetch s.student st + where s.admin.id = :adminId + order by s.createdAt desc + """) List findAllSuggestions(@Param("adminId") Long adminId); } diff --git a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java index 2995ff6..8f1896e 100644 --- a/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java +++ b/src/main/java/com/assu/server/domain/suggestion/service/SuggestionServiceImpl.java @@ -10,6 +10,7 @@ import com.assu.server.domain.suggestion.repository.SuggestionRepository; import com.assu.server.domain.user.entity.Student; import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.domain.common.entity.enums.ReportedStatus; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import jakarta.transaction.Transactional; @@ -46,8 +47,9 @@ public SuggestionResponseDTO.WriteSuggestionResponseDTO writeSuggestion(Suggesti @Override public List getSuggestions(Long adminId) { + // 신고되지 않은 건의글과 신고되지 않은 학생이 작성한 건의글만 조회 List list = suggestionRepository - .findAllSuggestions(adminId); + .findAllSuggestionsWithStatus(adminId, ReportedStatus.NORMAL, ReportedStatus.NORMAL); return SuggestionConverter.toGetSuggestionDTOList(list); } From 2cf74f73bcc2fca68841d2c45345c546951f947b Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:24:02 +0900 Subject: [PATCH 219/270] =?UTF-8?q?[FEAT/#149]=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=9C=ED=96=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/service/ReportServiceImpl.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java diff --git a/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java b/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java new file mode 100644 index 0000000..622a181 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java @@ -0,0 +1,166 @@ +package com.assu.server.domain.report.service; + +import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.member.repository.MemberRepository; +import com.assu.server.domain.report.dto.ReportRequestDTO; +import com.assu.server.domain.report.dto.ReportResponseDTO; +import com.assu.server.domain.report.entity.Report; +import com.assu.server.domain.report.entity.enums.ReportStatus; +import com.assu.server.domain.report.entity.enums.ReportTargetType; +import com.assu.server.domain.report.repository.ReportRepository; +import com.assu.server.domain.review.entity.Review; +import com.assu.server.domain.review.repository.ReviewRepository; +import com.assu.server.domain.suggestion.entity.Suggestion; +import com.assu.server.domain.suggestion.repository.SuggestionRepository; +import com.assu.server.domain.report.exception.ReportException; +import com.assu.server.domain.report.event.ReportProcessedEvent; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import org.springframework.context.ApplicationEventPublisher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReportServiceImpl implements ReportService { + + private final ReportRepository reportRepository; + private final MemberRepository memberRepository; + private final ReviewRepository reviewRepository; + private final SuggestionRepository suggestionRepository; + private final ApplicationEventPublisher eventPublisher; + + @Override + @Transactional + public ReportResponseDTO.CreateReportResponse reportContent(Long reporterId, + ReportRequestDTO.CreateContentReportRequest request) { + // 신고자 존재 확인 + Member reporter = memberRepository.findById(reporterId) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_MEMBER)); + + // 신고 대상 존재 확인 및 자기 자신 신고 방지 + validateContentReportTarget(reporterId, request.getTargetType(), request.getTargetId()); + + // 중복 신고 확인 + if (reportRepository.existsByReporterIdAndTargetTypeAndTargetId(reporterId, request.getTargetType(), + request.getTargetId())) { + throw new ReportException(ErrorStatus.REPORT_DUPLICATE); + } + + // 콘텐츠 신고 생성 + Report report = Report.builder() + .reporter(reporter) + .targetType(request.getTargetType()) + .targetId(request.getTargetId()) + .reported(null) // 콘텐츠 신고는 피신고자 없음 + .reportType(request.getReportType()) + .status(ReportStatus.PENDING) + .build(); + + Report savedReport = reportRepository.save(report); + + // 신고 생성 이벤트 발행 + eventPublisher.publishEvent(new ReportProcessedEvent( + savedReport.getId(), + savedReport.getTargetType(), + savedReport.getTargetId(), + savedReport.getStatus())); + + return ReportResponseDTO.CreateReportResponse.of(savedReport.getId()); + } + + @Override + @Transactional + public ReportResponseDTO.CreateReportResponse reportStudent(Long reporterId, + ReportRequestDTO.CreateStudentReportRequest request) { + // 신고자 존재 확인 + Member reporter = memberRepository.findById(reporterId) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_MEMBER)); + + // 신고 대상 존재 확인 및 자기 자신 신고 방지 + Student reportedStudent = validateStudentReportTarget(reporterId, request.getTargetType(), + request.getTargetId()); + + // 중복 신고 확인 (작성자 기준) + if (reportRepository.existsByReporterIdAndTargetTypeAndTargetId( + reporterId, + ReportTargetType.STUDENT_USER, + reportedStudent.getId()) + ) { + throw new ReportException(ErrorStatus.REPORT_DUPLICATE); + } + + // 작성자 신고 생성 + Report report = Report.builder() + .reporter(reporter) + .targetType(ReportTargetType.STUDENT_USER) + .targetId(reportedStudent.getId()) + .reported(reportedStudent.getMember()) + .reportType(request.getReportType()) + .status(ReportStatus.PENDING) + .build(); + + Report savedReport = reportRepository.save(report); + + // 신고 생성 이벤트 발행 + eventPublisher.publishEvent(new ReportProcessedEvent( + savedReport.getId(), + savedReport.getTargetType(), + savedReport.getTargetId(), + savedReport.getStatus())); + + return ReportResponseDTO.CreateReportResponse.of(savedReport.getId()); + } + + // 콘텐츠 신고 대상 검증 메서드 + private void validateContentReportTarget(Long reporterId, ReportTargetType targetType, Long targetId) { + switch (targetType) { + case REVIEW: + Review review = reviewRepository.findById(targetId) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_SUCH_MEMBER)); + if (reporterId.equals(review.getStudent().getId())) { + throw new ReportException(ErrorStatus.REVIEW_REPORT_SELF_NOT_ALLOWED); + } + break; + case SUGGESTION: + Suggestion suggestion = suggestionRepository.findById(targetId) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_SUCH_SUGGESTION)); + if (reporterId.equals(suggestion.getStudent().getId())) { + throw new ReportException(ErrorStatus.SUGGESTION_REPORT_SELF_NOT_ALLOWED); + } + break; + default: + throw new ReportException(ErrorStatus.INVALID_REPORT_TYPE); + } + } + + // 작성자 신고 대상 검증 메서드 + private Student validateStudentReportTarget(Long reporterId, ReportTargetType targetType, Long targetId) { + Student student; + + switch (targetType) { + case REVIEW: + Review review = reviewRepository.findById(targetId) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_SUCH_MEMBER)); + student = review.getStudent(); + break; + case SUGGESTION: + Suggestion suggestion = suggestionRepository.findById(targetId) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_SUCH_SUGGESTION)); + student = suggestion.getStudent(); + break; + default: + throw new ReportException(ErrorStatus.INVALID_REPORT_TYPE); + } + + // 자기 자신 신고 방지 + if (reporterId.equals(student.getId())) { + throw new ReportException(ErrorStatus.REPORT_SELF_NOT_ALLOWED); + } + + return student; + } +} \ No newline at end of file From 3e977c6e73ff87c341e11307d56ae921db673ff9 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:24:09 +0900 Subject: [PATCH 220/270] =?UTF-8?q?[FEAT/#149]=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReportStatusSyncService.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/report/service/ReportStatusSyncService.java diff --git a/src/main/java/com/assu/server/domain/report/service/ReportStatusSyncService.java b/src/main/java/com/assu/server/domain/report/service/ReportStatusSyncService.java new file mode 100644 index 0000000..f7d94a8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/service/ReportStatusSyncService.java @@ -0,0 +1,99 @@ +package com.assu.server.domain.report.service; + +import com.assu.server.domain.common.entity.enums.ReportedStatus; +import com.assu.server.domain.report.entity.enums.ReportStatus; +import com.assu.server.domain.report.event.ReportProcessedEvent; +import com.assu.server.domain.review.entity.Review; +import com.assu.server.domain.review.repository.ReviewRepository; +import com.assu.server.domain.suggestion.entity.Suggestion; +import com.assu.server.domain.suggestion.repository.SuggestionRepository; +import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.domain.report.exception.ReportException; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReportStatusSyncService { + + private final ReviewRepository reviewRepository; + private final SuggestionRepository suggestionRepository; + private final StudentRepository studentRepository; + + @EventListener + @Async + @Transactional + public void handleReportProcessed(ReportProcessedEvent event) { + log.info("신고 처리 이벤트 수신: Report ID: {}, Target Type: {}, Target ID: {}, Status: {}", + event.getReportId(), event.getTargetType(), event.getTargetId(), event.getStatus()); + + try { + switch (event.getTargetType()) { + case REVIEW: + syncReviewStatus(event); + break; + case SUGGESTION: + syncSuggestionStatus(event); + break; + case STUDENT_USER: + syncStudentUserStatus(event); + break; + default: + log.warn("알 수 없는 신고 대상 타입: {}", event.getTargetType()); + } + } catch (Exception e) { + log.error("신고 상태 동기화 실패: Report ID: {}, Error: {}", event.getReportId(), e.getMessage(), e); + } + } + + private void syncReviewStatus(ReportProcessedEvent event) { + Review review = reviewRepository.findById(event.getTargetId()) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_SUCH_MEMBER)); + + ReportedStatus newStatus = mapReportStatusToReportedStatus(event.getStatus()); + if (newStatus != null) { + review.updateReportedStatus(newStatus); + reviewRepository.save(review); + log.info("리뷰 상태 동기화 완료: Review ID: {}, Status: {}", event.getTargetId(), newStatus); + } + } + + private void syncSuggestionStatus(ReportProcessedEvent event) { + Suggestion suggestion = suggestionRepository.findById(event.getTargetId()) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_SUCH_SUGGESTION)); + + ReportedStatus newStatus = mapReportStatusToReportedStatus(event.getStatus()); + if (newStatus != null) { + suggestion.updateReportedStatus(newStatus); + suggestionRepository.save(suggestion); + log.info("건의글 상태 동기화 완료: Suggestion ID: {}, Status: {}", event.getTargetId(), newStatus); + } + } + + private void syncStudentUserStatus(ReportProcessedEvent event) { + Student student = studentRepository.findById(event.getTargetId()) + .orElseThrow(() -> new ReportException(ErrorStatus.NO_SUCH_MEMBER)); + + ReportedStatus newStatus = mapReportStatusToReportedStatus(event.getStatus()); + if (newStatus != null) { + student.updateReportedStatus(newStatus); + studentRepository.save(student); + log.info("학생 상태 동기화 완료: Student ID: {}, Status: {}", event.getTargetId(), newStatus); + } + } + + private ReportedStatus mapReportStatusToReportedStatus(ReportStatus reportStatus) { + return switch (reportStatus) { + case PROCESSED -> ReportedStatus.REPORTED; + case REJECTED -> ReportedStatus.NORMAL; + case PENDING, UNDER_REVIEW -> null; // PENDING 상태는 상태 변경하지 않음 + }; + } +} From 3e121f89b22cfad17d5942059ccb641bb64c8358 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:26:26 +0900 Subject: [PATCH 221/270] =?UTF-8?q?[FEAT/#149]=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EB=B3=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/controller/ReportController.java | 70 +++++++++++++++++++ .../domain/report/dto/ReportRequestDTO.java | 42 +++++++++++ .../domain/report/dto/ReportResponseDTO.java | 23 ++++++ .../server/domain/report/entity/Report.java | 52 ++++++++++++++ .../report/entity/enums/ReportStatus.java | 15 ++++ .../report/entity/enums/ReportTargetType.java | 14 ++++ .../report/entity/enums/ReportType.java | 28 ++++++++ .../report/event/ReportProcessedEvent.java | 15 ++++ .../report/repository/ReportRepository.java | 12 ++++ .../domain/report/service/ReportService.java | 13 ++++ 10 files changed, 284 insertions(+) create mode 100644 src/main/java/com/assu/server/domain/report/controller/ReportController.java create mode 100644 src/main/java/com/assu/server/domain/report/dto/ReportRequestDTO.java create mode 100644 src/main/java/com/assu/server/domain/report/dto/ReportResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/report/entity/Report.java create mode 100644 src/main/java/com/assu/server/domain/report/entity/enums/ReportStatus.java create mode 100644 src/main/java/com/assu/server/domain/report/entity/enums/ReportTargetType.java create mode 100644 src/main/java/com/assu/server/domain/report/entity/enums/ReportType.java create mode 100644 src/main/java/com/assu/server/domain/report/event/ReportProcessedEvent.java create mode 100644 src/main/java/com/assu/server/domain/report/repository/ReportRepository.java create mode 100644 src/main/java/com/assu/server/domain/report/service/ReportService.java diff --git a/src/main/java/com/assu/server/domain/report/controller/ReportController.java b/src/main/java/com/assu/server/domain/report/controller/ReportController.java new file mode 100644 index 0000000..8c46ab2 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/controller/ReportController.java @@ -0,0 +1,70 @@ +package com.assu.server.domain.report.controller; + +import com.assu.server.domain.report.dto.ReportRequestDTO; +import com.assu.server.domain.report.dto.ReportResponseDTO; +import com.assu.server.domain.report.entity.enums.ReportTargetType; +import com.assu.server.domain.report.service.ReportService; +import com.assu.server.global.apiPayload.BaseResponse; +import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.util.PrincipalDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Report", description = "신고 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/reports") +public class ReportController { + + private final ReportService reportService; + + @Operation(summary = "콘텐츠 신고 API", + description = "# [v1.0 (2025-09-24)]()\n" + + "- 신고자는 본인 Member ID로 자동 설정됩니다.\n" + + "- 자기 자신의 콘텐츠를 신고할 수 없습니다.\n" + + "- 동일한 대상을 중복 신고할 수 없습니다.\n\n" + + "**Request Body:**\n" + + "- `targetType` (String, required): 신고 대상 타입 (REVIEW, SUGGESTION)\n" + + "- `targetId` (Long, required): 리뷰 ID 또는 건의글 ID\n" + + "- `reportType` (String, required): 신고 유형\n" + + " - 리뷰 신고: REVIEW_INAPPROPRIATE_CONTENT, REVIEW_FALSE_INFORMATION, REVIEW_SPAM\n" + + " - 건의글 신고: SUGGESTION_INAPPROPRIATE_CONTENT, SUGGESTION_FALSE_INFORMATION, SUGGESTION_SPAM\n\n" + + "**Response:**\n" + + "- 성공 시 201(CREATED)과 신고 ID 반환") + @PostMapping + public BaseResponse reportContent( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody @Valid ReportRequestDTO.CreateContentReportRequest request + ) { + Long reporterId = principalDetails.getMember().getId(); + ReportResponseDTO.CreateReportResponse response = reportService.reportContent(reporterId, request); + return BaseResponse.onSuccess(SuccessStatus.REPORT_SUCCESS, response); + } + + @Operation(summary = "작성자 신고 API", + description = "# [v1.0 (2025-09-24)]()\n" + + "- 신고자는 본인 Member ID로 자동 설정됩니다.\n" + + "- 자기 자신을 신고할 수 없습니다.\n" + + "- 동일한 작성자를 중복 신고할 수 없습니다.\n\n" + + "**Request Body:**\n" + + "- `targetType` (String, required): 신고 대상 타입 (REVIEW, SUGGESTION)\n" + + "- `targetId` (Long, required): 리뷰 ID 또는 건의글 ID\n" + + "- `reportType` (String, required): 신고 유형\n" + + " - 사용자 신고: USER_SPAM, USER_INAPPROPRIATE_CONTENT, USER_HARASSMENT, USER_FRAUD, USER_PRIVACY_VIOLATION, USER_OTHER\n\n" + + + "**Response:**\n" + + "- 성공 시 201(CREATED)과 신고 ID 반환") + @PostMapping("/students") + public BaseResponse reportStudent( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody @Valid ReportRequestDTO.CreateStudentReportRequest request + ) { + Long reporterId = principalDetails.getMember().getId(); + ReportResponseDTO.CreateReportResponse response = reportService.reportStudent(reporterId, request); + return BaseResponse.onSuccess(SuccessStatus.REPORT_SUCCESS, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/report/dto/ReportRequestDTO.java b/src/main/java/com/assu/server/domain/report/dto/ReportRequestDTO.java new file mode 100644 index 0000000..932676b --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/dto/ReportRequestDTO.java @@ -0,0 +1,42 @@ +package com.assu.server.domain.report.dto; + +import com.assu.server.domain.report.entity.enums.ReportTargetType; +import com.assu.server.domain.report.entity.enums.ReportType; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class ReportRequestDTO { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CreateContentReportRequest { + @NotNull(message = "신고 대상 타입은 필수입니다.") + private ReportTargetType targetType; // REVIEW, SUGGESTION + + @NotNull(message = "신고 대상 ID는 필수입니다.") + private Long targetId; // 리뷰 ID 또는 건의글 ID + + @NotNull(message = "신고 유형은 필수입니다.") + private ReportType reportType; // REVIEW_*, SUGGESTION_* + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CreateStudentReportRequest { + @NotNull(message = "신고 대상의 작성 컨텐츠의 타입은 필수입니다.") + private ReportTargetType targetType; // REVIEW, SUGGESTION + + @NotNull(message = "신고 대상의 작성 컨텐츠 ID는 필수입니다.") + private Long targetId; // 리뷰 ID 또는 건의글 ID + + @NotNull(message = "유저 신고 유형은 필수입니다.") + private ReportType reportType; // USER_* + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/report/dto/ReportResponseDTO.java b/src/main/java/com/assu/server/domain/report/dto/ReportResponseDTO.java new file mode 100644 index 0000000..4f506b5 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/dto/ReportResponseDTO.java @@ -0,0 +1,23 @@ +package com.assu.server.domain.report.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class ReportResponseDTO { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CreateReportResponse { + private Long reportId; + + public static CreateReportResponse of(Long reportId) { + return CreateReportResponse.builder() + .reportId(reportId) + .build(); + } + } +} diff --git a/src/main/java/com/assu/server/domain/report/entity/Report.java b/src/main/java/com/assu/server/domain/report/entity/Report.java new file mode 100644 index 0000000..6e2b9a1 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/entity/Report.java @@ -0,0 +1,52 @@ +package com.assu.server.domain.report.entity; + +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.report.entity.enums.ReportStatus; +import com.assu.server.domain.report.entity.enums.ReportTargetType; +import com.assu.server.domain.report.entity.enums.ReportType; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Report extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reporter_id", nullable = false) + private Member reporter; // 신고자 + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ReportTargetType targetType; // 신고 대상 타입 (STUDENT_USER, REVIEW, SUGGESTION) + + @Column(nullable = false) + private Long targetId; // 신고 대상 ID (사용자 ID, 리뷰 ID, 건의 ID 등) + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reported_id") + private Member reported; // 피신고자 (사용자 신고인 경우에만) + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ReportType reportType; // 신고 유형 + + @Enumerated(EnumType.STRING) + @Builder.Default + private ReportStatus status = ReportStatus.PENDING; // 신고 상태 + + // Todo 관리자용 업데이트 로직 추가 + // 신고 상태 업데이트 메서드 + public void updateStatus(ReportStatus status) { + this.status = status; + } +} diff --git a/src/main/java/com/assu/server/domain/report/entity/enums/ReportStatus.java b/src/main/java/com/assu/server/domain/report/entity/enums/ReportStatus.java new file mode 100644 index 0000000..f9348ac --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/entity/enums/ReportStatus.java @@ -0,0 +1,15 @@ +package com.assu.server.domain.report.entity.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ReportStatus { + PENDING("대기중"), + PROCESSED("처리완료"), + REJECTED("기각"), + UNDER_REVIEW("검토중"); + + private final String description; +} diff --git a/src/main/java/com/assu/server/domain/report/entity/enums/ReportTargetType.java b/src/main/java/com/assu/server/domain/report/entity/enums/ReportTargetType.java new file mode 100644 index 0000000..fff8958 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/entity/enums/ReportTargetType.java @@ -0,0 +1,14 @@ +package com.assu.server.domain.report.entity.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ReportTargetType { + STUDENT_USER("학생 사용자"), + REVIEW("리뷰"), + SUGGESTION("건의글"); + + private final String description; +} diff --git a/src/main/java/com/assu/server/domain/report/entity/enums/ReportType.java b/src/main/java/com/assu/server/domain/report/entity/enums/ReportType.java new file mode 100644 index 0000000..fcd9bc6 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/entity/enums/ReportType.java @@ -0,0 +1,28 @@ +package com.assu.server.domain.report.entity.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ReportType { + // 사용자 신고용 + STUDENT_USER_SPAM("스팸/홍보"), + STUDENT_USER_INAPPROPRIATE_CONTENT("부적절한 내용"), + STUDENT_USER_HARASSMENT("괴롭힘/욕설"), + STUDENT_USER_FRAUD("사기/부정행위"), + STUDENT_USER_PRIVACY_VIOLATION("개인정보 침해"), + STUDENT_USER_OTHER("기타"), + + // 리뷰 신고용 + REVIEW_INAPPROPRIATE_CONTENT("부적절한 내용 및 욕설이 포함된 리뷰에요"), + REVIEW_FALSE_INFORMATION("허위사실 / 거짓이 포함된 리뷰에요"), + REVIEW_SPAM("홍보 / 광고를 위한 리뷰에요"), + + // 건의글 신고용 + SUGGESTION_INAPPROPRIATE_CONTENT("부적절한 내용 및 욕설이 포함된 건의글이에요"), + SUGGESTION_FALSE_INFORMATION("허위사실 / 거짓이 포함된 건의글에요"), + SUGGESTION_SPAM("홍보/광고를 위한 건의글이에요 "); + + private final String description; +} diff --git a/src/main/java/com/assu/server/domain/report/event/ReportProcessedEvent.java b/src/main/java/com/assu/server/domain/report/event/ReportProcessedEvent.java new file mode 100644 index 0000000..44af997 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/event/ReportProcessedEvent.java @@ -0,0 +1,15 @@ +package com.assu.server.domain.report.event; + +import com.assu.server.domain.report.entity.enums.ReportTargetType; +import com.assu.server.domain.report.entity.enums.ReportStatus; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ReportProcessedEvent { + private final Long reportId; + private final ReportTargetType targetType; + private final Long targetId; + private final ReportStatus status; +} diff --git a/src/main/java/com/assu/server/domain/report/repository/ReportRepository.java b/src/main/java/com/assu/server/domain/report/repository/ReportRepository.java new file mode 100644 index 0000000..155e6a9 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/repository/ReportRepository.java @@ -0,0 +1,12 @@ +package com.assu.server.domain.report.repository; + +import com.assu.server.domain.report.entity.Report; +import com.assu.server.domain.report.entity.enums.ReportTargetType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReportRepository extends JpaRepository { + // 특정 사용자가 특정 대상을 신고했는지 확인 + boolean existsByReporterIdAndTargetTypeAndTargetId(Long reporterId, ReportTargetType targetType, Long targetId); +} diff --git a/src/main/java/com/assu/server/domain/report/service/ReportService.java b/src/main/java/com/assu/server/domain/report/service/ReportService.java new file mode 100644 index 0000000..5ffaec7 --- /dev/null +++ b/src/main/java/com/assu/server/domain/report/service/ReportService.java @@ -0,0 +1,13 @@ +package com.assu.server.domain.report.service; + +import com.assu.server.domain.report.dto.ReportRequestDTO; +import com.assu.server.domain.report.dto.ReportResponseDTO; + +public interface ReportService { + + // 콘텐츠 신고 생성 (리뷰, 건의글) + ReportResponseDTO.CreateReportResponse reportContent(Long reporterId, ReportRequestDTO.CreateContentReportRequest request); + + // 작성자 신고 생성 (리뷰/건의글 작성자) + ReportResponseDTO.CreateReportResponse reportStudent(Long reporterId, ReportRequestDTO.CreateStudentReportRequest request); +} \ No newline at end of file From f899431e42083205bbe921955a2b4016c01aa4d2 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:26:53 +0900 Subject: [PATCH 222/270] =?UTF-8?q?[FEAT/#149]=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EC=8B=A0=EA=B3=A0=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/entity/enums/ReportedStatus.java | 14 ++++++++++++++ .../assu/server/domain/review/entity/Review.java | 14 +++++++++++++- .../domain/suggestion/entity/Suggestion.java | 13 ++++++++++++- .../assu/server/domain/user/entity/Student.java | 13 ++++++++++--- 4 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/common/entity/enums/ReportedStatus.java diff --git a/src/main/java/com/assu/server/domain/common/entity/enums/ReportedStatus.java b/src/main/java/com/assu/server/domain/common/entity/enums/ReportedStatus.java new file mode 100644 index 0000000..52301ee --- /dev/null +++ b/src/main/java/com/assu/server/domain/common/entity/enums/ReportedStatus.java @@ -0,0 +1,14 @@ +package com.assu.server.domain.common.entity.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ReportedStatus { + NORMAL("정상"), + REPORTED("신고됨"), + DELETED("삭제됨"); + + private final String description; +} diff --git a/src/main/java/com/assu/server/domain/review/entity/Review.java b/src/main/java/com/assu/server/domain/review/entity/Review.java index a4c7a1b..b82f758 100644 --- a/src/main/java/com/assu/server/domain/review/entity/Review.java +++ b/src/main/java/com/assu/server/domain/review/entity/Review.java @@ -3,12 +3,15 @@ import java.util.List; import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.common.entity.enums.ReportedStatus; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.user.entity.Student; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -21,7 +24,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; - @Entity @Getter @NoArgsConstructor @@ -46,6 +48,7 @@ public class Review extends BaseEntity { @OneToMany(mappedBy = "review", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List imageList = new ArrayList<>(); + public List getImageList() { if (imageList == null) { imageList = new ArrayList<>(); @@ -57,4 +60,13 @@ public List getImageList() { private String content; private String affiliation; + + @Enumerated(EnumType.STRING) + @Builder.Default + private ReportedStatus status = ReportedStatus.NORMAL; + + // 상태 업데이트 메서드 + public void updateReportedStatus(ReportedStatus status) { + this.status = status; + } } diff --git a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java index e3e6685..228cd8a 100644 --- a/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java +++ b/src/main/java/com/assu/server/domain/suggestion/entity/Suggestion.java @@ -2,10 +2,12 @@ import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.common.entity.BaseEntity; -import com.assu.server.domain.store.entity.Store; +import com.assu.server.domain.common.entity.enums.ReportedStatus; import com.assu.server.domain.user.entity.Student; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -37,4 +39,13 @@ public class Suggestion extends BaseEntity { private String storeName; private String content; + + @Enumerated(EnumType.STRING) + @Builder.Default + private ReportedStatus status = ReportedStatus.NORMAL; + + // 신고 상태 업데이트 메서드 + public void updateReportedStatus(ReportedStatus status) { + this.status = status; + } } diff --git a/src/main/java/com/assu/server/domain/user/entity/Student.java b/src/main/java/com/assu/server/domain/user/entity/Student.java index e7ee270..0c1b034 100644 --- a/src/main/java/com/assu/server/domain/user/entity/Student.java +++ b/src/main/java/com/assu/server/domain/user/entity/Student.java @@ -1,16 +1,14 @@ package com.assu.server.domain.user.entity; +import com.assu.server.domain.common.entity.enums.ReportedStatus; import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.user.entity.enums.Department; import com.assu.server.domain.user.entity.enums.EnrollmentStatus; import com.assu.server.domain.user.entity.enums.Major; import com.assu.server.domain.user.entity.enums.University; import jakarta.persistence.*; -import jakarta.validation.constraints.Pattern; import lombok.*; -import java.util.ArrayList; -import java.util.List; @Entity @Getter @@ -44,6 +42,10 @@ public class Student { @Enumerated(EnumType.STRING) private Major major; + @Enumerated(EnumType.STRING) + @Builder.Default + private ReportedStatus status = ReportedStatus.NORMAL; + public void setMember(Member member) { this.member = member; } @@ -66,4 +68,9 @@ public void updateStudentInfo(String name, Major major, EnrollmentStatus enrollm this.enrollmentStatus = enrollmentStatus; this.yearSemester = yearSemester; } + + // 신고 상태 업데이트 메서드 + public void updateReportedStatus(ReportedStatus status) { + this.status = status; + } } From c00a1ea81d38dacbbc03b51d44ef6126e2f2c49b Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:27:03 +0900 Subject: [PATCH 223/270] =?UTF-8?q?[FEAT/#149]=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/report/exception/ReportException.java | 6 ++---- .../global/apiPayload/code/status/SuccessStatus.java | 9 +-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/assu/server/domain/report/exception/ReportException.java b/src/main/java/com/assu/server/domain/report/exception/ReportException.java index 8df2612..2eb8749 100644 --- a/src/main/java/com/assu/server/domain/report/exception/ReportException.java +++ b/src/main/java/com/assu/server/domain/report/exception/ReportException.java @@ -4,7 +4,5 @@ import com.assu.server.global.exception.GeneralException; public class ReportException extends GeneralException { - public ReportException(BaseErrorCode errorCode) { - super(errorCode); - } -} + public ReportException(BaseErrorCode errorStatus) { super(errorStatus); } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java index 175a1a9..79f2836 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/SuccessStatus.java @@ -23,14 +23,7 @@ public enum SuccessStatus implements BaseCode { VERIFY_AUTH_NUMBER_SUCCESS(HttpStatus.OK, "AUTH_201", "성공적으로 생성되었습니다."), //신고 성공 - REPORT_HISTORY_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "기록 신고의 정보가 성공적으로 조회되었습니다."), - REPORT_HISTORY_SUCCESS(HttpStatus.OK, "REPORT_201", "기록을 성공적으로 신고했습니다."), - REPORT_COMMENT_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "댓글 신고의 정보가 성공적으로 조회되었습니다."), - REPORT_COMMENT_SUCCESS(HttpStatus.OK, "REPORT_201", "댓글을 성공적으로 신고했습니다."), - REPORT_PROFILE_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200", "계정 신고의 정보가 성공적으로 조회되었습니다."), - REPORT_PROFILE_SUCCESS(HttpStatus.OK, "REPORT_201", "계정을 성공적으로 신고했습니다."), - REPORT_ADMIN_VIEW_SUCCESS(HttpStatus.OK, "REPORT_200","관리자용 신고 기록이 성공적으로 조회되었습니다."), - REPORT_ADMIN_PROCESSED(HttpStatus.OK,"REPORT_204","신고가 성공적으로 처리되었습니다."), + REPORT_SUCCESS(HttpStatus.CREATED, "REPORT_201", "대상을 성공적으로 신고했습니다."), // 제휴 성공 PAPER_STORE_HISTORY_SUCCESS(HttpStatus.OK, "PAPER201", "가게 별 제휴 내용이 성공적으로 조회되었습니다."), From 34ace894a9e8c6c2a194c426392e394c21435d11 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Wed, 24 Sep 2025 03:55:55 +0900 Subject: [PATCH 224/270] =?UTF-8?q?[CHORE/#149]=20ReportController=20?= =?UTF-8?q?=EB=B2=84=EC=A0=80=EB=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/report/controller/ReportController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/report/controller/ReportController.java b/src/main/java/com/assu/server/domain/report/controller/ReportController.java index 8c46ab2..dbe3420 100644 --- a/src/main/java/com/assu/server/domain/report/controller/ReportController.java +++ b/src/main/java/com/assu/server/domain/report/controller/ReportController.java @@ -23,7 +23,7 @@ public class ReportController { private final ReportService reportService; @Operation(summary = "콘텐츠 신고 API", - description = "# [v1.0 (2025-09-24)]()\n" + + description = "# [v1.0 (2025-09-24)](https://clumsy-seeder-416.notion.site/API-2771197c19ed80b79afbf3d8d8d82c15?source=copy_link)\n" + "- 신고자는 본인 Member ID로 자동 설정됩니다.\n" + "- 자기 자신의 콘텐츠를 신고할 수 없습니다.\n" + "- 동일한 대상을 중복 신고할 수 없습니다.\n\n" + @@ -46,7 +46,7 @@ public BaseResponse reportContent( } @Operation(summary = "작성자 신고 API", - description = "# [v1.0 (2025-09-24)]()\n" + + description = "# [v1.0 (2025-09-24)](https://clumsy-seeder-416.notion.site/API-2771197c19ed80f8ab45e70772fcfc58?source=copy_link)\n" + "- 신고자는 본인 Member ID로 자동 설정됩니다.\n" + "- 자기 자신을 신고할 수 없습니다.\n" + "- 동일한 작성자를 중복 신고할 수 없습니다.\n\n" + From 61e93fb760d7b6c3f6994b6b360143695161c745 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Wed, 24 Sep 2025 22:22:30 +1000 Subject: [PATCH 225/270] =?UTF-8?q?[REFACTOR/#151]=20-=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/PaperRepository.java | 15 ++++++++++-- .../service/PartnershipServiceImpl.java | 24 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java index 4f9fdf3..d312543 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -37,8 +37,19 @@ Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc( Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(Long adminId, Long partnerId, List statuses); // Admin 기준 (SUSPEND) - @Query("select p from Paper p join fetch p.partner where p.isActivated = :status order by p.createdAt desc") - List findAllByIsActivatedWithPartner(@Param("status") ActivationStatus status); + @Query(""" +select p +from Paper p +left join fetch p.partner pt +left join fetch p.store s +where p.isActivated = :status + and p.admin.id = :adminId +order by p.createdAt desc +""") + List findAllSuspendedByAdminWithPartner( + @Param("status") ActivationStatus status, + @Param("adminId") Long adminId + ); // Partner 기준 (ACTIVE) List findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Sort sort); 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 8418f44..b189af0 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 @@ -252,12 +252,17 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long pa @Override @Transactional public List getSuspendedPapers(Long adminId) { - List suspendedPapers = paperRepository.findAllByIsActivatedWithPartner(ActivationStatus.SUSPEND); + List suspendedPapers = + paperRepository.findAllSuspendedByAdminWithPartner(ActivationStatus.SUSPEND, adminId); return suspendedPapers.stream() .map(paper -> PartnershipResponseDTO.SuspendedPaperDTO.builder() .paperId(paper.getId()) - .partnerName(paper.getPartner().getName()) + .partnerName( + paper.getPartner() != null + ? paper.getPartner().getName() + : (paper.getStore() != null ? paper.getStore().getName() : "미등록") + ) .createdAt(paper.getCreatedAt()) .build()) .toList(); @@ -400,16 +405,25 @@ public void deletePartnership(Long paperId) { Paper paper = paperRepository.findById(paperId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); + // 1. paperContent + goods 삭제 List contentsToDelete = paperContentRepository.findByPaperId(paperId); - if (contentsToDelete != null && !contentsToDelete.isEmpty()) { - List contentIds = contentsToDelete.stream().map(PaperContent::getId).toList(); - goodsRepository.deleteAllByContentIds(contentIds); + List contentIds = contentsToDelete.stream() + .map(PaperContent::getId) + .toList(); + goodsRepository.deleteAllByContentIds(contentIds); paperContentRepository.deleteAll(contentsToDelete); } + // 2. paper 삭제 paperRepository.delete(paper); + + // 3. 임시 store 삭제 (partner가 null인 경우만) + Store store = paper.getStore(); + if (store != null && paper.getPartner() == null) { + storeRepository.delete(store); + } } @Override From 9bf34065d3bd7bae927b79477fb70a4999016ae0 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Wed, 24 Sep 2025 22:01:55 +0900 Subject: [PATCH 226/270] =?UTF-8?q?refactor/#38=20-=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=20=EC=83=81=EB=8C=80?= =?UTF-8?q?=EB=B0=A9=20=EC=97=86=EC=9C=BC=EB=A9=B4=201=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 21 ++++- .../domain/chat/converter/ChatConverter.java | 4 +- .../domain/chat/dto/ChatRequestDTO.java | 16 ++-- .../domain/chat/dto/ChatResponseDTO.java | 11 ++- .../server/domain/chat/entity/Message.java | 2 + .../domain/chat/service/ChatServiceImpl.java | 9 +- .../server/global/util/PresenceTracker.java | 90 +++++++++++++++++++ 7 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/assu/server/global/util/PresenceTracker.java diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index d7893a9..c27eafb 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -4,6 +4,7 @@ import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.service.ChatService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; +import com.assu.server.global.util.PresenceTracker; import com.assu.server.global.util.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -24,6 +25,7 @@ public class ChatController { private final ChatService chatService; private final SimpMessagingTemplate simpMessagingTemplate; + private final PresenceTracker presenceTracker; @Operation( summary = "채팅방을 생성하는 API", @@ -61,10 +63,21 @@ public BaseResponse> ) @MessageMapping("/send") public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { - log.info("[WS] handleMessage IN: {}", request); // ★ 호출 여부 확인 - ChatResponseDTO.SendMessageResponseDTO response = chatService.handleMessage(request); - log.info("[WS] handleMessage SAVED id={}", response.messageId()); // 저장 확인용 - simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), response); + // 먼저 접속 여부 확인 후 unreadCount 계산 + boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); + int unreadForSender = receiverInRoom ? 0 : 1; + request.setUnreadCountForSender(unreadForSender); +// log.info("[WS] handleMessage IN: {}", request); // ★ 호출 여부 확인 + ChatResponseDTO.SendMessageResponseDTO saved = chatService.handleMessage(request); + + log.info(">>>> [CHECK 1] 수신자 ID {}의 접속 상태: {}, 계산된 unreadCount: {}", + request.getReceiverId(), receiverInRoom, unreadForSender); + + log.info(">>>> [CHECK 2] 브로드캐스팅 직전 메시지: {}", saved); + // 잘 전송됐는지 확인용 +// String destination = "/sub/chat/" + request.roomId(); +// log.info("[WS] convertAndSend → destination={}, payload={}", destination, response); + simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved); } @Operation( diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 6082b4c..b0b7877 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -57,7 +57,8 @@ public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO reque .chattingRoom(room) .sender(sender) .receiver(receiver) - .message(request.message()) + .message(request.getMessage()) + .unreadCount(request.getUnreadCountForSender()) .build(); } @@ -70,6 +71,7 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me .message(message.getMessage()) .sentAt(message.getCreatedAt()) .messageType(message.getType()) + .unreadCountForSender(message.getUnreadCount()) .build(); } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index 90798fc..575c46b 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -1,6 +1,7 @@ package com.assu.server.domain.chat.dto; import lombok.Getter; +import lombok.Setter; public class ChatRequestDTO { @Getter @@ -9,10 +10,13 @@ public static class CreateChatRoomRequestDTO { private Long partnerId; } - public record ChatMessageRequestDTO( - Long roomId, - Long senderId, - Long receiverId, - String message - ) {} + @Getter + @Setter + public static class ChatMessageRequestDTO { + private Long roomId; + private Long senderId; + private Long receiverId; + private String message; + private int unreadCountForSender; + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 29bbaa1..37df472 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -33,8 +33,15 @@ public record SendMessageResponseDTO( String message, MessageType messageType, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") - LocalDateTime sentAt - ) {} + LocalDateTime sentAt, + Integer unreadCountForSender + ) { + public SendMessageResponseDTO withUnreadCountForSender(Integer count) { + return new SendMessageResponseDTO( + messageId, roomId, senderId, receiverId, message, messageType, sentAt, count + ); + } + } // 메시지 읽음 처리 public record ReadMessageResponseDTO( diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java index e668060..e25ad2c 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/Message.java +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -36,6 +36,8 @@ public class Message extends BaseEntity { private String message; + private Integer unreadCount; + // private LocalDateTime sendTime; // private LocalDateTime readTime; diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index df09fde..b4238da 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -83,22 +83,19 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C @Transactional public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { // 유효성 검사 - ChattingRoom room = chatRepository.findById(request.roomId()) + ChattingRoom room = chatRepository.findById(request.getRoomId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); - Member sender = memberRepository.findById(request.senderId()) + Member sender = memberRepository.findById(request.getSenderId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); - Member receiver = memberRepository.findById(request.receiverId()) + Member receiver = memberRepository.findById(request.getReceiverId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); // messageRepository.save(message); - log.info("saved message start"); Message saved = messageRepository.saveAndFlush(message); - log.info("saved message middle"); log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); - log.info("saved message end"); boolean exists = messageRepository.existsById(saved.getId()); log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제 return ChatConverter.toSendMessageDTO(saved); diff --git a/src/main/java/com/assu/server/global/util/PresenceTracker.java b/src/main/java/com/assu/server/global/util/PresenceTracker.java new file mode 100644 index 0000000..e6f139b --- /dev/null +++ b/src/main/java/com/assu/server/global/util/PresenceTracker.java @@ -0,0 +1,90 @@ +package com.assu.server.global.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.SessionDisconnectEvent; +import org.springframework.web.socket.messaging.SessionSubscribeEvent; +import org.springframework.web.socket.messaging.SessionUnsubscribeEvent; + +import java.security.Principal; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Component +@Slf4j +public class PresenceTracker { + private final Map> roomSubscribers = new ConcurrentHashMap<>(); + private final Map sessionToMember = new ConcurrentHashMap<>(); + private final Map> sessionToRooms = new ConcurrentHashMap<>(); + + private Long parseRoomId(String dest) { // "/sub/chat/26" -> 26 + if (dest == null) return null; + String[] p = dest.split("/"); + if (p.length >= 4 && "chat".equals(p[2])) return Long.valueOf(p[3]); + return null; + } + + private Long memberIdFrom(Principal user) { + if (user == null) return null; + // StompAuthChannelInterceptor 에서 Principal.name을 memberId로 넣어두었다고 가정 + return Long.valueOf(user.getName()); + } + + @EventListener + public void onSubscribe(SessionSubscribeEvent e) { + var acc = StompHeaderAccessor.wrap(e.getMessage()); + Long roomId = parseRoomId(acc.getDestination()); + Long memberId = memberIdFrom(e.getUser()); + if (roomId == null || memberId == null) return; + + String sessionId = acc.getSessionId(); + sessionToMember.put(sessionId, memberId); + sessionToRooms.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(roomId); + roomSubscribers.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(memberId); + + log.debug("SUB: member {} -> room {}", memberId, roomId); + } + + @EventListener + public void onUnsubscribe(SessionUnsubscribeEvent e) { + var acc = StompHeaderAccessor.wrap(e.getMessage()); + String sessionId = acc.getSessionId(); + var rooms = sessionToRooms.getOrDefault(sessionId, Set.of()); + Long memberId = sessionToMember.get(sessionId); + if (memberId != null) { + for (Long roomId : rooms) { + var set = roomSubscribers.get(roomId); + if (set != null) { + set.remove(memberId); + if (set.isEmpty()) roomSubscribers.remove(roomId); + } + } + } + sessionToRooms.remove(sessionId); + log.debug("UNSUB: session {}", sessionId); + } + + @EventListener + public void onDisconnect(SessionDisconnectEvent e) { + String sessionId = e.getSessionId(); + Long memberId = sessionToMember.remove(sessionId); + var rooms = sessionToRooms.remove(sessionId); + if (memberId != null && rooms != null) { + for (Long roomId : rooms) { + var set = roomSubscribers.get(roomId); + if (set != null) { + set.remove(memberId); + if (set.isEmpty()) roomSubscribers.remove(roomId); + } + } + } + log.debug("DISCONNECT: session {}", sessionId); + } + + public boolean isInRoom(Long memberId, Long roomId) { + return roomSubscribers.getOrDefault(roomId, Set.of()).contains(memberId); + } +} From 478330b365c5cf308979d2baffcf3ce79b40be61 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Wed, 24 Sep 2025 22:13:16 +0900 Subject: [PATCH 227/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EB=82=98=EA=B0=94=EB=8B=A4=EC=99=80=EB=8F=84=201?= =?UTF-8?q?=20=EC=95=88=EC=82=AC=EB=9D=BC=EC=A7=80=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/assu/server/domain/chat/dto/ChatMessageDTO.java | 3 +++ .../assu/server/domain/chat/repository/MessageRepository.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java index ad20a6f..1e0f3ac 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java @@ -22,6 +22,9 @@ public class ChatMessageDTO { private String message; private LocalDateTime sendTime; + @JsonProperty("unreadCountForSender") + private Integer unreadCount; + @JsonProperty("isRead") private boolean isRead; diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java index 8d5a316..b560c0d 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -7,7 +7,6 @@ import org.springframework.data.repository.query.Param; import java.util.List; -import java.util.Optional; public interface MessageRepository extends JpaRepository { @Query(""" @@ -25,6 +24,7 @@ public interface MessageRepository extends JpaRepository { m.id, m.message, m.createdAt, + m.unreadCount, m.isRead, CASE WHEN m.sender.id = :memberId THEN true ELSE false From 9b251cd11deedfd95619d06d9b7219eea41aaf07 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Wed, 24 Sep 2025 22:36:59 +0900 Subject: [PATCH 228/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EB=82=98=EA=B0=94=EB=8B=A4=EC=99=80=EB=8F=84=201?= =?UTF-8?q?=20=EC=95=88=EC=82=AC=EB=9D=BC=EC=A7=80=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/assu/server/domain/chat/entity/Message.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/assu/server/domain/chat/entity/Message.java b/src/main/java/com/assu/server/domain/chat/entity/Message.java index e25ad2c..ba06e81 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/Message.java +++ b/src/main/java/com/assu/server/domain/chat/entity/Message.java @@ -49,5 +49,6 @@ public class Message extends BaseEntity { public void markAsRead() { this.isRead = true; + this.unreadCount = 0; } } \ No newline at end of file From 669db28655c52732196174b8cc6ad1a0a8f75b10 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Thu, 25 Sep 2025 00:18:36 +0900 Subject: [PATCH 229/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=EB=8F=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 31 ++++++++++++++----- .../domain/chat/dto/ChatRoomUpdateDTO.java | 15 +++++++++ .../chat/repository/MessageRepository.java | 9 ++++++ .../domain/chat/service/ChatServiceImpl.java | 1 - 4 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index c27eafb..4f78006 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -2,6 +2,8 @@ import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatResponseDTO; +import com.assu.server.domain.chat.dto.ChatRoomUpdateDTO; +import com.assu.server.domain.chat.repository.MessageRepository; import com.assu.server.domain.chat.service.ChatService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import com.assu.server.global.util.PresenceTracker; @@ -26,6 +28,7 @@ public class ChatController { private final ChatService chatService; private final SimpMessagingTemplate simpMessagingTemplate; private final PresenceTracker presenceTracker; + private final MessageRepository messageRepository; @Operation( summary = "채팅방을 생성하는 API", @@ -67,17 +70,29 @@ public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); int unreadForSender = receiverInRoom ? 0 : 1; request.setUnreadCountForSender(unreadForSender); -// log.info("[WS] handleMessage IN: {}", request); // ★ 호출 여부 확인 + ChatResponseDTO.SendMessageResponseDTO saved = chatService.handleMessage(request); + simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved); - log.info(">>>> [CHECK 1] 수신자 ID {}의 접속 상태: {}, 계산된 unreadCount: {}", - request.getReceiverId(), receiverInRoom, unreadForSender); + if (!receiverInRoom) { + Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver( + request.getRoomId(), + request.getReceiverId() + ); - log.info(">>>> [CHECK 2] 브로드캐스팅 직전 메시지: {}", saved); - // 잘 전송됐는지 확인용 -// String destination = "/sub/chat/" + request.roomId(); -// log.info("[WS] convertAndSend → destination={}, payload={}", destination, response); - simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved); + ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder() + .roomId(request.getRoomId()) + .lastMessage(saved.message()) + .lastMessageTime(saved.sentAt()) + .unreadCount(totalUnreadCount) + .build(); + + simpMessagingTemplate.convertAndSendToUser( + request.getReceiverId().toString(), + "/queue/updates", + updateDTO + ); + } } @Operation( diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java new file mode 100644 index 0000000..b83ee0c --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomUpdateDTO.java @@ -0,0 +1,15 @@ +package com.assu.server.domain.chat.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class ChatRoomUpdateDTO { + private Long roomId; + private String lastMessage; + private LocalDateTime lastMessageTime; + private Long unreadCount; // 해당 채팅방의 총 안읽은 메시지 수 +} diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java index b560c0d..9fe4581 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -17,6 +17,15 @@ public interface MessageRepository extends JpaRepository { """) List findUnreadMessagesByRoomAndReceiver(Long roomId, Long receiverId); + @Query(""" + SELECT COUNT(m) + FROM Message m + WHERE m.chattingRoom.id = :roomId + AND m.receiver.id = :receiverId + AND m.isRead = false + """) + Long countUnreadMessagesByRoomAndReceiver(@Param("roomId") Long roomId, @Param("receiverId") Long receiverId); + @Query(""" SELECT new com.assu.server.domain.chat.dto.ChatMessageDTO ( diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index b4238da..62feb6d 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -91,7 +91,6 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); -// messageRepository.save(message); Message saved = messageRepository.saveAndFlush(message); log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); From 8c551bfb1f40e3b15c8f208b81b154395dfec3dd Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Thu, 25 Sep 2025 15:44:59 +0900 Subject: [PATCH 230/270] =?UTF-8?q?[FIX/#154]=20JWT=20=EB=B0=8F=20Security?= =?UTF-8?q?Config=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/domain/auth/security/jwt/JwtAuthFilter.java | 5 ++++- .../java/com/assu/server/global/config/SecurityConfig.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) 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 0ad4bd4..fd1f998 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 @@ -48,14 +48,17 @@ public class JwtAuthFilter extends OncePerRequestFilter { private static final String[] WHITELIST = { "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/swagger-resources/**", "/webjars/**", - // Auth (로그아웃 제외) + // Auth (로그아웃/탈퇴/리프레시 제외) "/auth/phone-verification/send", "/auth/phone-verification/verify", + "/auth/phone-verification/check", + "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", "/auth/admins/signup", "/auth/commons/login", "/auth/students/login", + "/auth/tokens/refresh", "/auth/students/ssu-verify" }; 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 a4fceab..450a667 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -33,11 +33,14 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF .requestMatchers(// Auth (로그아웃 제외) "/auth/phone-verification/send", "/auth/phone-verification/verify", + "/auth/phone-verification/check", + "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", "/auth/admins/signup", "/auth/commons/login", "/auth/students/login", + "/auth/tokens/refresh", "/auth/students/ssu-verify", "/map/place" ).permitAll() From d9e9ad8efd94c30a30b18587d9c52efad02158ce Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:18:16 +1000 Subject: [PATCH 231/270] =?UTF-8?q?[REFACTOR/#157]=20-=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=83=88=EB=A1=9C=20=EB=93=B1=EB=A1=9D=EB=90=98?= =?UTF-8?q?=EC=97=88=EC=9D=84=EC=8B=9C,=20=ED=95=B4=EB=8B=B9=20store?= =?UTF-8?q?=EC=9D=98=20rate=20update=ED=95=98=EB=8A=94=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/service/ReviewServiceImpl.java | 25 ++++++++++++++++++- .../server/domain/store/entity/Store.java | 1 + .../store/repository/StoreRepository.java | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java index 7256cfa..8865e50 100644 --- a/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/assu/server/domain/review/service/ReviewServiceImpl.java @@ -52,6 +52,7 @@ public ReviewResponseDTO.WriteReviewResponseDTO writeReview(ReviewRequestDTO.Wri ); pu.setIsReviewed(true); partnershipUsageRepository.save(pu); + recalcAndUpdateStoreRate(review.getStore().getId()); return ReviewConverter.writeReviewResultDTO(review); } @@ -147,11 +148,16 @@ public Page checkPartnerReview(Long me @Override @Transactional public ReviewResponseDTO.DeleteReviewResponseDTO deleteReview(Long reviewId) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new DatabaseException(ErrorStatus._BAD_REQUEST)); + + Long storeId = review.getStore().getId(); + recalcAndUpdateStoreRate(storeId); + reviewRepository.deleteById(reviewId); return ReviewResponseDTO.DeleteReviewResponseDTO.builder() .reviewId(reviewId) .build(); - } private void updateReviewImageUrls(Review review) { @@ -219,4 +225,21 @@ public ReviewResponseDTO.StandardScoreResponseDTO myStoreAverage(Long memberId) .build(); } + + private void recalcAndUpdateStoreRate(Long storeId) { + // 이 시점에 영속성 컨텍스트의 변경분을 DB로 내보내 평균에 반영 + reviewRepository.flush(); + + Float avg = reviewRepository.standardScoreWithStatus( + storeId, ReportedStatus.NORMAL, ReportedStatus.NORMAL + ); + if (avg == null) avg = 0f; + + int rounded = (int) (Math.round(avg * 10f) / 10f); + + storeRepository.findById(storeId).ifPresent(s -> { + s.setRate(rounded); + storeRepository.save(s); + }); + } } diff --git a/src/main/java/com/assu/server/domain/store/entity/Store.java b/src/main/java/com/assu/server/domain/store/entity/Store.java index 7c460ba..f348d99 100644 --- a/src/main/java/com/assu/server/domain/store/entity/Store.java +++ b/src/main/java/com/assu/server/domain/store/entity/Store.java @@ -24,6 +24,7 @@ public class Store extends BaseEntity { @JoinColumn(name = "partner_id") private Partner partner; + @Setter private Integer rate; @Setter diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java index cfb67c4..2bf0bc0 100644 --- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java @@ -122,4 +122,5 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), s.point) Optional findByPartnerId(Long partnerId); + } From 0eec2a3a378c15a13d1a2862908c2a910158e745 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:21:01 +1000 Subject: [PATCH 232/270] =?UTF-8?q?[REFACTOR/#157]=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EB=A7=8C=EB=93=A0=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=EC=84=9C=EB=8A=94=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/partnership/repository/PaperRepository.java | 3 ++- .../domain/partnership/service/PartnershipServiceImpl.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java index d312543..4efd8c8 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -44,9 +44,10 @@ Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc( left join fetch p.store s where p.isActivated = :status and p.admin.id = :adminId + and p.partner is null order by p.createdAt desc """) - List findAllSuspendedByAdminWithPartner( + List findAllSuspendedByAdminWithNoPartner( @Param("status") ActivationStatus status, @Param("adminId") 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 b189af0..b2c360d 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 @@ -253,7 +253,7 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long pa @Transactional public List getSuspendedPapers(Long adminId) { List suspendedPapers = - paperRepository.findAllSuspendedByAdminWithPartner(ActivationStatus.SUSPEND, adminId); + paperRepository.findAllSuspendedByAdminWithNoPartner(ActivationStatus.SUSPEND, adminId); return suspendedPapers.stream() .map(paper -> PartnershipResponseDTO.SuspendedPaperDTO.builder() From 210383bddd664551f9f95f0bd6f9483a8837c607 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:25:45 +1000 Subject: [PATCH 233/270] =?UTF-8?q?[REFACTOR/#157]=20-=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20store(=3D=20partner=EA=B0=80=20NULL=EC=9D=B8=20?= =?UTF-8?q?=EB=A7=A4=EC=9E=A5)=E2=80=9D=EA=B0=80=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EA=B3=B3=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=EB=90=9C?= =?UTF-8?q?=EB=8B=A4=EB=A9=B4=20=EC=82=AD=EC=A0=9C=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/PaperRepository.java | 13 +++--------- .../service/PartnershipServiceImpl.java | 21 ++++++++++++++----- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java index 4efd8c8..3255685 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -37,16 +37,7 @@ Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc( Optional findTopByAdmin_IdAndPartner_IdAndIsActivatedInOrderByIdDesc(Long adminId, Long partnerId, List statuses); // Admin 기준 (SUSPEND) - @Query(""" -select p -from Paper p -left join fetch p.partner pt -left join fetch p.store s -where p.isActivated = :status - and p.admin.id = :adminId - and p.partner is null -order by p.createdAt desc -""") + @Query(" select p from Paper p left join fetch p.partner pt left join fetch p.store s where p.isActivated = :status and p.admin.id = :adminId and p.partner is null order by p.createdAt desc") List findAllSuspendedByAdminWithNoPartner( @Param("status") ActivationStatus status, @Param("adminId") Long adminId @@ -56,4 +47,6 @@ List findAllSuspendedByAdminWithNoPartner( List findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Sort sort); Page findByPartner_IdAndIsActivated(Long partnerId, ActivationStatus status, Pageable pageable); Optional findTopPaperByStoreId(Long storeId); + long countByStore_Id(Long storeId); + } 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 b2c360d..e791fe5 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 @@ -405,7 +405,7 @@ public void deletePartnership(Long paperId) { Paper paper = paperRepository.findById(paperId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); - // 1. paperContent + goods 삭제 + // 0. paperContent + goods 삭제 List contentsToDelete = paperContentRepository.findByPaperId(paperId); if (contentsToDelete != null && !contentsToDelete.isEmpty()) { List contentIds = contentsToDelete.stream() @@ -416,14 +416,25 @@ public void deletePartnership(Long paperId) { paperContentRepository.deleteAll(contentsToDelete); } + // 1. store 참조를 미리 잡아두기 (paper 삭제 후 사용) + Store store = paper.getStore(); + boolean isTempStore = (store != null && paper.getPartner() == null); + // 2. paper 삭제 paperRepository.delete(paper); - // 3. 임시 store 삭제 (partner가 null인 경우만) - Store store = paper.getStore(); - if (store != null && paper.getPartner() == null) { - storeRepository.delete(store); + // 3) 임시 store 삭제 (재사용 중이면 보존) + if (isTempStore) { + Long storeId = store.getId(); + + // 남은 paper 참조 수 + long remainingPaperRefs = paperRepository.countByStore_Id(storeId); + + if (remainingPaperRefs == 0) { + storeRepository.delete(store); + } } + } @Override From e83537c44535d85a9607ec4a537333ee97249716 Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Thu, 25 Sep 2025 20:49:59 +0900 Subject: [PATCH 234/270] =?UTF-8?q?[REFACTOR/#160]=20=EC=A0=84=ED=99=94?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A4=91=EB=B3=B5=20=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20API=EC=99=80=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A0=84=EC=86=A1=20API=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 33 +++++-------------- .../domain/auth/service/EmailAuthService.java | 8 +++++ ...iceImpl.java => EmailAuthServiceImpl.java} | 18 ++-------- .../domain/auth/service/PhoneAuthService.java | 2 +- .../auth/service/PhoneAuthServiceImpl.java | 10 +++++- .../auth/service/VerificationService.java | 11 ------- .../apiPayload/code/status/ErrorStatus.java | 6 ++-- 7 files changed, 32 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java rename src/main/java/com/assu/server/domain/auth/service/{VerificationServiceImpl.java => EmailAuthServiceImpl.java} (52%) delete mode 100644 src/main/java/com/assu/server/domain/auth/service/VerificationService.java 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 d046fca..35a2744 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 @@ -37,28 +37,29 @@ public class AuthController { private final PhoneAuthService phoneAuthService; + private final EmailAuthService emailAuthService; private final SignUpService signUpService; private final LoginService loginService; private final LogoutService logoutService; private final SSUAuthService ssuAuthService; private final WithdrawalService withdrawalService; - private final VerificationService verificationService; @Operation( - summary = "휴대폰 인증번호 발송 API", - description = "# [v1.0 (2025-09-03)](https://clumsy-seeder-416.notion.site/2241197c19ed801bbcd9f61c3e5f5457?source=copy_link)\n" + + summary = "휴대폰 번호 중복가입 확인 및 인증번호 발송 API", + description = "# [v1.1 (2025-09-25)](https://clumsy-seeder-416.notion.site/2241197c19ed801bbcd9f61c3e5f5457?source=copy_link)\n" + "- 입력한 휴대폰 번호로 1회용 인증번호(OTP)를 발송합니다.\n" + + "- 중복된 전화번호가 있으면 에러를 반환합니다.\n" + "- 유효시간/재요청 제한 정책은 서버 설정에 따릅니다.\n" + "\n**Request Body:**\n" + " - `phoneNumber` (String, required): 인증번호를 받을 휴대폰 번호\n" + "\n**Response:**\n" + " - 성공 시 200(OK)과 성공 메시지 반환" ) - @PostMapping("/phone-verification/send") - public BaseResponse sendAuthNumber( + @PostMapping("/phone-verification/check-and-send") + public BaseResponse checkPhoneAvailabilityAndSendAuthNumber( @RequestBody @Valid PhoneAuthRequestDTO.PhoneAuthSendRequest request ) { - phoneAuthService.sendAuthNumber(request.getPhoneNumber()); + phoneAuthService.checkAndSendAuthNumber(request.getPhoneNumber()); return BaseResponse.onSuccess(SuccessStatus.SEND_AUTH_NUMBER_SUCCESS, null); } @@ -84,23 +85,7 @@ public BaseResponse checkAuthNumber( return BaseResponse.onSuccess(SuccessStatus.VERIFY_AUTH_NUMBER_SUCCESS, null); } - @Operation(summary = "전화번호 중복 체크 API", - description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2551197c19ed808a9757f7f0fc4cf09b?source=copy_link)\n" + - "- 입력한 전화번호가 이미 가입된 사용자가 있는지 확인합니다.\n" + - "- 중복된 전화번호가 있으면 에러를 반환합니다.\n" + - "\n**Request Body:**\n" + - " - `phoneNumber` (String, required): 확인할 전화번호 (010XXXXXXXX 형식)\n" + - "\n**Response:**\n" + - " - 성공 시 200(OK)과 사용 가능 메시지 반환\n" + - " - 중복 시 404(NOT_FOUND)와 에러 메시지 반환") - @PostMapping("/phone-verification/check") - public BaseResponse checkPhoneNumberAvailability( - @RequestBody @Valid VerificationRequestDTO.PhoneVerificationCheckRequest request) { - verificationService.checkPhoneNumberAvailability(request); - return BaseResponse.onSuccess(SuccessStatus._OK, null); - } - - @Operation(summary = "이메일 중복 체크 API", + @Operation(summary = "이메일 형식 및 중복가입 확인 API", description = "# [v1.0 (2025-09-18)](https://clumsy-seeder-416.notion.site/2551197c19ed802d8f6dd373dd045f3a?source=copy_link)\n" + "- 입력한 이메일이 이미 가입된 사용자가 있는지 확인합니다.\n" + "- 중복된 이메일이 있으면 에러를 반환합니다.\n" + @@ -112,7 +97,7 @@ public BaseResponse checkPhoneNumberAvailability( @PostMapping("/email-verification/check") public BaseResponse checkEmailAvailability( @RequestBody @Valid VerificationRequestDTO.EmailVerificationCheckRequest request) { - verificationService.checkEmailAvailability(request); + emailAuthService.checkEmailAvailability(request); return BaseResponse.onSuccess(SuccessStatus._OK, null); } diff --git a/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java b/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java new file mode 100644 index 0000000..c3f9bb0 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/service/EmailAuthService.java @@ -0,0 +1,8 @@ +package com.assu.server.domain.auth.service; + +import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; + +public interface EmailAuthService { + + void checkEmailAvailability(VerificationRequestDTO.EmailVerificationCheckRequest request); +} diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java similarity index 52% rename from src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java rename to src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java index 85cc43e..c852420 100644 --- a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/EmailAuthServiceImpl.java @@ -3,32 +3,18 @@ import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; import com.assu.server.domain.auth.exception.CustomAuthException; import com.assu.server.domain.auth.repository.CommonAuthRepository; -import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class VerificationServiceImpl implements VerificationService { +public class EmailAuthServiceImpl implements EmailAuthService { - private final MemberRepository memberRepository; private final CommonAuthRepository commonAuthRepository; @Override - public void checkPhoneNumberAvailability( - VerificationRequestDTO.PhoneVerificationCheckRequest request) { - - boolean exists = memberRepository.existsByPhoneNum(request.getPhoneNumber()); - - if (exists) { - throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); - } - } - - @Override - public void checkEmailAvailability( - VerificationRequestDTO.EmailVerificationCheckRequest request) { + public void checkEmailAvailability(VerificationRequestDTO.EmailVerificationCheckRequest request) { boolean exists = commonAuthRepository.existsByEmail(request.getEmail()); diff --git a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java index 3b80700..1b345c3 100644 --- a/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java +++ b/src/main/java/com/assu/server/domain/auth/service/PhoneAuthService.java @@ -1,6 +1,6 @@ package com.assu.server.domain.auth.service; public interface PhoneAuthService { - void sendAuthNumber(String phoneNumber); + void checkAndSendAuthNumber(String phoneNumber); void verifyAuthNumber(String phoneNumber, String authNumber); } 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 6db58c4..239650c 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 @@ -1,5 +1,6 @@ package com.assu.server.domain.auth.service; +import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.util.RandomNumberUtil; import com.assu.server.domain.auth.exception.CustomAuthException; @@ -18,11 +19,18 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { private final StringRedisTemplate redisTemplate; private final AligoSmsClient aligoSmsClient; + private final MemberRepository memberRepository; private static final Duration AUTH_CODE_TTL = Duration.ofMinutes(5); // 인증번호 5분 유효 @Override - public void sendAuthNumber(String phoneNumber) { + public void checkAndSendAuthNumber(String phoneNumber) { + boolean exists = memberRepository.existsByPhoneNum(phoneNumber); + + if (exists) { + throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); + } + String authNumber = RandomNumberUtil.generateSixDigit(); redisTemplate.opsForValue().set(phoneNumber, authNumber, AUTH_CODE_TTL); diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java b/src/main/java/com/assu/server/domain/auth/service/VerificationService.java deleted file mode 100644 index aaf2823..0000000 --- a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.assu.server.domain.auth.service; - -import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; - -public interface VerificationService { - void checkPhoneNumberAvailability( - VerificationRequestDTO.PhoneVerificationCheckRequest request); - - void checkEmailAvailability( - VerificationRequestDTO.EmailVerificationCheckRequest request); -} diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index f21a23a..7ca4c04 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -53,9 +53,9 @@ public enum ErrorStatus implements BaseErrorCode { NO_PAPER_FOR_STORE(HttpStatus.NOT_FOUND, "ADMIN_4005", "존재하지 않는 paper ID입니다."), NO_AVAILABLE_PARTNER(HttpStatus.NOT_FOUND, "MEMBER_4009", "제휴업체를 찾을 수 없습니다."), NO_SUCH_STORE_WITH_THAT_PARTNER(HttpStatus.NOT_FOUND,"MEMBER_4006","해당 store ID에 해당하는 partner ID가 존재하지 않습니다."), - EXISTED_PHONE(HttpStatus.NOT_FOUND,"MEMBER_4007","이미 존재하는 전화번호입니다."), - EXISTED_EMAIL(HttpStatus.NOT_FOUND,"MEMBER_4008","이미 존재하는 이메일입니다."), - EXISTED_STUDENT(HttpStatus.NOT_FOUND,"MEMBER_4009","이미 존재하는 학번입니다."), + EXISTED_PHONE(HttpStatus.CONFLICT,"MEMBER_4007","이미 존재하는 전화번호입니다."), + EXISTED_EMAIL(HttpStatus.CONFLICT,"MEMBER_4008","이미 존재하는 이메일입니다."), + EXISTED_STUDENT(HttpStatus.CONFLICT,"MEMBER_4009","이미 존재하는 학번입니다."), MEMBER_ALREADY_WITHDRAWN(HttpStatus.BAD_REQUEST, "MEMBER_4010", "이미 탈퇴된 회원입니다."), From 1b568af47ff51c34062196be46ce61aeaa9aaeef Mon Sep 17 00:00:00 2001 From: 2ghrms Date: Thu, 25 Sep 2025 21:27:17 +0900 Subject: [PATCH 235/270] =?UTF-8?q?[FIX/#160]=20=EC=A0=84=ED=99=94?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A4=91=EB=B3=B5=20=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20API=EC=99=80=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A0=84=EC=86=A1=20API=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20->=20JWT=20=ED=95=84=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/domain/auth/security/jwt/JwtAuthFilter.java | 3 +-- .../java/com/assu/server/global/config/SecurityConfig.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) 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 fd1f998..8f90a3f 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 @@ -49,9 +49,8 @@ public class JwtAuthFilter extends OncePerRequestFilter { "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/swagger-resources/**", "/webjars/**", // Auth (로그아웃/탈퇴/리프레시 제외) - "/auth/phone-verification/send", + "/auth/phone-verification/check-and-send", "/auth/phone-verification/verify", - "/auth/phone-verification/check", "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", 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 450a667..b06c6c8 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -31,9 +31,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF ).permitAll() .requestMatchers(// Auth (로그아웃 제외) - "/auth/phone-verification/send", + "/auth/phone-verification/check-and-send", "/auth/phone-verification/verify", - "/auth/phone-verification/check", "/auth/email-verification/check", "/auth/students/signup", "/auth/partners/signup", From 79364780b964ea00322335b3476cb2efb56de3c1 Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Thu, 25 Sep 2025 23:43:21 +0900 Subject: [PATCH 236/270] =?UTF-8?q?[Feat/#24]=20=20-=20=EC=A0=9C=EC=95=88?= =?UTF-8?q?=EC=84=9C=20=EC=95=8C=EB=A6=BC=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/partnership/service/PartnershipServiceImpl.java | 2 ++ 1 file changed, 2 insertions(+) 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 a7c936a..217ed79 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 @@ -359,6 +359,8 @@ public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(Part Paper draftPaper = PartnershipConverter.toDraftPaperEntity(admin, partner, store); paperRepository.save(draftPaper); + notificationService.sendPartnerProposal(partner.getId(), draftPaper.getId(), admin.getName()); + return PartnershipConverter.toCreateDraftResponseDTO(draftPaper); } From 109d66ae71d42d4904597813e1d2e201074ba31e Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Fri, 26 Sep 2025 14:00:58 +0900 Subject: [PATCH 237/270] =?UTF-8?q?[Refactor/#24]=20=20-=20=EC=A0=9C?= =?UTF-8?q?=ED=9C=B4=20=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20?= =?UTF-8?q?updatedAt=20=EB=B0=9B=EC=95=84=EC=98=A4=EA=B8=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PartnershipController.java | 4 +--- .../converter/PartnershipConverter.java | 17 ++++++++++++++--- .../partnership/dto/PartnershipResponseDTO.java | 10 ++++++++++ .../partnership/service/PartnershipService.java | 6 +----- .../service/PartnershipServiceImpl.java | 5 ++--- 5 files changed, 28 insertions(+), 14 deletions(-) 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 0863097..c8dbd95 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 @@ -8,11 +8,9 @@ import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.notification.service.NotificationCommandService; -import com.assu.server.domain.partnership.dto.PaperResponseDTO; import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; import com.assu.server.domain.partnership.service.PartnershipService; -import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; @@ -110,7 +108,7 @@ public BaseResponse> li description = "제휴 아이디를 입력하세요." ) @GetMapping("/{partnershipId}") - public BaseResponse getPartnership( + public BaseResponse getPartnership( @PathVariable Long partnershipId ) { return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.getPartnership(partnershipId)); diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java index 7823df4..011bfd8 100644 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -18,11 +18,8 @@ import com.assu.server.domain.user.entity.Student; import com.assu.server.domain.store.entity.Store; -import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; public class PartnershipConverter { @@ -292,4 +289,18 @@ public static void updatePaperFromDto(Paper paper, PartnershipRequestDTO.WritePa paper.setPartnershipPeriodEnd(dto.getPartnershipPeriodEnd()); paper.setIsActivated(ActivationStatus.SUSPEND); } + + public static PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartnershipResultDTO( + Paper paper, + List contents, + List> goodsBatches + ) { + PartnershipResponseDTO.WritePartnershipResponseDTO responseInfo = + writePartnershipResultDTO(paper, contents, goodsBatches); + + return PartnershipResponseDTO.GetPartnershipDetailResponseDTO.builder() + .updatedAt(paper.getUpdatedAt()) // UpdatedAt 값 가져오기 + .responseInfo(responseInfo) // 상세정보 DTO 설정 + .build(); + } } diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java index 2229ca2..4e34933 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java @@ -128,4 +128,14 @@ public static class PartnerPartnershipWithAdminResponseDTO { private String adminName; private String adminAddress; } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class GetPartnershipDetailResponseDTO { + private LocalDateTime updatedAt; + private WritePartnershipResponseDTO responseInfo; + } } 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 2e58750..783a57a 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 @@ -2,11 +2,7 @@ import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; -import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; -import com.assu.server.global.util.PrincipalDetails; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -26,7 +22,7 @@ PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership( List listPartnershipsForPartner(boolean all, Long adminId); // 제휴 제안서 조회 - PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId); + PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartnership(Long partnershipId); List getSuspendedPapers(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 d2bbdaa..aa3306a 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 @@ -8,7 +8,6 @@ import org.springframework.stereotype.Service; import com.assu.server.domain.member.entity.Member; -import com.assu.server.domain.notification.repository.NotificationRepository; import com.assu.server.domain.notification.service.NotificationCommandService; import com.assu.server.domain.partnership.converter.PartnershipConverter; import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; @@ -236,7 +235,7 @@ public List listPartnerships @Override @Transactional - public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long partnershipId) { + public PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartnership(Long partnershipId) { Paper paper = paperRepository.findById(partnershipId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); @@ -246,7 +245,7 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO getPartnership(Long pa .map(pc -> pc.getGoods() == null ? Collections.emptyList() : pc.getGoods()) .toList(); - return PartnershipConverter.writePartnershipResultDTO(paper, contents, goodsBatches); + return PartnershipConverter.getPartnershipResultDTO(paper, contents, goodsBatches); } @Override From 4a2f6a14a125a9ce55abb0330d7cd486369b8762 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sun, 28 Sep 2025 01:57:11 +0900 Subject: [PATCH 238/270] =?UTF-8?q?refactor/#38=20-=20=EC=83=81=EB=8C=80?= =?UTF-8?q?=EB=B0=A9=20=EC=B0=A8=EB=8B=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 66 +++++++++- .../domain/chat/converter/BlockConverter.java | 50 +++++++ .../domain/chat/dto/BlockRequestDTO.java | 13 ++ .../domain/chat/dto/BlockResponseDTO.java | 39 ++++++ .../assu/server/domain/chat/entity/Block.java | 31 +++++ .../chat/repository/BlockRepository.java | 16 +++ .../domain/chat/service/BlockService.java | 12 ++ .../domain/chat/service/BlockServiceImpl.java | 124 ++++++++++++++++++ 8 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java create mode 100644 src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java create mode 100644 src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java create mode 100644 src/main/java/com/assu/server/domain/chat/entity/Block.java create mode 100644 src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java create mode 100644 src/main/java/com/assu/server/domain/chat/service/BlockService.java create mode 100644 src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 4f78006..e29d822 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -1,9 +1,8 @@ package com.assu.server.domain.chat.controller; -import com.assu.server.domain.chat.dto.ChatRequestDTO; -import com.assu.server.domain.chat.dto.ChatResponseDTO; -import com.assu.server.domain.chat.dto.ChatRoomUpdateDTO; +import com.assu.server.domain.chat.dto.*; import com.assu.server.domain.chat.repository.MessageRepository; +import com.assu.server.domain.chat.service.BlockService; import com.assu.server.domain.chat.service.ChatService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import com.assu.server.global.util.PresenceTracker; @@ -29,6 +28,7 @@ public class ChatController { private final SimpMessagingTemplate simpMessagingTemplate; private final PresenceTracker presenceTracker; private final MessageRepository messageRepository; + private final BlockService blockService; @Operation( summary = "채팅방을 생성하는 API", @@ -139,4 +139,64 @@ public BaseResponse leaveChattingR Long memberId = pd.getMember().getId(); return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId, memberId)); } + + @Operation( + summary = "상대방을 차단하는 API" + + "상대방을 차단합니다. 메시지를 주고받을 수 없습니다.", + description = "# [v1.0 (2025-09-25)]() 상대방을 차단합니다.\n"+ + "- memberId: Request Body, Long\n" + ) + @PostMapping("/block") + public BaseResponse block( + @AuthenticationPrincipal PrincipalDetails pd, + @RequestBody BlockRequestDTO.BlockMemberRequestDTO request + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.blockMember(memberId, request.getOpponentId())); + } + + @Operation( + summary = "상대방을 차단했는지 확인하는 API" + + "상대방을 차단했는지 여부를 알려줍니다.", + description = "# [v1.0 (2025-09-25)]() 상대방을 차단했는지 검사합니다.\n"+ + "- memberId: Request Body, Long\n" + ) + @GetMapping("/check/block/{opponentId}") + public BaseResponse checkBlock( + @AuthenticationPrincipal PrincipalDetails pd, + @PathVariable Long opponentId + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.checkBlock(memberId, opponentId)); + } + + @Operation( + summary = "상대방을 차단 해제하는 API" + + "상대방을 차단해제합니다. 앞으로 다시 메시지를 주고받을 수 있습니다.", + description = "# [v1.0 (2025-09-25)]() 상대방을 차단 해제합니다.\n"+ + "- memberId: Request Body, Long\n" + ) + @DeleteMapping("/unblock") + public BaseResponse unblock( + @AuthenticationPrincipal PrincipalDetails pd, + @RequestParam Long opponentId + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.unblockMember(memberId, opponentId)); + } + + @Operation( + summary = "차단한 대상을 조회합니다." + + "본인이 차단한 대상을 모두 조회합니다.", + description = "# [v1.0 (2025-09-25)]() 차단한 대상을 조회합니다..\n"+ + "- memberId: Request Body, Long\n" + ) + @GetMapping("/blockList") + public BaseResponse> getBlockList( + @AuthenticationPrincipal PrincipalDetails pd + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.getMyBlockList(memberId)); + } + } diff --git a/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java b/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java new file mode 100644 index 0000000..2ce2c00 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java @@ -0,0 +1,50 @@ +package com.assu.server.domain.chat.converter; + +import com.assu.server.domain.chat.dto.BlockResponseDTO; +import com.assu.server.domain.chat.entity.Block; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; + +import java.util.List; +import java.util.stream.Collectors; + +public class BlockConverter { + public static BlockResponseDTO.BlockMemberDTO toBlockDTO(Long blockedId, String blockedName) { + return BlockResponseDTO.BlockMemberDTO.builder() + .memberId(blockedId) + .name(blockedName) + .build(); + } + + public static BlockResponseDTO.CheckBlockMemberDTO toCheckBlockDTO(Long blockedId, String blockedName, boolean blocked) { + return BlockResponseDTO.CheckBlockMemberDTO.builder() + .memberId(blockedId) + .name(blockedName) + .blocked(blocked) + .build(); + } + + public static BlockResponseDTO.BlockMemberDTO toBlockedMemberDTO(Block block) { + // Block 엔티티에서 차단된 사용자(Member) 정보를 꺼냅니다. + Member blockedMember = block.getBlocked(); + UserRole blockedRole = blockedMember.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blockedMember.getAdminProfile().getName(); + } else { + blockedName = blockedMember.getPartnerProfile().getName(); + } + + return BlockResponseDTO.BlockMemberDTO.builder() + .memberId(blockedMember.getId()) + .name(blockedName) // 또는 getNickname() 등 실제 필드명 사용 + .build(); + } + + public static List toBlockedMemberListDTO(List blockList) { + return blockList.stream() + .map(BlockConverter::toBlockedMemberDTO) // 각 Block 객체에 대해 위 헬퍼 메소드를 호출 + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java new file mode 100644 index 0000000..938b4f8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java @@ -0,0 +1,13 @@ +package com.assu.server.domain.chat.dto; + +import lombok.Getter; +import lombok.Setter; + +public class BlockRequestDTO { + + @Getter + @Setter + public static class BlockMemberRequestDTO { + private Long opponentId; + } +} diff --git a/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java new file mode 100644 index 0000000..597ad69 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java @@ -0,0 +1,39 @@ +package com.assu.server.domain.chat.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +public class BlockResponseDTO { + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BlockMemberDTO { + private Long memberId; + private String name; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class CheckBlockMemberDTO { + private Long memberId; + private String name; + private boolean blocked; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BlockedMemberListDTO { + List blockedMembers; + } + +} diff --git a/src/main/java/com/assu/server/domain/chat/entity/Block.java b/src/main/java/com/assu/server/domain/chat/entity/Block.java new file mode 100644 index 0000000..7aa99d1 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/entity/Block.java @@ -0,0 +1,31 @@ +package com.assu.server.domain.chat.entity; + +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Block extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "blocker_id", nullable = false) + private Member blocker; // 차단한 사람 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "blocked_id", nullable = false) + private Member blocked; // 차단당한 사람 + + @Builder + public Block(Member blocker, Member blocked) { + this.blocker = blocker; + this.blocked = blocked; + } +} diff --git a/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java new file mode 100644 index 0000000..7de62dd --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java @@ -0,0 +1,16 @@ +package com.assu.server.domain.chat.repository; + + +import com.assu.server.domain.chat.entity.Block; +import com.assu.server.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface BlockRepository extends JpaRepository { + boolean existsByBlockerAndBlocked(Member blocker, Member blocked); + + void deleteByBlockerAndBlocked(Member blocker, Member blocked); + + List findByBlocker(Member blocker); +} diff --git a/src/main/java/com/assu/server/domain/chat/service/BlockService.java b/src/main/java/com/assu/server/domain/chat/service/BlockService.java new file mode 100644 index 0000000..27c0ac8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/service/BlockService.java @@ -0,0 +1,12 @@ +package com.assu.server.domain.chat.service; + +import com.assu.server.domain.chat.dto.BlockResponseDTO; + +import java.util.List; + +public interface BlockService { + BlockResponseDTO.BlockMemberDTO blockMember(Long blockerId, Long blockedId); + BlockResponseDTO.CheckBlockMemberDTO checkBlock(Long blockerId, Long blockedId); + BlockResponseDTO.BlockMemberDTO unblockMember(Long blockerId, Long blockedId); + List getMyBlockList(Long blockerId); +} diff --git a/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java new file mode 100644 index 0000000..abb9422 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java @@ -0,0 +1,124 @@ +package com.assu.server.domain.chat.service; + +import com.assu.server.domain.chat.converter.BlockConverter; +import com.assu.server.domain.chat.converter.ChatConverter; +import com.assu.server.domain.chat.dto.BlockResponseDTO; +import com.assu.server.domain.chat.entity.Block; +import com.assu.server.domain.chat.repository.BlockRepository; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.member.repository.MemberRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.GeneralException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BlockServiceImpl implements BlockService { + private final BlockRepository blockRepository; + private final MemberRepository memberRepository; + + @Transactional + @Override + public BlockResponseDTO.BlockMemberDTO blockMember(Long blockerId, Long blockedId) { + if (blockerId.equals(blockedId)) { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + Member blocked = memberRepository.findById(blockedId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + + + // 이미 차단했는지 확인 + if (blockRepository.existsByBlockerAndBlocked(blocker, blocked)) { + // 이미 차단한 경우, 아무것도 하지 않거나 예외 처리 + return null; + } + + UserRole blockedRole = blocked.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blocked.getAdminProfile().getName(); + } else if (blockedRole == UserRole.PARTNER) { + blockedName = blocked.getPartnerProfile().getName(); + } else { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + Block block = Block.builder() + .blocker(blocker) + .blocked(blocked) + .build(); + + blockRepository.save(block); + + return BlockConverter.toBlockDTO(blockedId, blockedName); + } + + @Override + public BlockResponseDTO.CheckBlockMemberDTO checkBlock(Long blockerId, Long blockedId) { + + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + Member blocked = memberRepository.findById(blockedId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + + UserRole blockedRole = blocked.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blocked.getAdminProfile().getName(); + } else if (blockedRole == UserRole.PARTNER) { + blockedName = blocked.getPartnerProfile().getName(); + } else { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + if (blockRepository.existsByBlockerAndBlocked(blocker, blocked)) { + return BlockConverter.toCheckBlockDTO(blockedId, blockedName, true); + } + else { + return BlockConverter.toCheckBlockDTO(blockedId, blockedName, false); + } + } + + @Transactional + @Override + public BlockResponseDTO.BlockMemberDTO unblockMember(Long blockerId, Long blockedId) { + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)); + Member blocked = memberRepository.findById(blockedId) + .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)); + + UserRole blockedRole = blocked.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blocked.getAdminProfile().getName(); + } else if (blockedRole == UserRole.PARTNER) { + blockedName = blocked.getPartnerProfile().getName(); + } else { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + // Transactional 환경에서는 Dirty-checking으로 delete 쿼리가 나갑니다. + blockRepository.deleteByBlockerAndBlocked(blocker, blocked); + return BlockConverter.toBlockDTO(blockedId, blockedName); + } + + @Transactional + @Override + public List getMyBlockList(Long blockerId) { + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + + List blockList = blockRepository.findByBlocker(blocker); + + return BlockConverter.toBlockedMemberListDTO(blockList); + } +} From 6a3701212e7e1a98b3573d6c4d7e936d2b511e40 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sun, 28 Sep 2025 02:37:03 +0900 Subject: [PATCH 239/270] =?UTF-8?q?refactor/#38=20-=20=EC=B0=A8=EB=8B=A8?= =?UTF-8?q?=20=EC=8B=9C=20=EC=83=81=EB=8C=80=EB=B0=A9=EB=8F=84=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EB=A5=BC=20=EB=B3=B4=EB=82=BC=20=EC=88=98=20?= =?UTF-8?q?=EC=97=86=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/chat/repository/BlockRepository.java | 9 +++++++++ .../server/domain/chat/service/BlockServiceImpl.java | 2 +- .../assu/server/domain/chat/service/ChatServiceImpl.java | 6 ++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java index 7de62dd..3a69e2c 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java @@ -4,6 +4,8 @@ import com.assu.server.domain.chat.entity.Block; import com.assu.server.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; @@ -13,4 +15,11 @@ public interface BlockRepository extends JpaRepository { void deleteByBlockerAndBlocked(Member blocker, Member blocked); List findByBlocker(Member blocker); + + // BlockRepository.java + @Query("SELECT COUNT(b) > 0 FROM Block b " + + "WHERE (b.blocker = :user1 AND b.blocked = :user2) " + + "OR (b.blocker = :user2 AND b.blocked = :user1)") + boolean existsBlockRelationBetween(@Param("user1") Member user1, @Param("user2") Member user2); + } diff --git a/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java index abb9422..b9edc56 100644 --- a/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java @@ -80,7 +80,7 @@ public BlockResponseDTO.CheckBlockMemberDTO checkBlock(Long blockerId, Long bloc throw new GeneralException(ErrorStatus._BAD_REQUEST); } - if (blockRepository.existsByBlockerAndBlocked(blocker, blocked)) { + if (blockRepository.existsBlockRelationBetween(blocker, blocked)) { return BlockConverter.toCheckBlockDTO(blockedId, blockedName, true); } else { diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 62feb6d..084a963 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -9,6 +9,7 @@ import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.chat.entity.Message; +import com.assu.server.domain.chat.repository.BlockRepository; import com.assu.server.domain.chat.repository.ChatRepository; import com.assu.server.domain.chat.repository.MessageRepository; import com.assu.server.domain.member.entity.Member; @@ -38,6 +39,7 @@ public class ChatServiceImpl implements ChatService { private final AdminRepository adminRepository; private final MessageRepository messageRepository; private final StoreRepository storeRepository; + private final BlockRepository blockRepository; @Override @@ -95,8 +97,8 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); - boolean exists = messageRepository.existsById(saved.getId()); - log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제 +// boolean exists = messageRepository.existsById(saved.getId()); +// log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제 return ChatConverter.toSendMessageDTO(saved); } From 127c9b05f28e4d1a446e6929b4ffaf0d3bc12832 Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Sun, 28 Sep 2025 16:41:51 +0900 Subject: [PATCH 240/270] =?UTF-8?q?[Refactor/#169]=20-=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A1=B0=ED=9A=8C=20Response=20DTO=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=88=98=EC=A0=95=20-=20Goods=EA=B0=80=20BaseEntit?= =?UTF-8?q?y=20=EC=83=81=EC=86=8D=EB=B0=9B=EC=95=84=20updatedAt=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=A5=BC=20=EA=B0=96=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20paper,=20paperContent,=20Goods=20?= =?UTF-8?q?=EC=A4=91=20=EA=B0=80=EC=9E=A5=20=EC=B5=9C=EA=B7=BC=EC=97=90=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=90=9C=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=EC=9D=98=20updatedAt=EC=9D=84=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../converter/PartnershipConverter.java | 59 ++++++++++++++++--- .../dto/PartnershipResponseDTO.java | 9 ++- .../domain/partnership/entity/Goods.java | 3 +- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java index 011bfd8..4adbe94 100644 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -1,8 +1,10 @@ package com.assu.server.domain.partnership.converter; import java.time.LocalDate; -import java.util.List; +import java.time.LocalDateTime; +import java.util.*; +import com.assu.server.domain.common.entity.BaseEntity; import com.assu.server.domain.partnership.dto.PaperContentResponseDTO; import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.common.enums.ActivationStatus; @@ -18,9 +20,6 @@ import com.assu.server.domain.user.entity.Student; import com.assu.server.domain.store.entity.Store; -import java.util.ArrayList; -import java.util.Collections; - public class PartnershipConverter { public static PartnershipUsage toPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Student student, Long paperId) { @@ -295,12 +294,56 @@ public static PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartners List contents, List> goodsBatches ) { - PartnershipResponseDTO.WritePartnershipResponseDTO responseInfo = - writePartnershipResultDTO(paper, contents, goodsBatches); + List allTimestamps = new ArrayList<>(); + + if (paper.getUpdatedAt() != null) allTimestamps.add(paper.getUpdatedAt()); + if (contents != null) { + contents.stream() + .map(BaseEntity::getUpdatedAt) + .filter(Objects::nonNull) + .forEach(allTimestamps::add); + } + if (goodsBatches != null) { + goodsBatches.stream() + .flatMap(List::stream) + .map(BaseEntity::getUpdatedAt) + .filter(Objects::nonNull) + .forEach(allTimestamps::add); + } + + LocalDateTime mostRecentUpdatedAt = allTimestamps.stream() + .max(Comparator.naturalOrder()) + .orElse(paper.getUpdatedAt()); + + List optionDTOS = new ArrayList<>(); + if (contents != null) { + for (int i = 0; i < contents.size(); i++) { + PaperContent pc = contents.get(i); + List goods = (goodsBatches != null && goodsBatches.size() > i) + ? goodsBatches.get(i) : List.of(); + optionDTOS.add( + PartnershipResponseDTO.PartnershipOptionResponseDTO.builder() + .optionType(pc.getOptionType()) + .criterionType(pc.getCriterionType()) + .people(pc.getPeople()) + .cost(pc.getCost()) + .category(pc.getCategory()) + .discountRate(pc.getDiscount()) + .goods(goodsResultDTO(goods)) + .build() + ); + } + } return PartnershipResponseDTO.GetPartnershipDetailResponseDTO.builder() - .updatedAt(paper.getUpdatedAt()) // UpdatedAt 값 가져오기 - .responseInfo(responseInfo) // 상세정보 DTO 설정 + .partnershipId(paper.getId()) + .updatedAt(mostRecentUpdatedAt) // 가장 최근 UpdatedAt 값 가져오기 + .partnershipPeriodStart(paper.getPartnershipPeriodStart()) + .partnershipPeriodEnd(paper.getPartnershipPeriodEnd()) + .adminId(paper.getAdmin() != null ? paper.getAdmin().getId() : null) + .partnerId(paper.getPartner()!= null ? paper.getPartner().getId() : null) // 수동등록이면 null + .storeId(paper.getStore() != null ? paper.getStore().getId() : null) + .options(optionDTOS) .build(); } } diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java index 4e34933..35ac7ae 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java @@ -26,7 +26,6 @@ public static class WritePartnershipResponseDTO { private Long storeId; private String storeName; private String adminName; - private Boolean activated; private ActivationStatus isActivated; private List options; } @@ -135,7 +134,13 @@ public static class PartnerPartnershipWithAdminResponseDTO { @AllArgsConstructor @Builder public static class GetPartnershipDetailResponseDTO { + private Long partnershipId; private LocalDateTime updatedAt; - private WritePartnershipResponseDTO responseInfo; + private LocalDate partnershipPeriodStart; + private LocalDate partnershipPeriodEnd; + private Long adminId; + private Long partnerId; + private Long storeId; + private List options; } } diff --git a/src/main/java/com/assu/server/domain/partnership/entity/Goods.java b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java index c63571d..9a2da2a 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/Goods.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/Goods.java @@ -1,5 +1,6 @@ package com.assu.server.domain.partnership.entity; +import com.assu.server.domain.common.entity.BaseEntity; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -19,7 +20,7 @@ @NoArgsConstructor @Builder @AllArgsConstructor -public class Goods { +public class Goods extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; From 169fa71204fb64a7c9d5e6ff15d3e288a16a49db Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sun, 28 Sep 2025 23:52:03 +1000 Subject: [PATCH 241/270] =?UTF-8?q?[REFACTOR/#157]=20-=20=EC=A0=84?= =?UTF-8?q?=ED=99=94=EB=B2=88=ED=98=B8=20=EC=9D=91=EB=8B=B5=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/assu/server/domain/admin/dto/AdminResponseDTO.java | 1 + .../assu/server/domain/admin/service/AdminServiceImpl.java | 1 + .../java/com/assu/server/domain/map/dto/MapResponseDTO.java | 3 +++ .../com/assu/server/domain/map/service/MapServiceImpl.java | 6 ++++++ .../assu/server/domain/partner/dto/PartnerResponseDTO.java | 1 + .../server/domain/partner/service/PartnerServiceImpl.java | 1 + 6 files changed, 13 insertions(+) diff --git a/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java b/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java index 13d0a26..6aa0df1 100644 --- a/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java +++ b/src/main/java/com/assu/server/domain/admin/dto/AdminResponseDTO.java @@ -17,5 +17,6 @@ public static class RandomPartnerResponseDTO { private String partnerDetailAddress; private String partnerName; private String partnerUrl; + private String partnerPhone; } } diff --git a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java index f0c5208..cfcdb41 100644 --- a/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/admin/service/AdminServiceImpl.java @@ -58,6 +58,7 @@ public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long admin .partnerAddress(picked.getAddress()) .partnerDetailAddress(picked.getDetailAddress()) .partnerUrl(picked.getMember().getProfileUrl()) + .partnerPhone(picked.getMember().getPhoneNum()) .build(); } diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java index 8fc2b55..204352d 100644 --- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java +++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java @@ -24,6 +24,7 @@ public static class PartnerMapResponseDTO { private Double latitude; private Double longitude; private String profileUrl; + private String phoneNumber; } @Getter @@ -41,6 +42,7 @@ public static class AdminMapResponseDTO { private Double latitude; private Double longitude; private String profileUrl; + private String phoneNumber; } @Getter @@ -64,6 +66,7 @@ public static class StoreMapResponseDTO { private Double latitude; private Double longitude; private String profileUrl; + private String phoneNumber; } diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java index eeeee0c..0647116 100644 --- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java +++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java @@ -73,6 +73,7 @@ public List getPartners(MapRequestDTO.View .latitude(p.getLatitude()) .longitude(p.getLongitude()) .profileUrl(url) + .phoneNumber(p.getMember().getPhoneNum()) .build(); }).toList(); } @@ -100,6 +101,7 @@ public List getAdmins(MapRequestDTO.ViewOnMa .latitude(a.getLatitude()) .longitude(a.getLongitude()) .profileUrl(url) + .phoneNumber(a.getMember().getPhoneNum()) .build(); }).toList(); } @@ -160,6 +162,7 @@ public List getStores(MapRequestDTO.ViewOnMa .latitude(s.getLatitude()) .longitude(s.getLongitude()) .profileUrl(profileUrl) + .phoneNumber(s.getPartner().getMember().getPhoneNum()) .build(); }).toList(); } @@ -220,6 +223,7 @@ else if (content.getOptionType() == OptionType.SERVICE) { .latitude(s.getLatitude()) .longitude(s.getLongitude()) .profileUrl(url) + .phoneNumber(s.getPartner().getMember().getPhoneNum()) .build(); }).toList(); } @@ -247,6 +251,7 @@ public List searchPartner(String keyword, .latitude(p.getLatitude()) .longitude(p.getLongitude()) .profileUrl(url) + .phoneNumber(p.getMember().getPhoneNum()) .build(); }).toList(); } @@ -274,6 +279,7 @@ public List searchAdmin(String keyword, Long .latitude(a.getLatitude()) .longitude(a.getLongitude()) .profileUrl(url) + .phoneNumber(a.getMember().getPhoneNum()) .build(); }).toList(); } diff --git a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java index 82254bd..cdc2f2a 100644 --- a/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partner/dto/PartnerResponseDTO.java @@ -27,5 +27,6 @@ public static class AdminLiteDTO { private String adminDetailAddress; private String adminName; private String adminUrl; + private String adminPhone; } } diff --git a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java index 265c0ea..f865fc2 100644 --- a/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java +++ b/src/main/java/com/assu/server/domain/partner/service/PartnerServiceImpl.java @@ -48,6 +48,7 @@ public PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(Long partnerId) .adminDetailAddress(a.getDetailAddress()) .adminName(a.getName()) .adminUrl(a.getMember().getProfileUrl()) + .adminPhone(a.getMember().getPhoneNum()) .build()) .collect(Collectors.toList()); From ececc1fa63814e397bdb68ae0a94c612f184dfc1 Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Mon, 29 Sep 2025 00:57:34 +0900 Subject: [PATCH 242/270] =?UTF-8?q?[Refactor/#169]=20-=20admin=EC=9D=B4=20?= =?UTF-8?q?BLANK=EC=9D=98=20=ED=8C=8C=ED=8A=B8=EB=84=88=EC=9D=98=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=ED=99=95=EC=9D=B8=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/partnership/service/PartnershipServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aa3306a..0a744e7 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 @@ -445,7 +445,7 @@ public PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartne Partner partner = partnerRepository.findById(partnerId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); - List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND); + List targetStatuses = List.of(ActivationStatus.ACTIVE, ActivationStatus.SUSPEND, ActivationStatus.BLANK); boolean isPartnered = paperRepository.existsByAdmin_IdAndPartner_IdAndIsActivatedIn(adminId, partnerId, targetStatuses); Long paperId = null; From 78edd7e0f9633d71cff260da59e7d56518222852 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Tue, 30 Sep 2025 00:32:33 +0900 Subject: [PATCH 243/270] =?UTF-8?q?refactor/#38=20-=20=EC=B0=A8=EB=8B=A8?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=97=90=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/chat/converter/BlockConverter.java | 1 + .../assu/server/domain/chat/dto/BlockResponseDTO.java | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java b/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java index 2ce2c00..aea2beb 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java @@ -38,6 +38,7 @@ public static BlockResponseDTO.BlockMemberDTO toBlockedMemberDTO(Block block) { return BlockResponseDTO.BlockMemberDTO.builder() .memberId(blockedMember.getId()) .name(blockedName) // 또는 getNickname() 등 실제 필드명 사용 + .blockDate(block.getCreatedAt()) .build(); } diff --git a/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java index 597ad69..e13d1db 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java @@ -5,7 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; +import java.time.LocalDateTime; public class BlockResponseDTO { @@ -16,6 +16,7 @@ public class BlockResponseDTO { public static class BlockMemberDTO { private Long memberId; private String name; + private LocalDateTime blockDate; } @Getter @@ -28,12 +29,4 @@ public static class CheckBlockMemberDTO { private boolean blocked; } - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class BlockedMemberListDTO { - List blockedMembers; - } - } From e5065fdf9842ad0b2d01b7c5382257067164dd31 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Tue, 30 Sep 2025 23:47:09 +0900 Subject: [PATCH 244/270] =?UTF-8?q?refactor/#38=20-=20=EC=A0=84=ED=99=94?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/domain/chat/converter/ChatConverter.java | 1 + .../server/domain/chat/dto/ChatRoomListResultDTO.java | 1 + .../server/domain/chat/repository/ChatRepository.java | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index b0b7877..3144f7e 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -26,6 +26,7 @@ public static ChatRoomListResultDTO toChatRoomResultDTO(ChatRoomListResultDTO re .opponentId(request.getOpponentId()) .opponentName(request.getOpponentName()) .opponentProfileImage(request.getOpponentProfileImage()) + .phoneNumber(request.getPhoneNumber()) .build(); } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java index bfeb0b8..4e57cb8 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRoomListResultDTO.java @@ -19,4 +19,5 @@ public class ChatRoomListResultDTO { private Long opponentId; private String opponentName; private String opponentProfileImage; + private String phoneNumber; } diff --git a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java index 8cdeb46..6579a90 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java @@ -46,7 +46,13 @@ SELECT MAX(m2.createdAt) WHEN am.id IS NULL AND pm.id = :memberId THEN -1 WHEN pm.id = :memberId THEN am.profileUrl ELSE pm.profileUrl - END + END, + CASE + WHEN pm.id IS NULL AND am.id = :memberId THEN '-1' + WHEN am.id IS NULL AND pm.id = :memberId THEN '-1' + WHEN pm.id = :memberId THEN am.phoneNum + ELSE pm.phoneNum + END ) FROM ChattingRoom r LEFT JOIN r.partner p From 1d69852e2ecd1e53466a9d752778927bbea30b9c Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:07:39 +1000 Subject: [PATCH 245/270] =?UTF-8?q?[REFACTOR/#181]=20-=20RabbitMQ=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationListener.java | 26 ++++++------------- src/main/resources/application.yml | 16 ++++++------ 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java index 07e1973..841f896 100644 --- a/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java @@ -23,28 +23,19 @@ public class NotificationListener { private final FcmClient fcmClient; private final OutboxStatusService outboxStatus; // ← 주입 - @RabbitListener(queues = AmqpConfig.QUEUE) + @RabbitListener(queues = AmqpConfig.QUEUE, ackMode = "MANUAL") public void onMessage(@Payload NotificationMessageDTO payload, Channel ch, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception { try { - fcmClient.sendToMemberId( - payload.getReceiverId(), - payload.getTitle(), - payload.getBody(), - payload.getData() - ); - ch.basicAck(tag, false); - - // idempotencyKey = outboxId 로 보냈으니 그대로 사용 + fcmClient.sendToMemberId(payload.getReceiverId(), payload.getTitle(), payload.getBody(), payload.getData()); Long outboxId = Long.valueOf(payload.getIdempotencyKey()); - outboxStatus.markSent(outboxId); // 새 트랜잭션에서 SENT 전이 + outboxStatus.markSent(outboxId); // ★ ACK 전에 완료 표시 + + ch.basicAck(tag, false); // ★ 맨 마지막에 단 한 번만 ACK } catch (RuntimeException e) { - if (isTransient(e)) { - ch.basicNack(tag, false, true); - } else { - ch.basicNack(tag, false, false); - } + if (isTransient(e)) ch.basicNack(tag, false, true); + else ch.basicNack(tag, false, false); } catch (Exception e) { ch.basicNack(tag, false, false); } @@ -61,5 +52,4 @@ private boolean isTransient(Throwable t) { } return false; } -} - +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 61918f5..4c10342 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,14 +19,14 @@ spring: highlight_sql : true lifecycle: timeout-per-shutdown-phase: 30s - rabbitmq: - listener: - simple: - acknowledge-mode: manual - prefetch: 20 - concurrency: 1 - max-concurrency: 4 - default-requeue-rejected: false + rabbitmq: + listener: + simple: + acknowledge-mode: manual + prefetch: 20 + concurrency: 1 + max-concurrency: 4 + default-requeue-rejected: false logging: level: From bd16694423af2d4900e5e8469a7e73fe9b650c1e Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:57:23 +1000 Subject: [PATCH 246/270] =?UTF-8?q?[REFACTOR/#181]=20-=20=EB=94=94?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=EC=8A=A4=20=ED=86=A0=ED=81=B0=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/DeviceTokenRepository.java | 7 + .../NotificationOutboxRepository.java | 11 ++ .../service/NotificationListener.java | 88 +++++++--- .../service/OutboxStatusService.java | 13 ++ .../assu/server/infra/firebase/FcmClient.java | 152 +++++++++++++----- 5 files changed, 207 insertions(+), 64 deletions(-) diff --git a/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java index 6720d39..ee0c834 100644 --- a/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java +++ b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java @@ -2,8 +2,10 @@ import com.assu.server.domain.deviceToken.entity.DeviceToken; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -12,5 +14,10 @@ public interface DeviceTokenRepository extends JpaRepository @Query("select dt.token from DeviceToken dt where dt.member.id =:memberId and dt.active=true") List findActiveTokensByMemberId(@Param("memberId") Long memberId); + @Transactional + @Modifying + @Query("update DeviceToken dt set dt.active = false where dt.token in :tokens") + void deactivateTokens(@Param("tokens") List tokens); + Optional findByToken(String token); } diff --git a/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java b/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java index 9894859..abbcfa1 100644 --- a/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java +++ b/src/main/java/com/assu/server/domain/notification/repository/NotificationOutboxRepository.java @@ -27,5 +27,16 @@ public interface NotificationOutboxRepository extends JpaRepository com.assu.server.domain.notification.entity.NotificationOutbox.Status.FAILED + """) + int markFailedById(@org.springframework.data.repository.query.Param("id") Long id); + + boolean existsByIdAndStatus(Long id, NotificationOutbox.Status status); + List findTop50ByStatusOrderByIdAsc(NotificationOutbox.Status status); } diff --git a/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java index 841f896..30d40a1 100644 --- a/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java +++ b/src/main/java/com/assu/server/domain/notification/service/NotificationListener.java @@ -3,53 +3,99 @@ import com.assu.server.infra.firebase.AmqpConfig; import com.assu.server.infra.firebase.FcmClient; import com.assu.server.domain.notification.dto.NotificationMessageDTO; - +import com.google.firebase.messaging.FirebaseMessagingException; import com.rabbitmq.client.Channel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; - @Slf4j @Component @RequiredArgsConstructor public class NotificationListener { private final FcmClient fcmClient; - private final OutboxStatusService outboxStatus; // ← 주입 + private final OutboxStatusService outboxStatus; @RabbitListener(queues = AmqpConfig.QUEUE, ackMode = "MANUAL") public void onMessage(@Payload NotificationMessageDTO payload, Channel ch, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception { + + final Long outboxId = safeParseLong(payload.getIdempotencyKey()); + try { - fcmClient.sendToMemberId(payload.getReceiverId(), payload.getTitle(), payload.getBody(), payload.getData()); - Long outboxId = Long.valueOf(payload.getIdempotencyKey()); - outboxStatus.markSent(outboxId); // ★ ACK 전에 완료 표시 - - ch.basicAck(tag, false); // ★ 맨 마지막에 단 한 번만 ACK - } catch (RuntimeException e) { - if (isTransient(e)) ch.basicNack(tag, false, true); - else ch.basicNack(tag, false, false); + // 0) Outbox 선확인: 이미 처리되었으면 중복 전송/SELECT 자체를 스킵 + if (outboxId != null && outboxStatus.isAlreadySent(outboxId)) { + log.debug("[Notify] already-sent outboxId={}, ACK", outboxId); + ch.basicAck(tag, false); + return; + } + + // 1) 전송 + FcmClient.FcmResult result = fcmClient.sendToMemberId( + payload.getReceiverId(), payload.getTitle(), payload.getBody(), payload.getData()); + + // 2) 성공 처리 + if (outboxId != null) outboxStatus.markSent(outboxId); + ch.basicAck(tag, false); + + // 3) 관측용 로그 + log.info("[Notify] sent outboxId={} memberId={} success={} fail={} invalidTokens={}", + outboxId, payload.getReceiverId(), + result.successCount(), result.failureCount(), result.invalidTokens()); + + } catch (FirebaseMessagingException fme) { + boolean permanent = isPermanent(fme); + log.error("[Notify] FCM failure outboxId={} memberId={} permanent={} http={} code={} root={}", + outboxId, payload.getReceiverId(), permanent, + FcmClient.httpStatusOf(fme), fme.getMessagingErrorCode(), rootSummary(fme), fme); + + if (outboxId != null) outboxStatus.markFailed(outboxId); + ch.basicNack(tag, false, false); // requeue 금지(지연 재시도 큐 권장) + + } catch (java.net.UnknownHostException | javax.net.ssl.SSLHandshakeException e) { + // 환경 문제(DNS/CA)는 영구 취급(루프 방지) + log.error("[Notify] ENV failure outboxId={} memberId={} root={}", + outboxId, payload.getReceiverId(), rootSummary(e), e); + if (outboxId != null) outboxStatus.markFailed(outboxId); + ch.basicNack(tag, false, false); + + } catch (java.util.concurrent.TimeoutException | java.net.SocketTimeoutException e) { + // 타임아웃 → 일시 장애. 그래도 requeue(true)는 쓰지 않음 + log.warn("[Notify] TIMEOUT outboxId={} memberId={} root={}", + outboxId, payload.getReceiverId(), rootSummary(e), e); + if (outboxId != null) outboxStatus.markFailed(outboxId); + ch.basicNack(tag, false, false); + } catch (Exception e) { + // 알 수 없는 오류 → DLQ + log.error("[Notify] UNKNOWN failure outboxId={} memberId={} root={}", + outboxId, payload.getReceiverId(), rootSummary(e), e); + if (outboxId != null) outboxStatus.markFailed(outboxId); ch.basicNack(tag, false, false); } } - private boolean isTransient(Throwable t) { - while (t != null) { - if (t instanceof java.util.concurrent.TimeoutException - || t instanceof java.net.SocketTimeoutException - || t instanceof java.io.IOException) { - return true; - } - t = t.getCause(); - } + private boolean isPermanent(FirebaseMessagingException fme) { + var code = fme.getMessagingErrorCode(); + Integer http = FcmClient.httpStatusOf(fme); + if (code == com.google.firebase.messaging.MessagingErrorCode.UNREGISTERED + || code == com.google.firebase.messaging.MessagingErrorCode.INVALID_ARGUMENT) return true; + if (http != null && (http == 401 || http == 403)) return true; return false; } + + private String rootSummary(Throwable t) { + Throwable r = t; while (r.getCause() != null) r = r.getCause(); + return r.getClass().getName() + ": " + String.valueOf(r.getMessage()); + } + + private Long safeParseLong(String s) { + try { return s == null ? null : Long.valueOf(s); } catch (Exception ignore) { return null; } + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java b/src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java index 1341220..80e4778 100644 --- a/src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java +++ b/src/main/java/com/assu/server/domain/notification/service/OutboxStatusService.java @@ -1,5 +1,6 @@ package com.assu.server.domain.notification.service; +import com.assu.server.domain.notification.entity.NotificationOutbox; import com.assu.server.domain.notification.repository.NotificationOutboxRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,4 +25,16 @@ public void markSent(Long id) { int updated = repo.markSentById(id); log.info("[OutboxStatus] SENT updated={} outboxId={}", updated, id); } + + @Transactional(readOnly = true) + public boolean isAlreadySent(Long id) { + return repo.existsByIdAndStatus(id, NotificationOutbox.Status.SENT); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void markFailed(Long id) { + int updated = repo.markFailedById(id); + log.info("[OutboxStatus] FAILED updated={} outboxId={}", updated, id); + } + } diff --git a/src/main/java/com/assu/server/infra/firebase/FcmClient.java b/src/main/java/com/assu/server/infra/firebase/FcmClient.java index 81fb040..78c3942 100644 --- a/src/main/java/com/assu/server/infra/firebase/FcmClient.java +++ b/src/main/java/com/assu/server/infra/firebase/FcmClient.java @@ -2,16 +2,16 @@ import com.assu.server.domain.deviceToken.repository.DeviceTokenRepository; import com.google.api.core.ApiFuture; -import com.google.firebase.messaging.AndroidConfig; -import com.google.firebase.messaging.FirebaseMessaging; -import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -19,58 +19,124 @@ @Component @RequiredArgsConstructor public class FcmClient { + private final FirebaseMessaging messaging; private final DeviceTokenRepository tokenRepo; - // 전송 타임아웃 - private static final Duration SEND_TIMEOUT = Duration.ofSeconds(3); + // 운영에서 3s는 다소 공격적 — 5s 정도 권장 + private static final Duration SEND_TIMEOUT = Duration.ofSeconds(5); - public void sendToMemberId(Long memberId, String title, String body, Map data) { - if (memberId == null) { - throw new IllegalArgumentException("receiverId is null"); - } + /** + * 멤버의 활성 토큰 전체에 멀티캐스트 전송. + * - 실패 토큰(UNREGISTERED/INVALID_ARGUMENT)은 즉시 비활성화 + * - 결과 요약을 반환 + */ + public FcmResult sendToMemberId(Long memberId, String title, String body, Map data) + throws TimeoutException, InterruptedException, FirebaseMessagingException, ExecutionException { + if (memberId == null) throw new IllegalArgumentException("receiverId is null"); // 1) 토큰 조회 List tokens = tokenRepo.findActiveTokensByMemberId(memberId); - if (tokens.isEmpty()) return; + if (tokens == null || tokens.isEmpty()) { + return FcmResult.empty(); + } - // 2) 널 세이프 처리 + // 2) 널 세이프 final String _title = title == null ? "" : title; final String _body = body == null ? "" : body; - String type = (data != null && data.get("type") != null) ? data.get("type") : ""; - String refId = (data != null && data.get("refId") != null) ? data.get("refId") : ""; - String deeplink = (data != null && data.get("deeplink") != null) ? data.get("deeplink") : ""; - String notificationId = (data != null && data.get("notificationId") != null) ? data.get("notificationId") : ""; - - // 3) 각 토큰에 FCM 전송 (data-only + HIGH) - for (String token : tokens) { - Message msg = Message.builder() - .setToken(token) - .setAndroidConfig(AndroidConfig.builder() - .setPriority(AndroidConfig.Priority.HIGH) - // 필요 시 TTL 등 추가 가능 - // .setTtl(10_000L) // 10초 - .build()) - // --- data-only payload --- - .putData("title", _title) - .putData("body", _body) - .putData("type", type) - .putData("refId", refId) - .putData("deeplink", deeplink) - .putData("notificationId", notificationId) - .build(); - - try { - ApiFuture future = messaging.sendAsync(msg); - String messageId = future.get(SEND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); - log.debug("[FCM] sent messageId={} memberId={}", messageId, memberId); - } catch (TimeoutException te) { - log.warn("[FCM] timeout ({} ms) memberId={}", SEND_TIMEOUT.toMillis(), memberId); - throw new RuntimeException("FCM timeout", te); - } catch (Exception e) { - throw new RuntimeException("FCM unexpected error", e); + String type = data != null ? data.getOrDefault("type", "") : ""; + String refId = data != null ? data.getOrDefault("refId", "") : ""; + String deeplink = data != null ? data.getOrDefault("deeplink", "") : ""; + String notificationId = data != null ? data.getOrDefault("notificationId", "") : ""; + + // 3) 멀티캐스트 메시지 구성 (data-only + HIGH) + com.google.firebase.messaging.MulticastMessage msg = + com.google.firebase.messaging.MulticastMessage.builder() + .addAllTokens(tokens) + .setAndroidConfig(AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .build()) + .putData("title", _title) + .putData("body", _body) + .putData("type", type) + .putData("refId", refId) + .putData("deeplink", deeplink) + .putData("notificationId", notificationId) + .build(); + + try { + ApiFuture future = messaging.sendEachForMulticastAsync(msg); + BatchResponse br = future.get(SEND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + + int success = 0, fail = 0; + List invalidTokens = new ArrayList<>(); + + List responses = br.getResponses(); + for (int i = 0; i < responses.size(); i++) { + SendResponse r = responses.get(i); + if (r.isSuccessful()) { + success++; + } else { + fail++; + FirebaseMessagingException fme = r.getException(); // per-token 예외 + if (fme != null && ( + fme.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED || + fme.getMessagingErrorCode() == MessagingErrorCode.INVALID_ARGUMENT)) { + invalidTokens.add(tokens.get(i)); + } + log.warn("[FCM] per-token fail memberId={} idx={} code={} root={}", + memberId, i, + (fme != null ? fme.getMessagingErrorCode() : null), + rootSummary(fme)); + } + } + + if (!invalidTokens.isEmpty()) { + try { + tokenRepo.deactivateTokens(invalidTokens); // UPDATE ... SET active=0 WHERE token IN (...) + } catch (Exception e) { + log.error("[FCM] deactivateTokens failed size={} memberId={} root={}", + invalidTokens.size(), memberId, rootSummary(e), e); + } + } + + return new FcmResult(success, fail, invalidTokens); + + } catch (TimeoutException te) { + log.warn("[FCM] timeout ({} ms) memberId={}", SEND_TIMEOUT.toMillis(), memberId); + throw te; + + } catch (ExecutionException ee) { + // ★ 핵심: Future가 싸서 던진 예외를 원형으로 복원 + Throwable c = ee.getCause(); + if (c instanceof FirebaseMessagingException fme) { + log.error("[FCM] FirebaseMessagingException memberId={} http={} code={} root={}", + memberId, httpStatusOf(fme), fme.getMessagingErrorCode(), rootSummary(fme), fme); + throw fme; // 리스너에서 코드/HTTP 기반 분류 가능 } + throw ee; // 그 외는 그대로 위로 + + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw ie; } } + + public static Integer httpStatusOf(com.google.firebase.messaging.FirebaseMessagingException fme) { + try { + var resp = fme.getHttpResponse(); + return resp != null ? resp.getStatusCode() : null; + } catch (Throwable ignore) { return null; } + } + + private String rootSummary(Throwable t) { + if (t == null) return "null"; + Throwable r = t; while (r.getCause() != null) r = r.getCause(); + return r.getClass().getName() + ": " + String.valueOf(r.getMessage()); + } + + public record FcmResult(int successCount, int failureCount, List invalidTokens) { + static FcmResult empty() { return new FcmResult(0, 0, java.util.List.of()); } + } } \ No newline at end of file From f2b384fa9734c1a03ee968da8ab0d90da7cde187 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Wed, 1 Oct 2025 23:16:09 +0900 Subject: [PATCH 247/270] =?UTF-8?q?refactor/#38=20-=20=EC=B0=A8=EB=8B=A8?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=82=B4=EB=A6=BC=EC=B0=A8?= =?UTF-8?q?=EC=88=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/domain/chat/repository/BlockRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java index 3a69e2c..197b25c 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java @@ -19,7 +19,8 @@ public interface BlockRepository extends JpaRepository { // BlockRepository.java @Query("SELECT COUNT(b) > 0 FROM Block b " + "WHERE (b.blocker = :user1 AND b.blocked = :user2) " + - "OR (b.blocker = :user2 AND b.blocked = :user1)") + "OR (b.blocker = :user2 AND b.blocked = :user1)" + + "ORDER BY b.createdAt DESC") boolean existsBlockRelationBetween(@Param("user1") Member user1, @Param("user2") Member user2); } From a72cf1c9b7985a9147cd9b2c62d7d4c9054ae964 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sun, 12 Oct 2025 21:13:12 +0900 Subject: [PATCH 248/270] =?UTF-8?q?[fix/#143]=20best=20store=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=A9=EB=B2=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PartnershipServiceImpl.java | 6 ++++++ .../repository/PartnershipUsageRepository.java | 15 +++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) 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 0a744e7..312f4f0 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 @@ -87,6 +87,12 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe // 5. 생성된 모든 Usage 기록을 한 번에 저장 partnershipUsageRepository.saveAll(usages); + Store store = storeRepository.findById(dto.getStoreId()).orElseThrow( + () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) + ); + Partner partner = store.getPartner(); + Long partnerId = partner.getId(); + notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); // @Transactional 환경에서는 studentsToUpdate의 변경 사항(스탬프)이 자동으로 DB에 반영됩니다. } diff --git a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java index d07c216..d457ee8 100644 --- a/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/PartnershipUsageRepository.java @@ -14,14 +14,13 @@ public interface PartnershipUsageRepository extends JpaRepository { @Query(value = """ - SELECT place - FROM partnership_usage - WHERE date >= CONVERT_TZ(CURDATE(), '+00:00', '+09:00') - AND date < CONVERT_TZ(CURDATE() + INTERVAL 1 DAY, '+00:00', '+09:00') - GROUP BY place - ORDER BY COUNT(*) DESC - LIMIT 10 - """, nativeQuery = true) + SELECT place + FROM partnership_usage + WHERE date = DATE(DATE_ADD(NOW(), INTERVAL 9 HOUR)) + GROUP BY place + ORDER BY COUNT(*) DESC + LIMIT 10 + """, nativeQuery = true) List findTodayPopularPartnership(); @Query("SELECT pu FROM PartnershipUsage pu " + From d83e803e134e4316db136205998c1f39e3a89dca Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:07:11 +1100 Subject: [PATCH 249/270] =?UTF-8?q?[REFACTOR/#191]=20-=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=A0=84=EC=86=A1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../auth/service/VerificationService.java | 11 ++++++ .../auth/service/VerificationServiceImpl.java | 39 +++++++++++++++++++ .../config/CertifyWebSocketConfig.java | 37 ++++++++++++++++++ .../assu/server/infra/firebase/FcmClient.java | 32 +++++++++------ 5 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/auth/service/VerificationService.java create mode 100644 src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java create mode 100644 src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java diff --git a/build.gradle b/build.gradle index 44aa4fc..62ac33e 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ dependencies { testImplementation 'org.springframework.amqp:spring-rabbit-test' // webflux; webclient - implementation 'org.springframework.boot:spring-boot-starter-webflux:3.4.5' + implementation 'org.springframework.boot:spring-boot-starter-webflux' // batch implementation 'org.springframework.boot:spring-boot-starter-batch' @@ -97,7 +97,7 @@ dependencies { // fcm implementation 'com.squareup.okhttp3:okhttp:4.10.0' - implementation 'com.fasterxml.jackson.core:jackson-databind' + //implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.google.auth:google-auth-library-oauth2-http:1.18.0' implementation 'com.google.firebase:firebase-admin:9.2.0' diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationService.java b/src/main/java/com/assu/server/domain/auth/service/VerificationService.java new file mode 100644 index 0000000..aaf2823 --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/service/VerificationService.java @@ -0,0 +1,11 @@ +package com.assu.server.domain.auth.service; + +import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; + +public interface VerificationService { + void checkPhoneNumberAvailability( + VerificationRequestDTO.PhoneVerificationCheckRequest request); + + void checkEmailAvailability( + VerificationRequestDTO.EmailVerificationCheckRequest request); +} diff --git a/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java new file mode 100644 index 0000000..85cc43e --- /dev/null +++ b/src/main/java/com/assu/server/domain/auth/service/VerificationServiceImpl.java @@ -0,0 +1,39 @@ +package com.assu.server.domain.auth.service; + +import com.assu.server.domain.auth.dto.verification.VerificationRequestDTO; +import com.assu.server.domain.auth.exception.CustomAuthException; +import com.assu.server.domain.auth.repository.CommonAuthRepository; +import com.assu.server.domain.member.repository.MemberRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class VerificationServiceImpl implements VerificationService { + + private final MemberRepository memberRepository; + private final CommonAuthRepository commonAuthRepository; + + @Override + public void checkPhoneNumberAvailability( + VerificationRequestDTO.PhoneVerificationCheckRequest request) { + + boolean exists = memberRepository.existsByPhoneNum(request.getPhoneNumber()); + + if (exists) { + throw new CustomAuthException(ErrorStatus.EXISTED_PHONE); + } + } + + @Override + public void checkEmailAvailability( + VerificationRequestDTO.EmailVerificationCheckRequest request) { + + boolean exists = commonAuthRepository.existsByEmail(request.getEmail()); + + if (exists) { + throw new CustomAuthException(ErrorStatus.EXISTED_EMAIL); + } + } +} diff --git a/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java new file mode 100644 index 0000000..56da642 --- /dev/null +++ b/src/main/java/com/assu/server/domain/certification/config/CertifyWebSocketConfig.java @@ -0,0 +1,37 @@ +package com.assu.server.domain.certification.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +import lombok.RequiredArgsConstructor; + +// @EnableWebSocketMessageBroker +// @Configuration +// @RequiredArgsConstructor +// public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer { +// +// private final StompAuthChannelInterceptor stompAuthChannelInterceptor; +// @Override +// public void configureMessageBroker(MessageBrokerRegistry config) { +// config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소 +// config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소 +// } +// +// @Override +// public void registerStompEndpoints(StompEndpointRegistry registry) { +// registry.addEndpoint("/ws-certify").setAllowedOriginPatterns("*"); // 클라이언트 WebSocket 연결 주소 +// // .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용 +// } +// +// @Override +// public void configureClientInboundChannel(ChannelRegistration registration) { +// registration.interceptors(stompAuthChannelInterceptor); +// } +// +// } diff --git a/src/main/java/com/assu/server/infra/firebase/FcmClient.java b/src/main/java/com/assu/server/infra/firebase/FcmClient.java index 78c3942..2db2707 100644 --- a/src/main/java/com/assu/server/infra/firebase/FcmClient.java +++ b/src/main/java/com/assu/server/infra/firebase/FcmClient.java @@ -51,19 +51,27 @@ public FcmResult sendToMemberId(Long memberId, String title, String body, Map future = messaging.sendEachForMulticastAsync(msg); From 2c867e5f420a8f5c2b7bc715fa585979e7941052 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:25:03 +1100 Subject: [PATCH 250/270] =?UTF-8?q?[REFACTOR/#191]=20-=20=EB=AC=B8?= =?UTF-8?q?=EC=9D=98=20=EB=8B=B5=EB=B3=80=EC=9D=80=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=95=88=20=ED=95=84=EC=9A=94=ED=95=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/config/SecurityConfig.java | 3 +- .../assu/server/infra/firebase/FcmClient.java | 33 +++++++------------ 2 files changed, 14 insertions(+), 22 deletions(-) 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 b06c6c8..2d32720 100644 --- a/src/main/java/com/assu/server/global/config/SecurityConfig.java +++ b/src/main/java/com/assu/server/global/config/SecurityConfig.java @@ -41,7 +41,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthF "/auth/students/login", "/auth/tokens/refresh", "/auth/students/ssu-verify", - "/map/place" + "/map/place", + "/member/inquiries/{inquiry-id}/answer" ).permitAll() .requestMatchers("/ws/**").permitAll() // 나머지는 인증 필요 diff --git a/src/main/java/com/assu/server/infra/firebase/FcmClient.java b/src/main/java/com/assu/server/infra/firebase/FcmClient.java index 2db2707..03785bc 100644 --- a/src/main/java/com/assu/server/infra/firebase/FcmClient.java +++ b/src/main/java/com/assu/server/infra/firebase/FcmClient.java @@ -50,28 +50,19 @@ public FcmResult sendToMemberId(Long memberId, String title, String body, Map future = messaging.sendEachForMulticastAsync(msg); From ba6860979c1d368a7c3af0a6ceb4329dc2468c34 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:40:22 +1100 Subject: [PATCH 251/270] =?UTF-8?q?[REFACTOR/#191]=20-=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=86=8D=EB=8F=84=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inquiry/service/ProfileImageServiceImpl.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java index 79fee6f..89a3b15 100644 --- a/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java +++ b/src/main/java/com/assu/server/domain/inquiry/service/ProfileImageServiceImpl.java @@ -58,20 +58,14 @@ public String getProfileImageUrl(Long memberId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new CustomAuthException(ErrorStatus.NO_SUCH_MEMBER)); - String keyOrUrl = member.getProfileUrl(); - if (keyOrUrl == null || keyOrUrl.isBlank()) { - throw new CustomAuthException(ErrorStatus.PROFILE_IMAGE_NOT_FOUND); - } + String key = member.getProfileUrl(); // DB에 저장된 경로 (ex. members/17/profile/aaa.png) - if (keyOrUrl.startsWith("http://") || keyOrUrl.startsWith("https://")) { - return keyOrUrl; - } - - String presigned = amazonS3Manager.generatePresignedUrl(keyOrUrl); - if (presigned == null) { + if (key == null || key.isBlank()) { throw new CustomAuthException(ErrorStatus.PROFILE_IMAGE_NOT_FOUND); } - return presigned; + + // S3 주소 리턴 + return "https://assu-bucket.s3.ap-northeast-2.amazonaws.com/" + key; } } From cba132809511b92a5edfe808714625a4e2a976e1 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Tue, 14 Oct 2025 23:14:41 +1100 Subject: [PATCH 252/270] =?UTF-8?q?[REFACTOR/#191]=20-=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=95=84=EC=9D=B4=EB=94=94=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/firebase/FirebaseInitLogger.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/com/assu/server/infra/firebase/FirebaseInitLogger.java diff --git a/src/main/java/com/assu/server/infra/firebase/FirebaseInitLogger.java b/src/main/java/com/assu/server/infra/firebase/FirebaseInitLogger.java new file mode 100644 index 0000000..a0fc1ee --- /dev/null +++ b/src/main/java/com/assu/server/infra/firebase/FirebaseInitLogger.java @@ -0,0 +1,31 @@ +package com.assu.server.infra.firebase; + +import com.google.firebase.FirebaseApp; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class FirebaseInitLogger { + + private static boolean logged = false; + + @PostConstruct + public void printFcmInitOnce() { + if (logged) return; // 이미 찍었으면 무시 + + try { + FirebaseApp app = FirebaseApp.getInstance(); + var options = app.getOptions(); + + log.info("[FCM_INIT] projectId={}", + options.getProjectId()); + + logged = true; + + } catch (Exception e) { + log.error("[FCM_INIT] FirebaseApp 초기화 실패", e); + } + } +} \ No newline at end of file From 63b604f062b01d7c4ae479b5533a953f719887fb Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Tue, 14 Oct 2025 22:22:59 +0900 Subject: [PATCH 253/270] =?UTF-8?q?fix/#38=20-=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=97=86=EB=8B=A4?= =?UTF-8?q?=EB=A9=B4=20=EC=83=9D=EC=84=B1,=20=EC=9E=88=EB=8B=A4=EB=A9=B4?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20=EB=B0=A9=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/converter/ChatConverter.java | 9 +++++++ .../domain/chat/dto/ChatResponseDTO.java | 1 + .../chat/repository/ChatRepository.java | 21 ++++++++++++++++ .../domain/chat/service/ChatServiceImpl.java | 25 ++++++++++++------- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 3144f7e..1a8a35a 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -49,8 +49,17 @@ public static ChatResponseDTO.CreateChatRoomResponseDTO toCreateChatRoomIdDTO(Ch .roomId(room.getId()) .adminViewName(room.getPartner().getName()) .partnerViewName(room.getAdmin().getName()) + .isNew(true) .build(); + } + public static ChatResponseDTO.CreateChatRoomResponseDTO toEnterChatRoomDTO(ChattingRoom room) { + return ChatResponseDTO.CreateChatRoomResponseDTO.builder() + .roomId(room.getId()) + .adminViewName(room.getPartner().getName()) + .partnerViewName(room.getAdmin().getName()) + .isNew(false) + .build(); } public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender, Member receiver) { diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java index 37df472..9a8f3d9 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatResponseDTO.java @@ -21,6 +21,7 @@ public static class CreateChatRoomResponseDTO { private Long roomId; private String adminViewName; private String partnerViewName; + private Boolean isNew; } // 메시지 전송 diff --git a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java index 6579a90..053b7f4 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/ChatRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; + import java.util.List; public interface ChatRepository extends JpaRepository { @@ -68,4 +69,24 @@ SELECT MAX(m2.createdAt) """) List findChattingRoomsByMemberId(@Param("memberId") Long memberId); + @Query(""" + SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END + FROM ChattingRoom c + WHERE c.admin.id = :adminId AND c.partner.id = :partnerId + """) + Boolean checkChattingRoomByAdminIdAndPartnerId( + @Param("adminId") Long adminId, + @Param("partnerId") Long partnerId + ); + + + @Query(""" + SELECT c + FROM ChattingRoom c + WHERE c.admin.id = :adminId AND c.partner.id = :partnerId + """) + ChattingRoom findChattingRoomByAdminIdAndPartnerId( + @Param("adminId") Long adminId, + @Param("partnerId")Long partnerId + ); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 084a963..17ce4e8 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -67,18 +67,25 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C throw new DatabaseException(ErrorStatus.NO_SUCH_STORE_WITH_THAT_PARTNER); } - ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner); + boolean isExist = chatRepository.checkChattingRoomByAdminIdAndPartnerId(admin.getId(), partner.getId()); - room.updateStatus(ActivationStatus.ACTIVE); + if(!isExist) { + ChattingRoom room = ChatConverter.toCreateChattingRoom(admin, partner); - room.updateMemberCount(2); + room.updateStatus(ActivationStatus.ACTIVE); - room.updateName( - partner.getName(), - admin.getName() - ); - ChattingRoom savedRoom = chatRepository.save(room); - return ChatConverter.toCreateChatRoomIdDTO(savedRoom); + room.updateMemberCount(2); + + room.updateName( + partner.getName(), + admin.getName() + ); + ChattingRoom savedRoom = chatRepository.save(room); + return ChatConverter.toCreateChatRoomIdDTO(savedRoom); + } else { + ChattingRoom existChatRoom = chatRepository.findChattingRoomByAdminIdAndPartnerId(admin.getId(), partner.getId()); + return ChatConverter.toEnterChatRoomDTO(existChatRoom); + } } @Override From a30d9d2fbc9a2b76c7776b7596df47743609f8f4 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sat, 18 Oct 2025 22:44:05 +0900 Subject: [PATCH 254/270] =?UTF-8?q?[fix/#143]=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/repository/ReviewRepository.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java index 97ea6fb..98f8d33 100644 --- a/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/assu/server/domain/review/repository/ReviewRepository.java @@ -29,11 +29,10 @@ Page findByMemberIdAndStatusAndStudentStatus( ); @Query(""" - SELECT r - FROM Review r - WHERE r.student.id = :memberId - ORDER BY r.createdAt DESC - """) + SELECT r + FROM Review r + WHERE r.student.id = :memberId + """) Page findByMemberId(@Param("memberId") Long memberId, Pageable pageable); @Query(""" From 00a248438f4bec69c4ccfdbf78662557d707cc83 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sun, 19 Oct 2025 17:52:51 +0900 Subject: [PATCH 255/270] =?UTF-8?q?fix/#38=20-=20=EC=A0=9C=ED=9C=B4=20?= =?UTF-8?q?=EC=A0=9C=EC=95=88=EC=84=9C=20=EC=B4=88=EC=95=88=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EC=8B=9C=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EA=B0=80=20=EA=B0=80=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/converter/ChatConverter.java | 13 ++++++++++ .../domain/chat/dto/ChatRequestDTO.java | 5 ++++ .../domain/chat/entity/enums/MessageType.java | 2 +- .../domain/chat/service/ChatService.java | 1 + .../domain/chat/service/ChatServiceImpl.java | 25 +++++++++++++++++-- .../service/PartnershipServiceImpl.java | 21 +++++++++++++++- 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java index 1a8a35a..1d38eae 100644 --- a/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java +++ b/src/main/java/com/assu/server/domain/chat/converter/ChatConverter.java @@ -1,6 +1,7 @@ package com.assu.server.domain.chat.converter; import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.chat.entity.enums.MessageType; import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.chat.dto.ChatMessageDTO; import com.assu.server.domain.chat.dto.ChatRequestDTO; @@ -69,6 +70,18 @@ public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO reque .receiver(receiver) .message(request.getMessage()) .unreadCount(request.getUnreadCountForSender()) + .type(MessageType.TEXT) + .build(); + } + + public static Message toGuideMessageEntity(ChatRequestDTO.ChatMessageRequestDTO request, ChattingRoom room, Member sender, Member receiver) { + return Message.builder() + .chattingRoom(room) + .sender(sender) + .receiver(receiver) + .message(request.getMessage()) + .unreadCount(0) + .type(MessageType.GUIDE) .build(); } diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java index 575c46b..c518055 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java @@ -1,6 +1,9 @@ package com.assu.server.domain.chat.dto; +import com.assu.server.domain.chat.entity.enums.MessageType; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; public class ChatRequestDTO { @@ -12,6 +15,8 @@ public static class CreateChatRoomRequestDTO { @Getter @Setter + @AllArgsConstructor + @NoArgsConstructor public static class ChatMessageRequestDTO { private Long roomId; private Long senderId; diff --git a/src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java b/src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java index a3255d2..94c6c4b 100644 --- a/src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java +++ b/src/main/java/com/assu/server/domain/chat/entity/enums/MessageType.java @@ -1,5 +1,5 @@ package com.assu.server.domain.chat.entity.enums; public enum MessageType { - TEXT, PROPOSAL, SYSTEM + TEXT, PROPOSAL, SYSTEM, GUIDE } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index e11b3c1..de2e476 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -12,4 +12,5 @@ public interface ChatService { ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId); ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId); ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId); + ChatResponseDTO.SendMessageResponseDTO sendGuideMessage(ChatRequestDTO.ChatMessageRequestDTO request); } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 17ce4e8..3c50bdf 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -9,7 +9,6 @@ import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.chat.entity.Message; -import com.assu.server.domain.chat.repository.BlockRepository; import com.assu.server.domain.chat.repository.ChatRepository; import com.assu.server.domain.chat.repository.MessageRepository; import com.assu.server.domain.member.entity.Member; @@ -24,6 +23,7 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -39,7 +39,7 @@ public class ChatServiceImpl implements ChatService { private final AdminRepository adminRepository; private final MessageRepository messageRepository; private final StoreRepository storeRepository; - private final BlockRepository blockRepository; + private final SimpMessagingTemplate simpMessagingTemplate; @Override @@ -109,6 +109,27 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM return ChatConverter.toSendMessageDTO(saved); } + + @Override + @Transactional + public ChatResponseDTO.SendMessageResponseDTO sendGuideMessage(ChatRequestDTO.ChatMessageRequestDTO request) { + // 유효성 검사 + ChattingRoom room = chatRepository.findById(request.getRoomId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); + Member sender = memberRepository.findById(request.getSenderId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + Member receiver = memberRepository.findById(request.getReceiverId()) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + + Message message = ChatConverter.toGuideMessageEntity(request, room, sender, receiver); + Message saved = messageRepository.saveAndFlush(message); + + ChatResponseDTO.SendMessageResponseDTO responseDTO = ChatConverter.toSendMessageDTO(saved); + simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), responseDTO); + + return responseDTO; + } + @Transactional @Override public ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId) { 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 312f4f0..fe4be31 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 @@ -4,7 +4,10 @@ import java.util.Collections; import java.util.List; import java.util.Optional; - +import com.assu.server.domain.chat.dto.ChatRequestDTO; +import com.assu.server.domain.chat.entity.ChattingRoom; +import com.assu.server.domain.chat.repository.ChatRepository; +import com.assu.server.domain.chat.service.ChatService; import org.springframework.stereotype.Service; import com.assu.server.domain.member.entity.Member; @@ -54,6 +57,8 @@ public class PartnershipServiceImpl implements PartnershipService { private final StudentRepository studentRepository; private final PaperContentRepository contentRepository; private final NotificationCommandService notificationService; + private final ChatService chatService; + private final ChatRepository chatRepository; @Override @@ -403,6 +408,20 @@ public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(Part notificationService.sendPartnerProposal(partner.getId(), draftPaper.getId(), admin.getName()); + ChattingRoom chattingRoom = chatRepository.findChattingRoomByAdminIdAndPartnerId(admin.getId(), partner.getId()); + + String guideMessage = admin.getName() + "님이 제휴 제안서 초안을 전송했어요! 확인 후 제휴 제안서를 작성해 주세요"; + ChatRequestDTO.ChatMessageRequestDTO guideMessageRequest = new ChatRequestDTO.ChatMessageRequestDTO( + chattingRoom.getId(), + admin.getId(), + partner.getId(), + guideMessage, + 0 + ); + + // 5. 완성된 DTO를 사용해서 안내 메시지를 전송합니다. + chatService.sendGuideMessage(guideMessageRequest); + return PartnershipConverter.toCreateDraftResponseDTO(draftPaper); } From e5c8f28b4ba0b9dcfae353d82bbefb60f3571f87 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Mon, 20 Oct 2025 19:38:20 +0900 Subject: [PATCH 256/270] =?UTF-8?q?fix/#38=20-=20=EC=A0=9C=ED=9C=B4=20?= =?UTF-8?q?=EC=A0=9C=EC=95=88=EC=84=9C=20=EC=83=81=ED=83=9C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=8B=9C=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PartnershipServiceImpl.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 fe4be31..17e171a 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 @@ -293,6 +293,37 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par paper.setIsActivated(next); + Long adminId = paper.getAdmin().getId(); + Admin admin = adminRepository.findById(adminId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + Long partnerId = paper.getPartner().getId(); + Partner partner = partnerRepository.findById(partnerId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); + ChattingRoom chattingRoom = chatRepository.findChattingRoomByAdminIdAndPartnerId(admin.getId(), partner.getId()); + + if (next.equals(ActivationStatus.SUSPEND)) { + String guideMessage = partner.getName() + "님이 제휴 제안서를 전송했어요!\n내용을 확인 후 동의해 주세요"; + ChatRequestDTO.ChatMessageRequestDTO guideMessageRequest = new ChatRequestDTO.ChatMessageRequestDTO( + chattingRoom.getId(), + partnerId, + adminId, + guideMessage, + 0 + ); + chatService.sendGuideMessage(guideMessageRequest); + + } else if (next.equals(ActivationStatus.ACTIVE)) { + String guideMessage = "축하드립니다!\n" +admin.getName() + "님이 동의했습니다! 제휴 계약서를 다시한번 확인해 보세요!"; + ChatRequestDTO.ChatMessageRequestDTO guideMessageRequest = new ChatRequestDTO.ChatMessageRequestDTO( + chattingRoom.getId(), + adminId, + partnerId, + guideMessage, + 0 + ); + chatService.sendGuideMessage(guideMessageRequest); + } + return PartnershipResponseDTO.UpdateResponseDTO.builder() .partnershipId(paper.getId()) .prevStatus(prev == null ? null : prev.name()) From ee071bb611640c9cfab6fb469c2d504ba3eafc50 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Mon, 20 Oct 2025 20:07:04 +0900 Subject: [PATCH 257/270] =?UTF-8?q?fix/#38=20-=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=8B=9C=20=ED=83=80=EC=9E=85=EB=8F=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/assu/server/domain/chat/dto/ChatMessageDTO.java | 3 +++ .../assu/server/domain/chat/repository/MessageRepository.java | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java index 1e0f3ac..9886cba 100644 --- a/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java +++ b/src/main/java/com/assu/server/domain/chat/dto/ChatMessageDTO.java @@ -1,5 +1,6 @@ package com.assu.server.domain.chat.dto; +import com.assu.server.domain.chat.entity.enums.MessageType; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -30,4 +31,6 @@ public class ChatMessageDTO { @JsonProperty("isMyMessage") private boolean isMyMessage; + + private MessageType messageType; } diff --git a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java index 9fe4581..53e12d3 100644 --- a/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java +++ b/src/main/java/com/assu/server/domain/chat/repository/MessageRepository.java @@ -37,7 +37,8 @@ SELECT COUNT(m) m.isRead, CASE WHEN m.sender.id = :memberId THEN true ELSE false - END + END, + m.type ) FROM Message m WHERE m.chattingRoom.id = :roomId From 9edc17b952e7b64751b53b0762466c2aa258d491 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Mon, 20 Oct 2025 21:43:00 +0900 Subject: [PATCH 258/270] =?UTF-8?q?fix/#38=20-=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/controller/ChatController.java | 15 +++++++++++++++ .../domain/chat/service/ChatServiceImpl.java | 3 +++ .../service/PartnershipServiceImpl.java | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index e29d822..9395a09 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -4,6 +4,10 @@ import com.assu.server.domain.chat.repository.MessageRepository; import com.assu.server.domain.chat.service.BlockService; import com.assu.server.domain.chat.service.ChatService; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.member.repository.MemberRepository; +import com.assu.server.domain.notification.service.NotificationCommandService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import com.assu.server.global.util.PresenceTracker; import com.assu.server.global.util.PrincipalDetails; @@ -28,7 +32,9 @@ public class ChatController { private final SimpMessagingTemplate simpMessagingTemplate; private final PresenceTracker presenceTracker; private final MessageRepository messageRepository; + private final MemberRepository memberRepository; private final BlockService blockService; + private final NotificationCommandService notificationCommandService; @Operation( summary = "채팅방을 생성하는 API", @@ -92,6 +98,15 @@ public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) "/queue/updates", updateDTO ); + Member sender = memberRepository.findById(request.getSenderId()).orElse(null); + String senderName = ""; + if (sender.getRole()== UserRole.ADMIN) { + senderName = sender.getAdminProfile().getName(); + } else { + senderName = sender.getPartnerProfile().getName(); + } + + notificationCommandService.sendChat(request.getReceiverId(), request.getRoomId(), senderName, request.getMessage()); } } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 3c50bdf..14bc8e3 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -14,6 +14,7 @@ import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.member.repository.MemberRepository; +import com.assu.server.domain.notification.service.NotificationCommandService; import com.assu.server.domain.partner.entity.Partner; import com.assu.server.domain.partner.repository.PartnerRepository; import com.assu.server.domain.store.entity.Store; @@ -40,6 +41,7 @@ public class ChatServiceImpl implements ChatService { private final MessageRepository messageRepository; private final StoreRepository storeRepository; private final SimpMessagingTemplate simpMessagingTemplate; + private final NotificationCommandService notificationCommandService; @Override @@ -126,6 +128,7 @@ public ChatResponseDTO.SendMessageResponseDTO sendGuideMessage(ChatRequestDTO.Ch ChatResponseDTO.SendMessageResponseDTO responseDTO = ChatConverter.toSendMessageDTO(saved); simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), responseDTO); + notificationCommandService.sendChat(receiver.getId(), room.getId(), sender.getAdminProfile().getName(), "제안서 초안이 도착했습니다. 확인해 주세요"); return responseDTO; } 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 17e171a..3557eb5 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 @@ -313,7 +313,7 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par chatService.sendGuideMessage(guideMessageRequest); } else if (next.equals(ActivationStatus.ACTIVE)) { - String guideMessage = "축하드립니다!\n" +admin.getName() + "님이 동의했습니다! 제휴 계약서를 다시한번 확인해 보세요!"; + String guideMessage = "축하드립니다!\n" + "제휴 계약이 성립되었습니다. 제휴 계약서를 다시한번 확인해 보세요!"; ChatRequestDTO.ChatMessageRequestDTO guideMessageRequest = new ChatRequestDTO.ChatMessageRequestDTO( chattingRoom.getId(), adminId, From 0084eac64ad84efc4bf882457337cf9f4b4dc6cd Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Wed, 22 Oct 2025 21:14:28 +0900 Subject: [PATCH 259/270] =?UTF-8?q?fix/#38=20-=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20@Transactional=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assu/server/domain/chat/controller/ChatController.java | 6 +++++- .../assu/server/domain/chat/service/ChatServiceImpl.java | 1 - .../domain/partnership/service/PartnershipServiceImpl.java | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 9395a09..176b23a 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -12,6 +12,7 @@ import com.assu.server.global.util.PresenceTracker; import com.assu.server.global.util.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -70,6 +71,7 @@ public BaseResponse> "- receiverId: Request Body, Long\n" + "- message: Request Body, String\n" ) + @Transactional @MessageMapping("/send") public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { // 먼저 접속 여부 확인 후 unreadCount 계산 @@ -99,14 +101,16 @@ public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) updateDTO ); Member sender = memberRepository.findById(request.getSenderId()).orElse(null); - String senderName = ""; + String senderName; if (sender.getRole()== UserRole.ADMIN) { senderName = sender.getAdminProfile().getName(); } else { senderName = sender.getPartnerProfile().getName(); } + log.info(">>>>>>>>메시지 전송은 될걸"); notificationCommandService.sendChat(request.getReceiverId(), request.getRoomId(), senderName, request.getMessage()); + log.info(">>>>>>>>알림이 가나"); } } diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 14bc8e3..a4f7ad0 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -128,7 +128,6 @@ public ChatResponseDTO.SendMessageResponseDTO sendGuideMessage(ChatRequestDTO.Ch ChatResponseDTO.SendMessageResponseDTO responseDTO = ChatConverter.toSendMessageDTO(saved); simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), responseDTO); - notificationCommandService.sendChat(receiver.getId(), room.getId(), sender.getAdminProfile().getName(), "제안서 초안이 도착했습니다. 확인해 주세요"); return responseDTO; } 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 3557eb5..89955e4 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 @@ -311,6 +311,7 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par 0 ); chatService.sendGuideMessage(guideMessageRequest); + notificationService.sendChat(adminId, chattingRoom.getId(), partner.getName(), guideMessage); } else if (next.equals(ActivationStatus.ACTIVE)) { String guideMessage = "축하드립니다!\n" + "제휴 계약이 성립되었습니다. 제휴 계약서를 다시한번 확인해 보세요!"; @@ -322,6 +323,7 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par 0 ); chatService.sendGuideMessage(guideMessageRequest); + notificationService.sendChat(partnerId, chattingRoom.getId(), admin.getName(), guideMessage); } return PartnershipResponseDTO.UpdateResponseDTO.builder() @@ -452,6 +454,7 @@ public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(Part // 5. 완성된 DTO를 사용해서 안내 메시지를 전송합니다. chatService.sendGuideMessage(guideMessageRequest); + notificationService.sendChat(partner.getId(), chattingRoom.getId(), admin.getName(), guideMessage); return PartnershipConverter.toCreateDraftResponseDTO(draftPaper); } From a913ea6aa076af591b83c5d5e907e58e5755dffb Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sat, 25 Oct 2025 15:42:06 +0900 Subject: [PATCH 260/270] =?UTF-8?q?refactor/#38=20-=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 96 +++++++++++-------- .../chat/dto/MessageHandlingResult.java | 26 +++++ .../domain/chat/service/ChatService.java | 5 +- .../domain/chat/service/ChatServiceImpl.java | 77 +++++++++++++-- 4 files changed, 154 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 176b23a..53af1a4 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -4,15 +4,12 @@ import com.assu.server.domain.chat.repository.MessageRepository; import com.assu.server.domain.chat.service.BlockService; import com.assu.server.domain.chat.service.ChatService; -import com.assu.server.domain.common.enums.UserRole; -import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.domain.notification.service.NotificationCommandService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import com.assu.server.global.util.PresenceTracker; import com.assu.server.global.util.PrincipalDetails; import io.swagger.v3.oas.annotations.Operation; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -31,11 +28,11 @@ public class ChatController { private final ChatService chatService; private final SimpMessagingTemplate simpMessagingTemplate; - private final PresenceTracker presenceTracker; - private final MessageRepository messageRepository; - private final MemberRepository memberRepository; +// private final PresenceTracker presenceTracker; +// private final MessageRepository messageRepository; +// private final MemberRepository memberRepository; private final BlockService blockService; - private final NotificationCommandService notificationCommandService; +// private final NotificationCommandService notificationCommandService; @Operation( summary = "채팅방을 생성하는 API", @@ -71,49 +68,68 @@ public BaseResponse> "- receiverId: Request Body, Long\n" + "- message: Request Body, String\n" ) - @Transactional @MessageMapping("/send") public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { - // 먼저 접속 여부 확인 후 unreadCount 계산 - boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); - int unreadForSender = receiverInRoom ? 0 : 1; - request.setUnreadCountForSender(unreadForSender); - - ChatResponseDTO.SendMessageResponseDTO saved = chatService.handleMessage(request); - simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved); - - if (!receiverInRoom) { - Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver( - request.getRoomId(), - request.getReceiverId() - ); - ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder() - .roomId(request.getRoomId()) - .lastMessage(saved.message()) - .lastMessageTime(saved.sentAt()) - .unreadCount(totalUnreadCount) - .build(); + // 1. 서비스 호출 (모든 비즈니스 로직 위임) + MessageHandlingResult result = chatService.handleMessage(request); + + // 2. [항상 전송] 채팅방 메시지 전송 + simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), result.sendMessageResponseDTO()); + // 3. [조건부 전송] 채팅방 목록 업데이트 전송 + if (result.hasRoomUpdates()) { simpMessagingTemplate.convertAndSendToUser( - request.getReceiverId().toString(), + result.receiverId().toString(), "/queue/updates", - updateDTO + result.chatRoomUpdateDTO() ); - Member sender = memberRepository.findById(request.getSenderId()).orElse(null); - String senderName; - if (sender.getRole()== UserRole.ADMIN) { - senderName = sender.getAdminProfile().getName(); - } else { - senderName = sender.getPartnerProfile().getName(); - } - - log.info(">>>>>>>>메시지 전송은 될걸"); - notificationCommandService.sendChat(request.getReceiverId(), request.getRoomId(), senderName, request.getMessage()); - log.info(">>>>>>>>알림이 가나"); } } +// @Transactional +// @MessageMapping("/send") +// public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) { +// // 먼저 접속 여부 확인 후 unreadCount 계산 +// boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); +// int unreadForSender = receiverInRoom ? 0 : 1; +// request.setUnreadCountForSender(unreadForSender); +// +// ChatResponseDTO.SendMessageResponseDTO saved = chatService.handleMessage(request); +// simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved); +// +// if (!receiverInRoom) { +// Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver( +// request.getRoomId(), +// request.getReceiverId() +// ); +// +// ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder() +// .roomId(request.getRoomId()) +// .lastMessage(saved.message()) +// .lastMessageTime(saved.sentAt()) +// .unreadCount(totalUnreadCount) +// .build(); +// +// simpMessagingTemplate.convertAndSendToUser( +// request.getReceiverId().toString(), +// "/queue/updates", +// updateDTO +// ); +// Member sender = memberRepository.findById(request.getSenderId()).orElse(null); +// String senderName; +// if (sender.getRole()== UserRole.ADMIN) { +// senderName = sender.getAdminProfile().getName(); +// } else { +// senderName = sender.getPartnerProfile().getName(); +// } +// +// log.info(">>>>>>>>메시지 전송은 될걸"); +// notificationCommandService.sendChat(request.getReceiverId(), request.getRoomId(), senderName, request.getMessage()); +// log.info(">>>>>>>>알림이 가나"); +// } +// } + @Operation( summary = "메시지 읽음 처리 API", description = "# [v1.0 (2025-08-05)](https://clumsy-seeder-416.notion.site/2241197c19ed800eab45c35073761c97?v=2241197c19ed8134b64f000cc26c5d31&p=2241197c19ed81ffa771cb18ab157b54&pm=s) 메시지를 읽음처리합니다.\n"+ diff --git a/src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java b/src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java new file mode 100644 index 0000000..56c142e --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/MessageHandlingResult.java @@ -0,0 +1,26 @@ +package com.assu.server.domain.chat.dto; + +public record MessageHandlingResult( + ChatResponseDTO.SendMessageResponseDTO sendMessageResponseDTO, + ChatRoomUpdateDTO chatRoomUpdateDTO, + Long receiverId +) { + + // 정적 팩토리 메소드 1 + public static MessageHandlingResult of(ChatResponseDTO.SendMessageResponseDTO sendMessageDTO) { + // record의 기본 생성자를 호출합니다. + return new MessageHandlingResult(sendMessageDTO, null, null); + } + + // 정적 팩토리 메소드 2 + public static MessageHandlingResult withUpdates(ChatResponseDTO.SendMessageResponseDTO sendMessageDTO, ChatRoomUpdateDTO updateDTO, Long receiverId) { + // record의 기본 생성자를 호출합니다. + return new MessageHandlingResult(sendMessageDTO, updateDTO, receiverId); + } + + // 헬퍼(Helper) 메소드 + public boolean hasRoomUpdates() { + // record는 'get' 접두사 없는 접근자(chatRoomUpdateDTO())를 사용합니다. + return chatRoomUpdateDTO != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatService.java b/src/main/java/com/assu/server/domain/chat/service/ChatService.java index de2e476..10c84fb 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatService.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatService.java @@ -3,12 +3,15 @@ import com.assu.server.domain.chat.dto.ChatRequestDTO; import com.assu.server.domain.chat.dto.ChatResponseDTO; import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; +import com.assu.server.domain.chat.dto.MessageHandlingResult; + import java.util.List; public interface ChatService { List getChatRoomList(Long memberId); ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.CreateChatRoomRequestDTO request, Long memberId); - ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); +// ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); + MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO request); ChatResponseDTO.ReadMessageResponseDTO readMessage(Long roomId, Long memberId); ChatResponseDTO.ChatHistoryResponseDTO readHistory(Long roomId, Long memberId); ChatResponseDTO.LeaveChattingRoomResponseDTO leaveChattingRoom(Long roomId, Long memberId); diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index a4f7ad0..c3e2558 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -3,14 +3,12 @@ import com.assu.server.domain.admin.entity.Admin; import com.assu.server.domain.admin.repository.AdminRepository; import com.assu.server.domain.chat.converter.ChatConverter; -import com.assu.server.domain.chat.dto.ChatMessageDTO; -import com.assu.server.domain.chat.dto.ChatRequestDTO; -import com.assu.server.domain.chat.dto.ChatResponseDTO; -import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; +import com.assu.server.domain.chat.dto.*; import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.chat.entity.Message; import com.assu.server.domain.chat.repository.ChatRepository; import com.assu.server.domain.chat.repository.MessageRepository; +import com.assu.server.domain.common.enums.UserRole; import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.common.enums.ActivationStatus; import com.assu.server.domain.member.repository.MemberRepository; @@ -21,6 +19,7 @@ import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; +import com.assu.server.global.util.PresenceTracker; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -42,6 +41,7 @@ public class ChatServiceImpl implements ChatService { private final StoreRepository storeRepository; private final SimpMessagingTemplate simpMessagingTemplate; private final NotificationCommandService notificationCommandService; + private final PresenceTracker presenceTracker; @Override @@ -90,10 +90,31 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C } } +// @Override +// @Transactional +// public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { +// // 유효성 검사 +// ChattingRoom room = chatRepository.findById(request.getRoomId()) +// .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); +// Member sender = memberRepository.findById(request.getSenderId()) +// .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); +// Member receiver = memberRepository.findById(request.getReceiverId()) +// .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); +// +// Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); +// Message saved = messageRepository.saveAndFlush(message); +// log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", +// saved.getId(), room.getId(), sender.getId(), receiver.getId()); +// +// return ChatConverter.toSendMessageDTO(saved); +// } + + // ChatService의 handleMessage 메서드 (수정) + @Override @Transactional - public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { - // 유효성 검사 + public MessageHandlingResult handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) { + // 1. 유효성 검사 (기존 로직) ChattingRoom room = chatRepository.findById(request.getRoomId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM)); Member sender = memberRepository.findById(request.getSenderId()) @@ -101,14 +122,52 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM Member receiver = memberRepository.findById(request.getReceiverId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER)); + // 2. 컨트롤러에서 가져온 비즈니스 로직 (접속 확인) + boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId()); + int unreadForSender = receiverInRoom ? 0 : 1; + request.setUnreadCountForSender(unreadForSender); + + // 3. 메시지 저장 (기존 로직) Message message = ChatConverter.toMessageEntity(request, room, sender, receiver); Message saved = messageRepository.saveAndFlush(message); log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); -// boolean exists = messageRepository.existsById(saved.getId()); -// log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제 - return ChatConverter.toSendMessageDTO(saved); + ChatResponseDTO.SendMessageResponseDTO savedDTO = ChatConverter.toSendMessageDTO(saved); + + // 4. 컨트롤러에서 가져온 비즈니스 로직 (수신자 부재 시) + if (!receiverInRoom) { + // 4-1. 안 읽은 수 계산 + Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver( + request.getRoomId(), + request.getReceiverId() + ); + + // 4-2. 채팅방 목록 업데이트 DTO 생성 + ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder() + .roomId(request.getRoomId()) + .lastMessage(savedDTO.message()) + .lastMessageTime(savedDTO.sentAt()) + .unreadCount(totalUnreadCount) + .build(); + + // 4-3. 발신자 이름 찾기 (기존 컨트롤러 로직) + String senderName; + if (sender.getRole() == UserRole.ADMIN) { // 이미 sender 객체가 있으므로 재활용 + senderName = sender.getAdminProfile().getName(); + } else { + senderName = sender.getPartnerProfile().getName(); + } + + // 4-4. 알림 전송 + notificationCommandService.sendChat(request.getReceiverId(), request.getRoomId(), senderName, request.getMessage()); + + // 5. [업데이트 포함] 결과 반환 + return MessageHandlingResult.withUpdates(savedDTO, updateDTO, request.getReceiverId()); + } + + // 5. [일반 메시지] 결과 반환 + return MessageHandlingResult.of(savedDTO); } From d33aa83549fb9cd11a04ec2ff1c340c46db1df34 Mon Sep 17 00:00:00 2001 From: SJ Hwang Date: Mon, 27 Oct 2025 18:17:11 +0900 Subject: [PATCH 261/270] =?UTF-8?q?[Refactor/#169]=20-=20paperContent=20?= =?UTF-8?q?=EC=97=86=EC=9D=84=20=EC=8B=9C=20null=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/map/dto/MapResponseDTO.java | 1 - .../domain/map/service/MapServiceImpl.java | 24 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java index 204352d..1277b40 100644 --- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java +++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java @@ -67,7 +67,6 @@ public static class StoreMapResponseDTO { private Double longitude; private String profileUrl; private String phoneNumber; - } @Getter @NoArgsConstructor @AllArgsConstructor @Builder diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java index 0647116..9d2c66b 100644 --- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java +++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java @@ -144,6 +144,13 @@ public List getStores(MapRequestDTO.ViewOnMa : null; final String profileUrl = (key != null ? amazonS3Manager.generatePresignedUrl(key) : null); + // phoneNumber null-safe 처리 (빈 문자열로 변환) + final String phoneNumber = (s.getPartner() != null + && s.getPartner().getMember() != null + && s.getPartner().getMember().getPhoneNum() != null) + ? s.getPartner().getMember().getPhoneNum() + : ""; + // 2-4) DTO 빌드 (content null 허용) return MapResponseDTO.StoreMapResponseDTO.builder() .storeId(s.getId()) @@ -162,7 +169,7 @@ public List getStores(MapRequestDTO.ViewOnMa .latitude(s.getLatitude()) .longitude(s.getLongitude()) .profileUrl(profileUrl) - .phoneNumber(s.getPartner().getMember().getPhoneNum()) + .phoneNumber(phoneNumber) .build(); }).toList(); } @@ -174,9 +181,7 @@ public List searchStores(String keyword) { return stores.stream().map(s -> { boolean hasPartner = s.getPartner() != null; PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId()) - .orElseThrow( - () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) - ); + .orElse(null); String key = (s.getPartner() != null) ? s.getPartner().getMember().getProfileUrl() : null; String url = amazonS3Manager.generatePresignedUrl(key); @@ -206,9 +211,16 @@ else if (content.getOptionType() == OptionType.SERVICE) { } } + // phoneNumber null-safe 처리 (빈 문자열로 변환) + String phoneNumber = (s.getPartner() != null + && s.getPartner().getMember() != null + && s.getPartner().getMember().getPhoneNum() != null) + ? s.getPartner().getMember().getPhoneNum() + : ""; + return MapResponseDTO.StoreMapResponseDTO.builder() .storeId(s.getId()) - .adminName(admin.getName()) + .adminName(admin != null ? admin.getName() : null) .adminId(adminId) .name(s.getName()) .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress()) @@ -223,7 +235,7 @@ else if (content.getOptionType() == OptionType.SERVICE) { .latitude(s.getLatitude()) .longitude(s.getLongitude()) .profileUrl(url) - .phoneNumber(s.getPartner().getMember().getPhoneNum()) + .phoneNumber(phoneNumber) .build(); }).toList(); } From 099cf81d5b06ed2f5972a2e7ab515c27482293e7 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:27:24 +1100 Subject: [PATCH 262/270] =?UTF-8?q?[FEAT/#215]=20-=20=ED=95=99=EC=83=9D?= =?UTF-8?q?=EC=9D=B4=20=EB=B0=9B=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=20=EC=A0=9C=ED=9C=B4=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EC=9E=91=20?= =?UTF-8?q?(=EC=8A=A4=EC=BC=80=EC=A4=84=EB=A7=81)=20-=20=ED=95=99=EC=83=9D?= =?UTF-8?q?=EC=9D=B4=20=EB=B0=9B=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=20=EC=A0=9C=ED=9C=B4=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/PaperContentRepository.java | 3 + .../repository/PaperRepository.java | 13 ++ .../user/controller/StudentController.java | 26 +++- .../domain/user/dto/StudentResponseDTO.java | 18 +++ .../user/repository/StudentRepository.java | 1 + .../user/repository/UserPaperRepository.java | 29 +++++ .../domain/user/service/StudentService.java | 5 +- .../user/service/StudentServiceImpl.java | 123 +++++++++++++++++- .../user/service/UserPaperScheduler.java | 23 ++++ 9 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/assu/server/domain/user/repository/UserPaperRepository.java create mode 100644 src/main/java/com/assu/server/domain/user/service/UserPaperScheduler.java diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java index 8f4336a..945d3b8 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java @@ -67,4 +67,7 @@ Optional findLatestValidByStoreIdNative( @Param("price") String price, // CriterionType.PRICE.name() @Param("headcount") String headcount // CriterionType.HEADCOUNT.name() ); + + Optional findTopByPaperIdOrderByIdDesc(Long paperId); + } diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java index 3255685..8c67b89 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -49,4 +50,16 @@ List findAllSuspendedByAdminWithNoPartner( Optional findTopPaperByStoreId(Long storeId); long countByStore_Id(Long storeId); + @Query(""" + SELECT p FROM Paper p + WHERE p.admin.id IN :adminIds + AND p.isActivated = :status + AND p.partnershipPeriodStart <= :today + AND p.partnershipPeriodEnd >= :today + """) + List findActivePapersByAdminIds(@Param("adminIds") List adminIds, + @Param("today") LocalDate today, + @Param("status") ActivationStatus status); + + List findByStoreIdAndAdminIdAndIsActivated(Long storeId, Long adminId, ActivationStatus isActivated); } diff --git a/src/main/java/com/assu/server/domain/user/controller/StudentController.java b/src/main/java/com/assu/server/domain/user/controller/StudentController.java index d1c7517..ff98eaa 100644 --- a/src/main/java/com/assu/server/domain/user/controller/StudentController.java +++ b/src/main/java/com/assu/server/domain/user/controller/StudentController.java @@ -21,6 +21,9 @@ import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import org.springframework.web.bind.annotation.*; + +import java.util.List; + @RestController @Tag(name = "유저 관련 api", description = "유저와 관련된 로직을 처리하는 api") @RequiredArgsConstructor @@ -73,9 +76,6 @@ public ResponseEntity>> get studentService.getUnreviewedUsage(pd.getId(), pageable))); } - - - @Operation( summary = "사용자 stamp 개수 조회 API", description = "# [v1.0 (2025-09-09)](https://www.notion.so/2691197c19ed805c980dd546adee9301?source=copy_link)\n" + @@ -90,4 +90,24 @@ public BaseResponse getStamp( ) { return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getStamp(pd.getId())); } + + @Operation( + summary = "사용자의 이용 가능한 제휴 조회 API", + description = "# [v1.0 (2025-10-30)](https://clumsy-seeder-416.notion.site/API-29c1197c19ed8030b1f5e2a744416651?source=copy_link)\n" + + "- all = true면 전체 조회, false면 2개만 조회" + ) + @GetMapping("/usable") + public BaseResponse> getUsablePartnership( + @AuthenticationPrincipal PrincipalDetails pd, + @RequestParam(name = "all", defaultValue = "false") boolean all + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, studentService.getUsablePartnership(pd.getId(), all)); + } + + @PostMapping("/sync/all") + public BaseResponse syncAllStudentsNow() { + studentService.syncUserPapersForAllStudents(); + return BaseResponse.onSuccess(SuccessStatus._OK, "전체 학생 user_paper 동기화 완료"); + } + } diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java index 92231a0..38cf04b 100644 --- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java @@ -4,6 +4,8 @@ import java.time.LocalDateTime; import java.util.List; +import com.assu.server.domain.partnership.entity.enums.CriterionType; +import com.assu.server.domain.partnership.entity.enums.OptionType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -66,4 +68,20 @@ public static class CheckStampResponseDTO { private String message; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class UsablePartnershipDTO { + private Long partnershipId; + private String adminName; + private String partnerName; + private CriterionType criterionType; + private OptionType optionType; + private Integer people; + private Long cost; + private String category; + private Long discountRate; + } + } diff --git a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java index c9128d1..625d4fb 100644 --- a/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java +++ b/src/main/java/com/assu/server/domain/user/repository/StudentRepository.java @@ -11,4 +11,5 @@ public interface StudentRepository extends JpaRepository { Optional findStudentById(Long id); + } diff --git a/src/main/java/com/assu/server/domain/user/repository/UserPaperRepository.java b/src/main/java/com/assu/server/domain/user/repository/UserPaperRepository.java new file mode 100644 index 0000000..511998f --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/repository/UserPaperRepository.java @@ -0,0 +1,29 @@ +package com.assu.server.domain.user.repository; + +import com.assu.server.domain.user.entity.UserPaper; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.util.List; + +public interface UserPaperRepository extends JpaRepository { + + @Query(""" + SELECT up FROM UserPaper up + JOIN FETCH up.paper p + LEFT JOIN FETCH p.store s + LEFT JOIN FETCH p.admin a + WHERE up.student.id = :studentId + AND p.isActivated = com.assu.server.domain.common.enums.ActivationStatus.ACTIVE + AND p.partnershipPeriodStart <= :today + AND p.partnershipPeriodEnd >= :today + ORDER BY p.id DESC + """) + List findActivePartnershipsByStudentId(@Param("studentId") Long studentId, + @Param("today") LocalDate today); + + boolean existsByStudentIdAndPaperId(Long studentId, Long paperId); + +} diff --git a/src/main/java/com/assu/server/domain/user/service/StudentService.java b/src/main/java/com/assu/server/domain/user/service/StudentService.java index 595613f..5076c46 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentService.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentService.java @@ -5,9 +5,12 @@ import com.assu.server.domain.user.dto.StudentResponseDTO; +import java.util.List; + public interface StudentService { StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month); StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId);//조회 - Page getUnreviewedUsage(Long memberId, Pageable pageable); + List getUsablePartnership(Long memberId, Boolean all); + void syncUserPapersForAllStudents(); } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java index 64f7b5e..d41dedc 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java @@ -1,18 +1,30 @@ package com.assu.server.domain.user.service; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partnership.entity.Goods; +import com.assu.server.domain.partnership.entity.Paper; import com.assu.server.domain.partnership.entity.PaperContent; +import com.assu.server.domain.partnership.entity.enums.OptionType; +import com.assu.server.domain.partnership.repository.GoodsRepository; import com.assu.server.domain.partnership.repository.PaperContentRepository; +import com.assu.server.domain.partnership.repository.PaperRepository; import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.user.converter.StudentConverter; import com.assu.server.domain.user.dto.StudentResponseDTO; import com.assu.server.domain.user.entity.PartnershipUsage; import com.assu.server.domain.user.entity.Student; +import com.assu.server.domain.user.entity.UserPaper; import com.assu.server.domain.user.repository.PartnershipUsageRepository; import com.assu.server.domain.user.repository.StudentRepository; +import com.assu.server.domain.user.repository.UserPaperRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import jakarta.transaction.Transactional; @@ -27,6 +39,13 @@ @RequiredArgsConstructor public class StudentServiceImpl implements StudentService { private final StudentRepository studentRepository; + private final UserPaperRepository userPaperRepository; + private final PaperContentRepository paperContentRepository; + private final PartnershipUsageRepository partnershipUsageRepository; + private final GoodsRepository goodsRepository; + private final AdminRepository adminRepository; + private final PaperRepository paperRepository; + @Override @Transactional public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) { @@ -36,9 +55,6 @@ public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) { return StudentConverter.checkStampResponseDTO(student, "스탬프 조회 성공"); } - private final PaperContentRepository paperContentRepository; - private final PartnershipUsageRepository partnershipUsageRepository; - @Override @Transactional public StudentResponseDTO.myPartnership getMyPartnership(Long studentId, int year, int month) { @@ -112,4 +128,105 @@ public Page getUnreviewedUsage(Long memberId, }); } + @Override + public List getUsablePartnership(Long memberId, Boolean all) { + LocalDate today = LocalDate.now(); + + List userPapers = userPaperRepository.findActivePartnershipsByStudentId(memberId, today); + + List result = userPapers.stream().map(up -> { + Paper paper = up.getPaper(); + PaperContent content = up.getPaperContent(); + Store store = paper.getStore(); + + String adminName = (paper.getAdmin() != null) ? paper.getAdmin().getName() : null; + String partnerName = (store != null) ? store.getName() : null; + + // 카테고리 결정 로직 그대로 + String finalCategory = null; + if (content != null) { + if (content.getCategory() != null) { + finalCategory = content.getCategory(); + } else if (content.getOptionType() == OptionType.SERVICE) { + List goods = goodsRepository.findByContentId(content.getId()); + if (!goods.isEmpty()) { + finalCategory = goods.get(0).getBelonging(); + } + } + } + + return StudentResponseDTO.UsablePartnershipDTO.builder() + .partnershipId(paper.getId()) + .adminName(adminName) + .partnerName(partnerName) + .criterionType(content != null ? content.getCriterionType() : null) + .optionType(content != null ? content.getOptionType() : null) + .people(content != null ? content.getPeople() : null) + .cost(content != null ? content.getCost() : null) + .category(finalCategory) + .discountRate(content != null ? content.getDiscount() : null) + .build(); + }).toList(); + + return Boolean.FALSE.equals(all) ? result.stream().limit(2).toList() : result; + } + + @Transactional + public void syncUserPapersForStudent(Long studentId) { + Student student = studentRepository.findById(studentId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STUDENT)); + + // 1. 학생 기준으로 admin 찾기 + List admins = adminRepository.findMatchingAdmins( + student.getUniversity(), + student.getDepartment(), + student.getMajor() + ); + + if (admins.isEmpty()) { + return; + } + + List adminIds = admins.stream().map(Admin::getId).toList(); + LocalDate today = LocalDate.now(); + + // 2. admin들이 만든 오늘 유효한 paper 조회 + List papers = paperRepository.findActivePapersByAdminIds( + adminIds, + today, + ActivationStatus.ACTIVE + ); + + // 3. user_paper에 없으면 넣기 + for (Paper paper : papers) { + boolean exists = userPaperRepository.existsByStudentIdAndPaperId(studentId, paper.getId()); + if (exists) continue; + + PaperContent latestContent = paperContentRepository + .findTopByPaperIdOrderByIdDesc(paper.getId()) + .orElse(null); + + UserPaper up = UserPaper.builder() + .paper(paper) + .paperContent(latestContent) + .student(student) + .build(); + + userPaperRepository.save(up); + } + } + + /** + * 전체 학생에 대해 일괄로 user_paper 채워 넣는 메서드 + * (스케줄러에서 이거만 호출하면 됨) + */ + @Transactional + @Override + public void syncUserPapersForAllStudents() { + List students = studentRepository.findAll(); + for (Student s : students) { + syncUserPapersForStudent(s.getId()); + } + } } + diff --git a/src/main/java/com/assu/server/domain/user/service/UserPaperScheduler.java b/src/main/java/com/assu/server/domain/user/service/UserPaperScheduler.java new file mode 100644 index 0000000..70b70cb --- /dev/null +++ b/src/main/java/com/assu/server/domain/user/service/UserPaperScheduler.java @@ -0,0 +1,23 @@ +package com.assu.server.domain.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserPaperScheduler { + + private final StudentServiceImpl studentService; // 또는 StudentService + + /** + * 매일 새벽 3시에 전체 학생의 user_paper를 동기화 + * cron 형식: 초 분 시 일 월 요일 + * "0 0 3 * * *" → 매일 03:00:00 + */ + @Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul") + public void syncAllStudentsDaily() { + studentService.syncUserPapersForAllStudents(); + } + +} From 3182eb4073128eee8a996838d83476ee54eaaaf7 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Sat, 1 Nov 2025 22:56:29 +0900 Subject: [PATCH 263/270] =?UTF-8?q?[FIX/#98]=20QA=20=EB=B0=98=EC=98=81=20a?= =?UTF-8?q?ctive=EB=90=9C=20store=EB=A7=8C=20=EB=9C=A8=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/store/dto/StoreResponseDTO.java | 15 +++---- .../store/repository/StoreRepository.java | 40 ++++++++++++------- .../store/service/StoreServiceImpl.java | 2 +- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java b/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java index 597b036..5a28476 100644 --- a/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java +++ b/src/main/java/com/assu/server/domain/store/dto/StoreResponseDTO.java @@ -19,13 +19,7 @@ public static class WeeklyRankResponseDTO { private Long rank; // 그 주 순위(1부터) private Long usageCount; // 그 주 사용 건수 } - @AllArgsConstructor - @RequiredArgsConstructor - @Builder - @Getter - public static class todayBest{ - List bestStores; - } + @Getter @NoArgsConstructor @AllArgsConstructor @@ -35,5 +29,12 @@ public static class ListWeeklyRankResponseDTO { private String storeName; private List items; // 과거→현재 (6개) } + @AllArgsConstructor + @RequiredArgsConstructor + @Builder + @Getter + public static class todayBest{ + List bestStores; + } } diff --git a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java index 2bf0bc0..7114810 100644 --- a/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/assu/server/domain/store/repository/StoreRepository.java @@ -11,10 +11,12 @@ import java.util.List; public interface StoreRepository extends JpaRepository { - Optional findByPartner(Partner partner); + + Optional findByPartner(Partner partner); Optional findByNameAndAddressAndDetailAddress(String name, String address, String detailAddress); - // [이번 주] 전체 스토어 중 특정 storeId의 주간 순위/건수 1건 + + // [이번 주] 전체 스토어 중 특정 storeId의 주간 순위/건수 1건 (ACTIVE만) @Query(value = """ WITH w AS ( SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AS week_start @@ -26,10 +28,9 @@ per_store AS ( CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount, (SELECT week_start FROM w) AS weekStart FROM store s - LEFT JOIN paper p ON p.store_id = s.id - LEFT JOIN paper_content pc ON pc.paper_id = p.id + LEFT JOIN paper p ON p.store_id = s.id AND p.is_activated = 'ACTIVE' LEFT JOIN partnership_usage pu - ON pu.paper_id = pc.id + ON pu.paper_id = p.id AND pu.created_at >= (SELECT week_start FROM w) AND pu.created_at < (SELECT week_start FROM w) + INTERVAL 7 DAY GROUP BY s.id, s.name @@ -56,7 +57,7 @@ interface GlobalWeeklyRankRow { Long getStoreRank(); } - // [최근 6주] 전체 스토어 기준, 특정 storeId의 주간 순위/건수(월요일 시작) 추세 + // [최근 6주] 전체 스토어 기준, 특정 storeId의 주간 순위/건수(월요일 시작) 추세 (ACTIVE만) @Query(value = """ WITH RECURSIVE weeks AS ( SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY) AS week_start @@ -71,11 +72,10 @@ per_store_week AS ( s.name AS storeName, CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount FROM weeks w - JOIN store s ON 1=1 - LEFT JOIN paper p ON p.store_id = s.id - LEFT JOIN paper_content pc ON pc.paper_id = p.id + JOIN store s ON 1=1 + LEFT JOIN paper p ON p.store_id = s.id AND p.is_activated = 'ACTIVE' LEFT JOIN partnership_usage pu - ON pu.paper_id = pc.id + ON pu.paper_id = p.id AND pu.created_at >= w.week_start AND pu.created_at < w.week_start + INTERVAL 7 DAY GROUP BY w.week_start, s.id, s.name @@ -102,7 +102,6 @@ per_store_week AS ( WHERE s.address = :address AND ((:detail IS NULL AND s.detailAddress IS NULL) OR s.detailAddress = :detail) """) - Optional findBySameAddress( @Param("address") String address, @Param("detail") String detail @@ -119,8 +118,21 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), s.point) List findByNameContainingIgnoreCaseOrderByIdDesc(String name); Optional findByName(String name); Optional findById(Long id); - Optional findByPartnerId(Long partnerId); - -} + // [오늘] 전체 스토어 중 사용 건수 상위 10개 (ACTIVE만) + @Query(value = """ + SELECT s.name + FROM store s + LEFT JOIN paper p ON p.store_id = s.id AND p.is_activated = 'ACTIVE' + LEFT JOIN partnership_usage pu + ON pu.paper_id = p.id + AND pu.created_at >= CURDATE() + AND pu.created_at < CURDATE() + INTERVAL 1 DAY + GROUP BY s.id, s.name + HAVING COUNT(pu.id) > 0 + ORDER BY COUNT(pu.id) DESC, s.id ASC + LIMIT 10 + """, nativeQuery = true) + List findTodayBestStoreNames(); +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java index d1fc5eb..232c21b 100644 --- a/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java +++ b/src/main/java/com/assu/server/domain/store/service/StoreServiceImpl.java @@ -26,7 +26,7 @@ public class StoreServiceImpl implements StoreService { @Override @Transactional public StoreResponseDTO.todayBest getTodayBestStore() { - List bestStores = partnershipUsageRepository.findTodayPopularPartnership(); + List bestStores = storeRepository.findTodayBestStoreNames(); return StoreResponseDTO.todayBest.builder() .bestStores(bestStores) From e259adc86e0e8676569f50fc828d7a39caa5e1c6 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Sat, 1 Nov 2025 23:23:26 +0900 Subject: [PATCH 264/270] =?UTF-8?q?[FIX/#98]=20QA=20=EB=B0=98=EC=98=81=20a?= =?UTF-8?q?dmin=20dashboard=20=EC=A0=9C=ED=9C=B4=20=EB=A7=BA=EC=9D=80=20st?= =?UTF-8?q?ore=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/StudentAdminRepository.java | 42 +++++++-- .../service/StudentAdminServiceImpl.java | 89 +++++++++++++------ .../apiPayload/code/status/ErrorStatus.java | 2 +- 3 files changed, 95 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java index a61a068..0480859 100644 --- a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java +++ b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java @@ -12,6 +12,8 @@ import java.util.List; public interface StudentAdminRepository extends JpaRepository { + + // 총 누적 가입자 수 @Query(""" select count(sa) from StudentAdmin sa @@ -19,7 +21,7 @@ select count(sa) """) Long countAllByAdminId(@Param("adminId") Long adminId); - + // 기간별 가입자 수 @Query(""" select count(sa) from StudentAdmin sa @@ -31,12 +33,14 @@ Long countByAdminIdBetween(@Param("adminId") Long adminId, @Param("from") LocalDateTime from, @Param("to") LocalDateTime to); + // 이번 달 신규 가입자 수 default Long countThisMonthByAdminId(Long adminId) { LocalDateTime from = YearMonth.now().atDay(1).atStartOfDay(); LocalDateTime to = LocalDateTime.now(); return countByAdminIdBetween(adminId, from, to); } - // 오늘 하루, '나를 admin으로 제휴 맺은 partner'의 제휴를 사용한 '고유 사용자 수' + + // 오늘 제휴 사용 고유 사용자 수 @Query(value = """ SELECT COUNT(DISTINCT pu.student_id) FROM partnership_usage pu @@ -48,25 +52,47 @@ SELECT COUNT(DISTINCT pu.student_id) """, nativeQuery = true) Long countTodayUsersByAdmin(@Param("adminId") Long adminId); - // 누적: admin이 제휴한 모든 store의 사용 건수 (0건 포함), 사용량 내림차순 + // 🔧 핵심 수정: Paper 정보를 포함한 사용량 조회 (N+1 해결) + // Paper ID를 함께 반환하여 별도 조회 불필요 @Query(value = """ SELECT + p.id AS paperId, p.store_id AS storeId, s.name AS storeName, + CAST(COUNT(pu.id) AS UNSIGNED) AS usageCount + FROM paper p + JOIN store s ON s.id = p.store_id + JOIN paper_content pc ON pc.paper_id = p.id + JOIN partnership_usage pu ON pu.paper_id = pc.id + WHERE p.admin_id = :adminId + GROUP BY p.id, p.store_id, s.name + HAVING usageCount > 0 + ORDER BY usageCount DESC, p.id ASC + """, nativeQuery = true) + List findUsageByStoreWithPaper(@Param("adminId") Long adminId); + + // 🆕 추가: 0건 포함 조회 (대시보드에서 모든 제휴 업체를 보여줘야 하는 경우) + @Query(value = """ + SELECT + p.id AS paperId, + p.store_id AS storeId, + s.name AS storeName, CAST(COALESCE(COUNT(pu.id), 0) AS UNSIGNED) AS usageCount FROM paper p JOIN store s ON s.id = p.store_id LEFT JOIN paper_content pc ON pc.paper_id = p.id LEFT JOIN partnership_usage pu ON pu.paper_id = pc.id WHERE p.admin_id = :adminId - GROUP BY p.store_id, s.name - ORDER BY usageCount DESC, storeId ASC + GROUP BY p.id, p.store_id, s.name + ORDER BY usageCount DESC, p.id ASC """, nativeQuery = true) - List findUsageByStore(@Param("adminId") Long adminId); + List findUsageByStoreIncludingZero(@Param("adminId") Long adminId); - interface StoreUsage { + // 🔧 Projection 인터페이스: Paper ID 추가 + interface StoreUsageWithPaper { + Long getPaperId(); // 🆕 추가: Paper ID Long getStoreId(); String getStoreName(); Long getUsageCount(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java index 5c91bb1..8728636 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java @@ -6,6 +6,7 @@ import com.assu.server.domain.mapping.dto.StudentAdminResponseDTO; import com.assu.server.domain.mapping.repository.StudentAdminRepository; import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.domain.partnership.repository.PaperRepository; import com.assu.server.domain.partnership.repository.PartnershipRepository; import com.assu.server.domain.user.service.StudentService; import com.assu.server.global.apiPayload.code.status.ErrorStatus; @@ -15,6 +16,8 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @Transactional @@ -22,69 +25,97 @@ public class StudentAdminServiceImpl implements StudentAdminService { private final StudentAdminRepository studentAdminRepository; private final AdminRepository adminRepository; - private final PartnershipRepository partnershipRepository; + private final PaperRepository paperRepository; // 🔧 수정: PaperRepository 사용 @Override @Transactional public StudentAdminResponseDTO.CountAdminAuthResponseDTO getCountAdminAuth(Long memberId) { - + Admin admin = getAdminOrThrow(memberId); Long total = studentAdminRepository.countAllByAdminId(memberId); - Admin admin = adminRepository.findById(memberId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - String adminName = admin.getName(); - return StudentAdminConverter.countAdminAuthDTO(memberId, total, adminName); + return StudentAdminConverter.countAdminAuthDTO(memberId, total, admin.getName()); } + @Override @Transactional public StudentAdminResponseDTO.NewCountAdminResponseDTO getNewStudentCountAdmin(Long memberId) { - + Admin admin = getAdminOrThrow(memberId); Long total = studentAdminRepository.countThisMonthByAdminId(memberId); - Admin admin = adminRepository.findById(memberId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - String adminName = admin.getName(); - return StudentAdminConverter.newCountAdminResponseDTO(memberId, total, adminName); + + return StudentAdminConverter.newCountAdminResponseDTO(memberId, total, admin.getName()); } @Override @Transactional public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson(Long memberId) { - + Admin admin = getAdminOrThrow(memberId); Long total = studentAdminRepository.countTodayUsersByAdmin(memberId); - Admin admin = adminRepository.findById(memberId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - String adminName =admin.getName(); - return StudentAdminConverter.countUsagePersonDTO(memberId, total, adminName); + + return StudentAdminConverter.countUsagePersonDTO(memberId, total, admin.getName()); } @Override @Transactional public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage(Long memberId) { - Admin admin = adminRepository.findById(memberId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - String adminName =admin.getName(); - List storeUsages = studentAdminRepository.findUsageByStore(memberId); + Admin admin = getAdminOrThrow(memberId); + + // 🔧 수정: Paper 정보를 포함한 조회 (N+1 해결) + List storeUsages = + studentAdminRepository.findUsageByStoreWithPaper(memberId); + + // 데이터가 없으면 예외 처리 + if (storeUsages.isEmpty()) { + throw new DatabaseException(ErrorStatus.NO_USAGE_DATA); + } + + // 첫 번째가 가장 사용량이 많은 업체 (ORDER BY usageCount DESC) var top = storeUsages.get(0); - Paper paper = partnershipRepository.findFirstByAdmin_IdAndStore_IdOrderByIdAsc(memberId, top.getStoreId()) + + // 🔧 수정: Paper ID로 직접 조회 (별도 쿼리 불필요) + Paper paper = paperRepository.findById(top.getPaperId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE)); - Long total = top.getUsageCount(); - return StudentAdminConverter.countUsageResponseDTO(admin, paper, total); + return StudentAdminConverter.countUsageResponseDTO(admin, paper, top.getUsageCount()); } @Override @Transactional public StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList(Long memberId) { + Admin admin = getAdminOrThrow(memberId); - Admin admin = adminRepository.findById(memberId) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - List storeUsages = studentAdminRepository.findUsageByStore(memberId); + // 🔧 핵심 수정: Paper 정보를 포함한 조회 (N+1 해결) + List storeUsages = + studentAdminRepository.findUsageByStoreWithPaper(memberId); + + if (storeUsages.isEmpty()) { + // 빈 리스트 반환 (선택: 예외 처리도 가능) + return StudentAdminConverter.countUsageListResponseDTO(List.of()); + } + + // 🔧 핵심 개선: Paper ID 목록을 한 번에 조회 (Batch Query) + List paperIds = storeUsages.stream() + .map(StudentAdminRepository.StoreUsageWithPaper::getPaperId) + .toList(); + + // 🔧 한 번의 IN 쿼리로 모든 Paper 조회 + Map paperMap = paperRepository.findAllById(paperIds).stream() + .collect(Collectors.toMap(Paper::getId, paper -> paper)); + + // 🔧 Paper 조회 없이 매핑만 수행 (N+1 완전 해결) var items = storeUsages.stream().map(row -> { - Paper paper = partnershipRepository.findFirstByAdmin_IdAndStore_IdOrderByIdAsc(memberId, row.getStoreId()) - .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE)); + Paper paper = paperMap.get(row.getPaperId()); + if (paper == null) { + throw new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE); + } return StudentAdminConverter.countUsageResponseDTO(admin, paper, row.getUsageCount()); }).toList(); + return StudentAdminConverter.countUsageListResponseDTO(items); } -} + // Admin 조회 중복 제거 + private Admin getAdminOrThrow(Long adminId) { + return adminRepository.findById(adminId) + .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); + } +} \ No newline at end of file diff --git a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java index 7ca4c04..9e396c5 100644 --- a/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/assu/server/global/apiPayload/code/status/ErrorStatus.java @@ -114,7 +114,7 @@ public enum ErrorStatus implements BaseErrorCode { REVIEW_REPORT_SELF_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REPORT_4003", "자신의 리뷰를 신고할 수 없습니다."), SUGGESTION_REPORT_SELF_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REPORT_4004", "자신의 건의글을 신고할 수 없습니다."), INVALID_REPORT_TYPE(HttpStatus.BAD_REQUEST, "REPORT_4005", "유효하지 않은 신고 타입입니다."), - ; + NO_USAGE_DATA(HttpStatus.NOT_FOUND, "ADMIN4001", "해당 관리자의 제휴 이용 내역이 없습니다."); private final HttpStatus httpStatus; private final String code; From 7a9d6d0d31f86bbb8700003b9d397a8635353927 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Sat, 1 Nov 2025 23:25:32 +0900 Subject: [PATCH 265/270] =?UTF-8?q?[FIX/#98]=20QA=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/repository/StudentAdminRepository.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java index 0480859..bac1def 100644 --- a/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java +++ b/src/main/java/com/assu/server/domain/mapping/repository/StudentAdminRepository.java @@ -52,8 +52,6 @@ SELECT COUNT(DISTINCT pu.student_id) """, nativeQuery = true) Long countTodayUsersByAdmin(@Param("adminId") Long adminId); - // 🔧 핵심 수정: Paper 정보를 포함한 사용량 조회 (N+1 해결) - // Paper ID를 함께 반환하여 별도 조회 불필요 @Query(value = """ SELECT p.id AS paperId, @@ -71,7 +69,7 @@ SELECT COUNT(DISTINCT pu.student_id) """, nativeQuery = true) List findUsageByStoreWithPaper(@Param("adminId") Long adminId); - // 🆕 추가: 0건 포함 조회 (대시보드에서 모든 제휴 업체를 보여줘야 하는 경우) + // 0건 포함 조회 (대시보드에서 모든 제휴 업체를 보여줘야 하는 경우) @Query(value = """ SELECT p.id AS paperId, @@ -88,7 +86,6 @@ SELECT COUNT(DISTINCT pu.student_id) """, nativeQuery = true) List findUsageByStoreIncludingZero(@Param("adminId") Long adminId); - // 🔧 Projection 인터페이스: Paper ID 추가 interface StoreUsageWithPaper { Long getPaperId(); // 🆕 추가: Paper ID Long getStoreId(); From bc74896558c9a313a956cedcfa7d4bf76c2341fc Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Sat, 1 Nov 2025 23:26:42 +0900 Subject: [PATCH 266/270] =?UTF-8?q?[FIX/#98]=20QA=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/service/StudentAdminServiceImpl.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java index 8728636..2eb03ee 100644 --- a/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java +++ b/src/main/java/com/assu/server/domain/mapping/service/StudentAdminServiceImpl.java @@ -25,7 +25,7 @@ public class StudentAdminServiceImpl implements StudentAdminService { private final StudentAdminRepository studentAdminRepository; private final AdminRepository adminRepository; - private final PaperRepository paperRepository; // 🔧 수정: PaperRepository 사용 + private final PaperRepository paperRepository; @Override @Transactional @@ -59,11 +59,10 @@ public StudentAdminResponseDTO.CountUsagePersonResponseDTO getCountUsagePerson(L public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage(Long memberId) { Admin admin = getAdminOrThrow(memberId); - // 🔧 수정: Paper 정보를 포함한 조회 (N+1 해결) List storeUsages = studentAdminRepository.findUsageByStoreWithPaper(memberId); - // 데이터가 없으면 예외 처리 + //예외 처리 if (storeUsages.isEmpty()) { throw new DatabaseException(ErrorStatus.NO_USAGE_DATA); } @@ -71,7 +70,6 @@ public StudentAdminResponseDTO.CountUsageResponseDTO getCountUsage(Long memberId // 첫 번째가 가장 사용량이 많은 업체 (ORDER BY usageCount DESC) var top = storeUsages.get(0); - // 🔧 수정: Paper ID로 직접 조회 (별도 쿼리 불필요) Paper paper = paperRepository.findById(top.getPaperId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_PAPER_FOR_STORE)); @@ -92,16 +90,13 @@ public StudentAdminResponseDTO.CountUsageListResponseDTO getCountUsageList(Long return StudentAdminConverter.countUsageListResponseDTO(List.of()); } - // 🔧 핵심 개선: Paper ID 목록을 한 번에 조회 (Batch Query) List paperIds = storeUsages.stream() .map(StudentAdminRepository.StoreUsageWithPaper::getPaperId) .toList(); - // 🔧 한 번의 IN 쿼리로 모든 Paper 조회 Map paperMap = paperRepository.findAllById(paperIds).stream() .collect(Collectors.toMap(Paper::getId, paper -> paper)); - // 🔧 Paper 조회 없이 매핑만 수행 (N+1 완전 해결) var items = storeUsages.stream().map(row -> { Paper paper = paperMap.get(row.getPaperId()); if (paper == null) { From 4d1d7268f24be64e6e0e36ff6d440a6952cf5d95 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sun, 2 Nov 2025 15:35:20 +0900 Subject: [PATCH 267/270] =?UTF-8?q?[refactor/#220]=20note=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/map/dto/MapResponseDTO.java | 1 + .../domain/map/service/MapServiceImpl.java | 1 + .../converter/PartnershipConverter.java | 24 ++++++++++- .../dto/PartnershipRequestDTO.java | 1 + .../dto/PartnershipResponseDTO.java | 1 + .../partnership/entity/PaperContent.java | 2 + .../service/PaperQueryServiceImpl.java | 11 ----- .../service/PartnershipServiceImpl.java | 43 +------------------ 8 files changed, 30 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java index 1277b40..23d63f4 100644 --- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java +++ b/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java @@ -61,6 +61,7 @@ public static class StoreMapResponseDTO { private Integer people; private Long cost; private String category; + private String note; private Long discountRate; private boolean hasPartner; private Double latitude; diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java index 9d2c66b..2e93605 100644 --- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java +++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java @@ -223,6 +223,7 @@ else if (content.getOptionType() == OptionType.SERVICE) { .adminName(admin != null ? admin.getName() : null) .adminId(adminId) .name(s.getName()) + .note(content.getNote()) .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress()) .rate(s.getRate()) .criterionType(content != null ? content.getCriterionType() : null) diff --git a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java index 4adbe94..e62f263 100644 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ b/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java @@ -56,6 +56,7 @@ public static List toPaperContents( } return partnershipRequestDTO.getOptions().stream() .map(optionDto -> PaperContent.builder() + .note(optionDto.getNote()) // 일단 노트까지 받아서 변환 .paper(paper) // 어떤 Paper에 속하는지 연결 .optionType(optionDto.getOptionType()) .criterionType(optionDto.getCriterionType()) @@ -102,7 +103,14 @@ public static List toContentR public static PaperContentResponseDTO.storePaperContentResponse toContentResponse(PaperContent content) { List goodsList = extractGoods(content); Integer peopleValue = extractPeople(content); - String paperContentText = buildPaperContentText(content, goodsList, peopleValue); + + String paperContentText; + if(content.getNote()!= null){ + paperContentText = content.getNote(); + }else{ + paperContentText = buildPaperContentText(content, goodsList, peopleValue); + } + return PaperContentResponseDTO.storePaperContentResponse.builder() .adminId(content.getPaper().getAdmin().getId()) @@ -204,6 +212,7 @@ public static List toPaperContentsForManual( .paper(paper) .optionType(o.getOptionType()) .criterionType(o.getCriterionType()) + .note(o.getNote()) .people(o.getPeople()) .cost(o.getCost()) .category(o.getCategory()) @@ -238,6 +247,11 @@ public static PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershi if (contents != null) { for (int i = 0; i < contents.size(); i++) { PaperContent pc = contents.get(i); + + String note = null; + if(pc.getNote()!= null){ + note = pc.getNote(); + } List goods = (goodsBatches != null && goodsBatches.size() > i) ? goodsBatches.get(i) : List.of(); optionDTOS.add( @@ -245,6 +259,7 @@ public static PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershi .optionType(pc.getOptionType()) .criterionType(pc.getCriterionType()) .people(pc.getPeople()) + .note(note) .cost(pc.getCost()) .category(pc.getCategory()) .discountRate(pc.getDiscount()) @@ -253,6 +268,8 @@ public static PartnershipResponseDTO.WritePartnershipResponseDTO writePartnershi ); } } + + return PartnershipResponseDTO.WritePartnershipResponseDTO.builder() .partnershipId(paper.getId()) .partnershipPeriodStart(paper.getPartnershipPeriodStart()) @@ -319,6 +336,10 @@ public static PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartners if (contents != null) { for (int i = 0; i < contents.size(); i++) { PaperContent pc = contents.get(i); + String note = null; + if(pc.getNote()!= null){ + note = pc.getNote(); + } List goods = (goodsBatches != null && goodsBatches.size() > i) ? goodsBatches.get(i) : List.of(); optionDTOS.add( @@ -327,6 +348,7 @@ public static PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartners .criterionType(pc.getCriterionType()) .people(pc.getPeople()) .cost(pc.getCost()) + .note(note) .category(pc.getCategory()) .discountRate(pc.getDiscount()) .goods(goodsResultDTO(goods)) diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java index 79a795b..65dea83 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java @@ -40,6 +40,7 @@ public static class PartnershipOptionRequestDTO { private Long cost; private String category; private Long discountRate; + private String note; private List goods; // 서비스 제공 항목 } diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java index 35ac7ae..712273c 100644 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java @@ -40,6 +40,7 @@ public static class PartnershipOptionResponseDTO { private CriterionType criterionType; private Integer people; private Long cost; + private String note; private String category; private Long discountRate; diff --git a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java index 8af76bf..94cd973 100644 --- a/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java +++ b/src/main/java/com/assu/server/domain/partnership/entity/PaperContent.java @@ -33,6 +33,8 @@ public class PaperContent extends BaseEntity { @Enumerated(EnumType.STRING) private OptionType optionType; + private String note; + private Integer people; private Long cost; diff --git a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java index 8cef7ca..8d32e65 100644 --- a/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java +++ b/src/main/java/com/assu/server/domain/partnership/service/PaperQueryServiceImpl.java @@ -50,17 +50,6 @@ public PaperResponseDTO.partnershipContent getStorePaperContent(Long storeId, Me student.getDepartment(), student.getMajor()); - // // 한번 더 거르기 위해서 - // List filteredAdmin = adminList.stream() - // .filter(admin -> { - // String name = admin.getName(); - // Major major = admin.getMajor(); - // return name.contains(student.getUniversity()) - // || name.contains(student.getDepartment()) - // || major.equals(student.getMajor()); - // }).toList(); - - // 추출한 admin, store와 일치하는 paperId 를 추출합니다. List paperList = adminList.stream() .flatMap(admin -> 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 89955e4..76679a2 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 @@ -101,48 +101,7 @@ public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Membe // @Transactional 환경에서는 studentsToUpdate의 변경 사항(스탬프)이 자동으로 DB에 반영됩니다. } - // public void recordPartnershipUsage(PartnershipRequestDTO.finalRequest dto, Member member){ - // - // Student requestStudent = studentRepository.findById(member.getId()).orElseThrow( - // () -> new GeneralException(ErrorStatus.NO_SUCH_STUDENT) // 혹은 적절한 예외 처리 - // ); - // - // List usages = new ArrayList<>(); - // - // PaperContent content = contentRepository.findById(dto.getContentId()).orElseThrow( - // () -> new GeneralException(ErrorStatus.NO_SUCH_CONTENT) - // ); - // Long paperId = content.getPaper().getId(); - // // 1) 요청한 member 본인 - // usages.add(PartnershipConverter.toPartnershipUsage(dto, requestStudent, paperId)); - // requestStudent.setStamp(); - // System.out.println("update 된 stamp : "+requestStudent.getStamp()); - // - // List userIds = Optional.ofNullable(dto.getUserIds()).orElse(Collections.emptyList()); - // // 2) dto의 userIds에 있는 다른 사용자들 - // for (Long userId : userIds) { - // if(userId != member.getId()){ - // Student student = studentRepository.getReferenceById(userId); - // usages.add(PartnershipConverter.toPartnershipUsage(dto, student, paperId)); - // student.setStamp(); - // } - // - // } - // partnershipUsageRepository.saveAll(usages); - // - // // Store store = storeRepository.findById(dto.getStoreId()).orElseThrow( - // // () -> new GeneralException(ErrorStatus.NO_SUCH_STORE) - // // ); - // // Partner partner = store.getPartner(); - // // if (partner != null) { - // // Long partnerId = partner.getId(); - // // System.out.println("알림 요청이 들어갑니다."); - // // notificationService.sendOrder(partnerId, 0L, dto.getTableNumber(), dto.getPartnershipContent()); - // // - // // } else { - // // throw new GeneralException(ErrorStatus.NO_SUCH_PARTNER); - // // } - // } + From e9e51eadb436bad92c65c74e1afcf58e613cd57b Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sun, 2 Nov 2025 15:59:25 +0900 Subject: [PATCH 268/270] =?UTF-8?q?[refactor/#220]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20note=20=EB=82=B4=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/assu/server/domain/user/dto/StudentResponseDTO.java | 1 + .../assu/server/domain/user/service/StudentServiceImpl.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java index 38cf04b..f06c74c 100644 --- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java @@ -80,6 +80,7 @@ public static class UsablePartnershipDTO { private OptionType optionType; private Integer people; private Long cost; + private String note; private String category; private Long discountRate; } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java index d41dedc..afda921 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java @@ -144,7 +144,11 @@ public List getUsablePartnership(Long m // 카테고리 결정 로직 그대로 String finalCategory = null; + String note = null; if (content != null) { + if(content.getNote() != null){ + note = content.getNote(); + } if (content.getCategory() != null) { finalCategory = content.getCategory(); } else if (content.getOptionType() == OptionType.SERVICE) { @@ -159,6 +163,7 @@ public List getUsablePartnership(Long m .partnershipId(paper.getId()) .adminName(adminName) .partnerName(partnerName) + .note(note) .criterionType(content != null ? content.getCriterionType() : null) .optionType(content != null ? content.getOptionType() : null) .people(content != null ? content.getPeople() : null) From be58ee6a5c59b6b80917f692e5a3f4215c27eaf0 Mon Sep 17 00:00:00 2001 From: eeeeeaaan Date: Sun, 2 Nov 2025 21:04:54 +0900 Subject: [PATCH 269/270] =?UTF-8?q?[refactor/#220]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20paper=5Fid=20=EC=B6=94=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EB=82=B4=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/assu/server/domain/user/dto/StudentResponseDTO.java | 1 + .../com/assu/server/domain/user/service/StudentServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java index f06c74c..d415f75 100644 --- a/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java +++ b/src/main/java/com/assu/server/domain/user/dto/StudentResponseDTO.java @@ -81,6 +81,7 @@ public static class UsablePartnershipDTO { private Integer people; private Long cost; private String note; + private Long paperId; private String category; private Long discountRate; } diff --git a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java index afda921..a13eb1f 100644 --- a/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java +++ b/src/main/java/com/assu/server/domain/user/service/StudentServiceImpl.java @@ -163,7 +163,7 @@ public List getUsablePartnership(Long m .partnershipId(paper.getId()) .adminName(adminName) .partnerName(partnerName) - .note(note) + .note(note).paperId(content != null? content.getPaper().getId(): null) .criterionType(content != null ? content.getCriterionType() : null) .optionType(content != null ? content.getOptionType() : null) .people(content != null ? content.getPeople() : null) From d660c298e800bdc3fa0f653667792982f7468001 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:53:00 +1100 Subject: [PATCH 270/270] =?UTF-8?q?[FEAT/#217]=20-=20=EB=94=94=EB=B0=94?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=ED=86=A0=ED=81=B0=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deviceToken/entity/DeviceToken.java | 2 +- .../repository/DeviceTokenRepository.java | 6 +++ .../service/DeviceTokenServiceImpl.java | 42 ++++++++++++++----- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java index 4d98454..f0ce00f 100644 --- a/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java +++ b/src/main/java/com/assu/server/domain/deviceToken/entity/DeviceToken.java @@ -18,7 +18,7 @@ public class DeviceToken extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="member_id", nullable=false) private Member member; - @Column(nullable=false, length=200, unique=true) + @Column(nullable=false, length=200) private String token; @Setter diff --git a/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java index ee0c834..95c1fcf 100644 --- a/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java +++ b/src/main/java/com/assu/server/domain/deviceToken/repository/DeviceTokenRepository.java @@ -20,4 +20,10 @@ public interface DeviceTokenRepository extends JpaRepository void deactivateTokens(@Param("tokens") List tokens); Optional findByToken(String token); + + // 같은 회원 + 같은 토큰 있는지 확인 + Optional findByMemberIdAndToken(Long memberId, String token); + + // 같은 회원이 가진 모든 토큰 (비활성화용) + List findAllByMemberId(Long memberId); } diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java index 9d4cabb..fe37ddd 100644 --- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java +++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java @@ -21,19 +21,41 @@ public class DeviceTokenServiceImpl implements DeviceTokenService { @Transactional @Override public Long register(String tokenId, Long memberId) { - Member member = memberRepository.findMemberById(memberId).orElseThrow( - () -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER) - ); - if (member == null) { - throw new DatabaseException(ErrorStatus.NO_SUCH_MEMBER); + Member member = memberRepository.findMemberById(memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)); + + // 1) 같은 회원 + 같은 토큰이 이미 있으면 → active = true 로만 복구 + // (가장 정확한 쿼리: findByMemberIdAndToken) + var sameTokenOpt = deviceTokenRepository.findByMemberIdAndToken(memberId, tokenId); + if (sameTokenOpt.isPresent()) { + DeviceToken exist = sameTokenOpt.get(); + exist.setActive(true); + deviceTokenRepository.save(exist); + return exist.getId(); + } + + // 2) 같은 회원 + 다른 토큰 → 그 회원의 기존 active 토큰 전부 비활성화 + // (현재 보유 메서드 활용: 활성 토큰 문자열 가져와 deactivate) + var activeTokens = deviceTokenRepository.findActiveTokensByMemberId(memberId); + if (!activeTokens.isEmpty()) { + // 현재 등록하려는 tokenId 와 다른 것들만 비활성화 + var toDeactivate = activeTokens.stream() + .filter(t -> !t.equals(tokenId)) + .toList(); + if (!toDeactivate.isEmpty()) { + deviceTokenRepository.deactivateTokens(toDeactivate); + } } - DeviceToken dt = deviceTokenRepository.findByToken(tokenId) - .map(deviceToken -> { deviceToken.setActive(true); return deviceToken; }) - .orElse(DeviceToken.builder().member(member).token(tokenId).active(true).build()); - deviceTokenRepository.save(dt); + // 3) 새 토큰 insert (다른 회원이 같은 토큰을 갖고 있어도 상관 없이 insert) + DeviceToken newToken = DeviceToken.builder() + .member(member) + .token(tokenId) + .active(true) + .build(); + deviceTokenRepository.save(newToken); - return dt.getId(); + return newToken.getId(); } @Transactional