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 0ea470cd..b6ed493e 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,27 +6,37 @@ 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.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Admin", description = "관리자 관련 API") @RestController -@RequestMapping("/admin") @RequiredArgsConstructor +@RequestMapping("/admin") public class AdminController { private final AdminService adminService; @Operation( - summary = "파트너 추천 API", - description = "제휴하지 않은 파트너 중 한 곳을 랜덤으로 조회합니다." - ) + summary = "제휴업체 추천 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2591197c19ed80f5b05cffcfecef9c24?source=copy_link)\n" + + "- 현재 로그인 한 관리자와 제휴하지 않은 제휴업체 중 한 곳을 랜덤으로 조회합니다.\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `RandomPartnerResponse` 객체 반환\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `partnerAddress` (String): 제휴업체 주소\n" + + " - `partnerDetailAddress` (String): 제휴업체 상세주소\n" + + " - `partnerName` (String): 제휴업체 상호명\n" + + " - `partnerUrl` (String): 제휴업체 카카오맵 URL\n" + + " - `partnerPhone` (String): 제휴업체 전화번호\n") @GetMapping("/partner-recommend") - public BaseResponse randomPartnerRecommend( + public BaseResponse randomPartnerRecommend( @AuthenticationPrincipal PrincipalDetails pd - ) { + ) { return BaseResponse.onSuccess(SuccessStatus._OK, adminService.suggestRandomPartner(pd.getId())); } } diff --git a/src/main/java/com/assu/server/domain/admin/converter/AdminConverter.java b/src/main/java/com/assu/server/domain/admin/converter/AdminConverter.java deleted file mode 100644 index 082e324d..00000000 --- a/src/main/java/com/assu/server/domain/admin/converter/AdminConverter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.admin.converter; - -public class AdminConverter { -} diff --git a/src/main/java/com/assu/server/domain/admin/dto/AdminRequestDTO.java b/src/main/java/com/assu/server/domain/admin/dto/AdminRequestDTO.java deleted file mode 100644 index bf5c379a..00000000 --- a/src/main/java/com/assu/server/domain/admin/dto/AdminRequestDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.admin.dto; - -public class AdminRequestDTO { -} 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 6aa0df16..04d6bf80 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,22 +1,37 @@ 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; - private String partnerUrl; - private String partnerPhone; +import com.assu.server.domain.partner.entity.Partner; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record AdminResponseDTO ( + @Schema(description = "제휴업체 ID", example = "101") + @NotNull Long partnerId, + + @Schema(description = "제휴업체 이름", example = "역전할머니 맥주 숭실대점") + @NotNull String partnerName, + + @Schema(description = "제휴업체 주소", example = "서울특별시 동작구") + @NotNull String partnerAddress, + + @Schema(description = "제휴업체 상세주소", example = "2층 201호") + String partnerDetailAddress, + + @Schema(description = "제휴업체 URL", example = "https://www.beer.co.kr") + String partnerUrl, + + @Schema(description = "제휴업체 전화번호", example = "02-123-4567") + String partnerPhone +) { + public static AdminResponseDTO from(Partner partner) { + return new AdminResponseDTO( + partner.getId(), + partner.getName(), + partner.getAddress(), + partner.getDetailAddress(), + partner.getMember() != null ? partner.getMember().getProfileUrl() : null, + partner.getMember() != null ? partner.getMember().getPhoneNum() : null + ); } + } 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 875e5265..f7ae09e8 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,18 +1,9 @@ package com.assu.server.domain.admin.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.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; -import jakarta.persistence.Enumerated; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.MapsId; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Id; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.*; @@ -38,13 +29,16 @@ public class Admin { @JoinColumn(name = "id") private Member member; + @Column(name = "name", length = 255, nullable = false) private String name; + @Column(name = "office_address", length = 255, nullable = false) private String officeAddress; + @Column(name = "detail", length = 255) private String detailAddress; - private String signUrl; + private String signImageUrl; private Boolean isSignVerified; @@ -66,7 +60,7 @@ public class Admin { private double latitude; private double longitude; - public void setMember(Member member) { + public void updateMember(Member member) { this.member = member; } } 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 8a1eaeeb..21fc458d 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 @@ -4,24 +4,25 @@ import java.util.Optional; 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 com.assu.server.domain.admin.entity.Admin; 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; public interface AdminRepository extends JpaRepository { - // 여기 예원이 머지하고 수정 - @Query("SELECT a FROM Admin a WHERE " + - "(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)") + @Query(""" + SELECT a FROM Admin a + WHERE a.university = :university + AND ( + (a.department IS NULL AND a.major IS NULL) OR + (a.department = :department AND a.major IS NULL) OR + (a.department = :department AND a.major = :major) + ) + """) List findMatchingAdmins(@Param("university") University university, @Param("department") Department department, @Param("major") Major major); @@ -32,11 +33,12 @@ List findMatchingAdmins(@Param("university") University university, @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 + WHERE NOT EXISTS ( + SELECT 1 FROM paper pa + WHERE pa.admin_id = a.id + AND pa.partner_id = :partnerId + AND pa.is_activated = 'ACTIVE' + ) """, nativeQuery = true) long countPartner(@Param("partnerId") Long partnerId); @@ -44,35 +46,34 @@ SELECT COUNT(*) @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 + WHERE NOT EXISTS ( + SELECT 1 FROM paper pa + WHERE pa.admin_id = a.id + AND pa.partner_id = :partnerId + AND pa.is_activated = 'ACTIVE' + ) LIMIT :offset, :limit """, nativeQuery = true) List findPartnerWithOffset(@Param("partnerId") Long partnerId, - @Param("offset") int offset, - @Param("limit") int limit); + @Param("offset") int offset, + @Param("limit") int limit); - @Query(value = """ - SELECT a.* - FROM admin a + @Query(""" + SELECT DISTINCT a + FROM Admin a + LEFT JOIN FETCH a.member WHERE a.point IS NOT NULL - AND ST_Contains(ST_GeomFromText(:wkt, 4326), a.point) - """, nativeQuery = true) - List findAllWithinViewport(@Param("wkt") String wkt); + AND function('ST_Contains', function('ST_GeomFromText', :wkt, 4326), a.point) = true + """) + List findAllWithinViewportWithMember(@Param("wkt") String wkt); @Query(""" - select distinct a - from Admin a - where lower(a.name) like lower(concat('%', :keyword, '%')) + SELECT DISTINCT a + FROM Admin a + LEFT JOIN FETCH a.member + WHERE LOWER(a.name) LIKE LOWER(CONCAT('%', :keyword, '%')) """) - List searchAdminByKeyword( + List searchAdminByKeywordWithMember( @Param("keyword") String keyword ); - - Long member(Member member); - - Optional findById(Long id); } 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 32da6ff1..ece352c2 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 @@ -12,6 +12,6 @@ public interface AdminService { List findMatchingAdmins(University university, Department department, Major major); - AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId); + AdminResponseDTO 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 cfcdb411..3ccd57a9 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,6 +1,5 @@ package com.assu.server.domain.admin.service; - import java.util.List; import org.springframework.stereotype.Service; import com.assu.server.domain.admin.dto.AdminResponseDTO; @@ -8,15 +7,15 @@ 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; +import org.springframework.transaction.annotation.Transactional; +import java.util.concurrent.ThreadLocalRandom; @Service @RequiredArgsConstructor @@ -24,18 +23,19 @@ public class AdminServiceImpl implements AdminService { private final AdminRepository adminRepository; private final PartnerRepository partnerRepository; + @Override @Transactional public List findMatchingAdmins(University university, Department department, Major major){ - - List adminList = adminRepository.findMatchingAdmins(university, department,major); + List adminList = adminRepository.findMatchingAdmins(university, department, major); return adminList; } + @Override - @Transactional - public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long adminId) { + @Transactional(readOnly = true) + public AdminResponseDTO suggestRandomPartner(Long adminId) { Admin admin = adminRepository.findById(adminId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); @@ -52,14 +52,7 @@ public AdminResponseDTO.RandomPartnerResponseDTO suggestRandomPartner(Long admin throw new DatabaseException(ErrorStatus.NO_AVAILABLE_PARTNER); } - return AdminResponseDTO.RandomPartnerResponseDTO.builder() - .partnerId(picked.getId()) - .partnerName(picked.getName()) - .partnerAddress(picked.getAddress()) - .partnerDetailAddress(picked.getDetailAddress()) - .partnerUrl(picked.getMember().getProfileUrl()) - .partnerPhone(picked.getMember().getPhoneNum()) - .build(); + return AdminResponseDTO.from(picked); } } 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 8ca40939..75ac4f50 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 @@ -247,7 +247,7 @@ public SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile si .name(info.name()) .officeAddress(address) .detailAddress(info.detailAddress()) - .signUrl(signUrl) + .signImageUrl(signUrl) .point(point) .latitude(lat) .longitude(lng) 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 c19890c1..f090ee90 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,8 +1,11 @@ package com.assu.server.domain.map.controller; import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.map.dto.AdminMapResponseDTO; import com.assu.server.domain.map.dto.MapRequestDTO; -import com.assu.server.domain.map.dto.MapResponseDTO; +import com.assu.server.domain.map.dto.PartnerMapResponseDTO; +import com.assu.server.domain.map.dto.PlaceSuggestionDTO; +import com.assu.server.domain.map.dto.StoreMapResponseDTO; import com.assu.server.domain.map.service.MapService; import com.assu.server.domain.map.service.PlaceSearchService; import com.assu.server.global.apiPayload.BaseResponse; @@ -10,6 +13,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.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -17,6 +21,7 @@ import java.util.List; +@Tag(name = "Map", description = "지도 API") @RestController @RequiredArgsConstructor @RequestMapping("/map") @@ -27,11 +32,67 @@ public class MapController { @Operation( summary = "주변 장소 조회 API", - description = "공간 인덱싱에 들어갈 좌표 4개를 경도, 위도 순서로 입력해주세요 (user -> store 조회 / admin -> partner 조회 / partner -> admin 조회)" - ) + description = "# [v1.3 (2025-01-04)](https://clumsy-seeder-416.notion.site/2441197c19ed80bcb55fcad675dd9837?source=copy_link)\n" + + "- 로그인한 유저의 역할에 따라 Map 객체를 반환합니다.\n" + + "- 경도, 위도 순서로 입력한 Viewport 객체 입력.\n" + + "- 성공 시 200(OK)과 Map 객체 반환.\n"+ + "\n**Request Body:**\n" + + " - `viewport` 객체 (JSON, required): 공간인덱싱을 위한 경도, 위도 객체\n" + + " - `lng1` (double): 좌 상단 경도\n" + + " - `lat1` (double): 좌 상단 위도\n" + + " - `lng2` (double): 우 상단 경도\n" + + " - `lat2` (double): 우 상단 위도\n" + + " - `lng3` (double): 우 하단 경도\n" + + " - `lat3` (double): 우 하단 위도\n" + + " - `lng4` (double): 좌 하단 경도\n" + + " - `lat4` (double): 좌 하단 위도\n" + + "\n**Response:**\n" + + " - `User`: 가게 조회\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `adminId` (Long): 관리자 ID\n" + + " - `adminName` (String): 관리자 이름\n" + + " - `name` (String): 가게 이름\n" + + " - `address` (String): 가게 주소\n" + + " - `rate` (Integer): 가게 별점\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준(PRICE/HEADCOUNT)\n" + + " - `optionType` (OptionType): 제공 서비스 종류(SERVICE/DISCOUNT)\n" + + " - `people` (Integer): 인원 수(HEADCOUNT)\n" + + " - `cost` (Long): 가격(PRICE)\n" + + " - `category` (String): 카테고리(SERVICE)\n" + + " - `note` (String): 제휴 설명 (형식에 맞지 않는 제휴일때)\n" + + " - `discountRate` (Long): 할인률(DISCOUNT)\n" + + " - `hasPartner` (boolean): 제휴업체인지 여부\n" + + " - `latitude` (Double): 가게 위치 위도\n" + + " - `longitude` (Double): 가게 위치 경도\n" + + " - `profileUrl` (String): 가게 카카오맵 URL\n" + + " - `phoneNumber` (String): 가게 전화번호\n" + + " - `Admin`: 제휴업체 조회\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `name` (String): 제휴업체 이름\n" + + " - `address` (String): 제휴업체 주소\n" + + " - `isPartnered` (boolean): 관리자와 제휴 여부\n" + + " - `partnershipId` (Long): 제휴 ID\n" + + " - `partnershipStartDate` (LocalDate): 제휴 시작일\n" + + " - `partnershipEndDate` (LocalDate): 제휴 마감일\n" + + " - `latitude` (Double): 제휴업체 위도\n" + + " - `longitude` (Double): 제휴업체 경도\n" + + " - `profileUrl` (String): 제휴업체 카카오맵 Url\n" + + " - `phoneNumber` (String): 제휴업체 전화번호\n" + + " - `Partner`: 관리자 조회\n" + + " - `adminId` (Long): 관리자 ID\n" + + " - `name` (String): 관리자 이름\n" + + " - `address` (String): 관리자 주소\n" + + " - `isPartnered` (boolean): 제휴업체와 제휴 여부\n" + + " - `partnershipId` (Long): 제휴 ID\n" + + " - `partnershipStartDate` (LocalDate): 제휴 시작일\n" + + " - `partnershipEndDate` (LocalDate): 제휴 마감일\n" + + " - `latitude` (Double): 관리자 위도\n" + + " - `longitude` (Double): 관리자 경도\n" + + " - `profileUrl` (String): 관리자 카카오맵 Url\n" + + " - `phoneNumber` (String): 관리자 전화번호\n") @GetMapping("/nearby") public BaseResponse getLocations( - @ModelAttribute MapRequestDTO.ViewOnMapDTO viewport, + @ModelAttribute MapRequestDTO viewport, @AuthenticationPrincipal PrincipalDetails pd ) { Long memberId = pd.getMember().getId(); @@ -46,22 +107,70 @@ public BaseResponse getLocations( } @Operation( - summary = "주변 장소 조회 API", - description = "공간 인덱싱에 들어갈 좌표 4개를 경도, 위도 순서로 입력해주세요 (user -> store 조회 / admin -> partner 조회 / partner -> admin 조회)" + summary = "주변 장소 조회 API (학생 혜택 기반)", + description = "공간 인덱싱에 들어갈 좌표 4개를 경도, 위도 순서로 입력해주세요 (user -> store 조회)" ) @GetMapping("/nearby/v2") public BaseResponse getLocationsV2( - @ModelAttribute MapRequestDTO.ViewOnMapDTO viewport, + @ModelAttribute MapRequestDTO viewport, @AuthenticationPrincipal PrincipalDetails pd ) { Long memberId = pd.getMember().getId(); - return BaseResponse.onSuccess(SuccessStatus._OK, mapService.getStoresV2(viewport, memberId)); + return BaseResponse.onSuccess(SuccessStatus._OK, mapService.getStores(viewport, memberId)); } @Operation( summary = "검색어 기반 장소 조회 API", - description = "검색어를 입력해주세요. (user → store 전체조회 / admin → partner 전체조회 / partner → admin 전체조회)" - ) + description = "# [v1.3 (2025-01-04)](https://clumsy-seeder-416.notion.site/2591197c19ed8017adf1f3d711bab3d4)\n" + + "- 로그인한 유저의 역할과 검색어에 따라 Map 객체를 반환합니다.\n" + + "- 검색어(searchKeyword) 입력.\n" + + "- 성공 시 200(OK)과 Map 객체 반환.\n"+ + "\n**Request Parts:**\n" + + " - `searchKeyword` (String, required): 검색어\n" + + "\n**Response:**\n" + + " - `User`: 가게 조회\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `adminId` (Long): 관리자 ID\n" + + " - `adminName` (String): 관리자 이름\n" + + " - `name` (String): 가게 이름\n" + + " - `address` (String): 가게 주소\n" + + " - `rate` (Integer): 가게 별점\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준(PRICE/HEADCOUNT)\n" + + " - `optionType` (OptionType): 제공 서비스 종류(SERVICE/DISCOUNT)\n" + + " - `people` (Integer): 인원 수(HEADCOUNT)\n" + + " - `cost` (Long): 가격(PRICE)\n" + + " - `category` (String): 카테고리(SERVICE)\n" + + " - `note` (String): 제휴 설명 (형식에 맞지 않는 제휴일때)\n" + + " - `discountRate` (Long): 할인률(DISCOUNT)\n" + + " - `hasPartner` (boolean): 제휴업체인지 여부\n" + + " - `latitude` (Double): 가게 위치 위도\n" + + " - `longitude` (Double): 가게 위치 경도\n" + + " - `profileUrl` (String): 가게 카카오맵 URL\n" + + " - `phoneNumber` (String): 가게 전화번호\n" + + " - `Admin`: 제휴업체 조회\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `name` (String): 제휴업체 이름\n" + + " - `address` (String): 제휴업체 주소\n" + + " - `isPartnered` (boolean): 관리자와 제휴 여부\n" + + " - `partnershipId` (Long): 제휴 ID\n" + + " - `partnershipStartDate` (LocalDate): 제휴 시작일\n" + + " - `partnershipEndDate` (LocalDate): 제휴 마감일\n" + + " - `latitude` (Double): 제휴업체 위도\n" + + " - `longitude` (Double): 제휴업체 경도\n" + + " - `profileUrl` (String): 제휴업체 카카오맵 Url\n" + + " - `phoneNumber` (String): 제휴업체 전화번호\n" + + " - `Partner`: 관리자 조회\n" + + " - `adminId` (Long): 관리자 ID\n" + + " - `name` (String): 관리자 이름\n" + + " - `address` (String): 관리자 주소\n" + + " - `isPartnered` (boolean): 제휴업체와 제휴 여부\n" + + " - `partnershipId` (Long): 제휴 ID\n" + + " - `partnershipStartDate` (LocalDate): 제휴 시작일\n" + + " - `partnershipEndDate` (LocalDate): 제휴 마감일\n" + + " - `latitude` (Double): 관리자 위도\n" + + " - `longitude` (Double): 관리자 경도\n" + + " - `profileUrl` (String): 관리자 카카오맵 Url\n" + + " - `phoneNumber` (String): 관리자 전화번호\n") @GetMapping("/search") public BaseResponse getLocationsByKeyword( @RequestParam("searchKeyword") @NotNull String keyword, @@ -72,15 +181,15 @@ public BaseResponse getLocationsByKeyword( return switch (role) { case STUDENT -> { - List list = mapService.searchStores(keyword); + List list = mapService.searchStores(keyword); yield BaseResponse.onSuccess(SuccessStatus._OK, list); } case ADMIN -> { - List list = mapService.searchPartner(keyword, memberId); + List list = mapService.searchPartner(keyword, memberId); yield BaseResponse.onSuccess(SuccessStatus._OK, list); } case PARTNER -> { - List list = mapService.searchAdmin(keyword, memberId); + List list = mapService.searchAdmin(keyword, memberId); yield BaseResponse.onSuccess(SuccessStatus._OK, list); } default -> BaseResponse.onFailure(ErrorStatus._BAD_REQUEST, null); @@ -89,14 +198,31 @@ public BaseResponse getLocationsByKeyword( @Operation( summary = "주소 입력 시 장소 검색용 API", - description = "검색어를 기반으로 장소를 검색하여 리스트로 반환합니다. limit로 개수를 제한할 수 있습니다." - ) + description = "# [v1.3 (2025-01-04)](https://clumsy-seeder-416.notion.site/25d1197c19ed807eb7a7c5ede2e75040)\n" + + "- 검색어에 따라 Map 객체를 반환합니다.\n" + + "- 검색어(searchKeyword) 입력.\n" + + "- limit을 통해 Map 객체 수 제한.\n" + + "- 성공 시 200(OK)과 Map 객체 반환.\n"+ + "\n**Request Parts:**\n" + + " - `searchKeyword` (String, required): 검색어\n" + + " - `limit` (Integer): 결과 객체 수\n" + + "\n**Response:**\n" + + " - `placeId` (String): kakao place ID\n" + + " - `name` (String): kakao place 이름\n" + + " - `category` (String): kakao 카테고리 또는 그룹 이름\n" + + " - `address` (String): 지번 주소\n" + + " - `roadAddress` (String): 도로명 주소\n" + + " - `phone` (String): 장소 전화번호\n" + + " - `placeUrl` (String): kakao place 상세 URL\n" + + " - `latitude` (Double): 장소 위도\n" + + " - `longitude` (Double): 장소 경도\n" + + " - `distance` (Integer): 장소의 m 좌표 (좌표바이어스/카테고리 검색 시 제공)\n") @GetMapping("/place") - public BaseResponse> search( + public BaseResponse> search( @RequestParam("searchKeyword") String query, @RequestParam(value = "limit", required = false) Integer size ) { - List list = placeSearchService.unifiedSearch(query, size); + List list = placeSearchService.unifiedSearch(query, size); return BaseResponse.onSuccess(SuccessStatus._OK, list); } 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 1d1a9af6..00000000 --- a/src/main/java/com/assu/server/domain/map/converter/MapConverter.java +++ /dev/null @@ -1,74 +0,0 @@ -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/AdminMapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/AdminMapResponseDTO.java new file mode 100644 index 00000000..30ba6b8d --- /dev/null +++ b/src/main/java/com/assu/server/domain/map/dto/AdminMapResponseDTO.java @@ -0,0 +1,41 @@ +package com.assu.server.domain.map.dto; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.infra.s3.AmazonS3Manager; + +import java.time.LocalDate; + +public record AdminMapResponseDTO( + Long adminId, + String name, + String address, + boolean isPartnered, + Long partnershipId, + LocalDate partnershipStartDate, + LocalDate partnershipEndDate, + Double latitude, + Double longitude, + String profileUrl, + String phoneNumber +) { + public static AdminMapResponseDTO of(Admin admin, Paper activePaper, AmazonS3Manager s3Manager) { + final String key = admin.getMember() != null ? admin.getMember().getProfileUrl() : null; + final String profileUrl = (key != null && !key.isBlank()) ? s3Manager.generatePresignedUrl(key) : null; + + assert admin.getMember() != null; + return new AdminMapResponseDTO( + admin.getId(), + admin.getName(), + admin.getOfficeAddress() != null ? admin.getOfficeAddress() : admin.getDetailAddress(), + activePaper != null, + activePaper != null ? activePaper.getId() : null, + activePaper != null ? activePaper.getPartnershipPeriodStart() : null, + activePaper != null ? activePaper.getPartnershipPeriodEnd() : null, + admin.getLatitude(), + admin.getLongitude(), + profileUrl, + admin.getMember().getPhoneNum() + ); + } +} 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 6b97d05c..7d71ac24 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 @@ -1,30 +1,38 @@ package com.assu.server.domain.map.dto; -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; - } - - @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 - } -} +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record MapRequestDTO( + @Schema(description = "화면 좌상단 경도") + @NotNull(message = "경도를 입력해주세요.") + double lng1, + + @Schema(description = "화면 좌상단 위도") + @NotNull(message = "위도를 입력해주세요.") + double lat1, + + @Schema(description = "화면 우상단 경도") + @NotNull(message = "경도를 입력해주세요.") + double lng2, + + @Schema(description = "화면 우상단 위도") + @NotNull(message = "위도를 입력해주세요.") + double lat2, + + @Schema(description = "화면 우하단 경도") + @NotNull(message = "경도를 입력해주세요.") + double lng3, + + @Schema(description = "화면 우하단 위도") + @NotNull(message = "위도를 입력해주세요.") + double lat3, + + @Schema(description = "화면 좌하단 경도") + @NotNull(message = "경도를 입력해주세요.") + double lng4, + + @Schema(description = "화면 좌하단 위도") + @NotNull(message = "위도를 입력해주세요.") + double lat4 +) {} \ No newline at end of file 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 deleted file mode 100644 index b3027a94..00000000 --- a/src/main/java/com/assu/server/domain/map/dto/MapResponseDTO.java +++ /dev/null @@ -1,115 +0,0 @@ -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.*; - -import java.time.LocalDate; -import java.util.List; - -public class MapResponseDTO { - - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class PartnerMapResponseDTO { - 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; - private String profileUrl; - private String phoneNumber; - } - - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class AdminMapResponseDTO { - 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; - private String profileUrl; - private String phoneNumber; - } - - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class StoreMapResponseDTO { - private Long storeId; - private Long adminId; - private String adminName; - 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 String note; - private Long discountRate; - private boolean hasPartner; - private Double latitude; - private Double longitude; - private String profileUrl; - private String phoneNumber; - } - - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class StoreMapResponseV2DTO { - private Long storeId; - private Long adminId; - private String adminName; - 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 String note; - private Long discountRate; - private boolean hasPartner; - private Double latitude; - private Double longitude; - private String profileUrl; - private String phoneNumber; - private String partner1; - private String partner2; - private String benefit1; - private String benefit2; - } - - @Getter @NoArgsConstructor @AllArgsConstructor @Builder - public static class PlaceSuggestionDTO { - private String placeId; // kakao place id - private String name; // place_name - 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 Double longitude; // x - private Integer distance; // m (좌표바이어스/카테고리 검색 시 제공) - } -} diff --git a/src/main/java/com/assu/server/domain/map/dto/PartnerMapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/PartnerMapResponseDTO.java new file mode 100644 index 00000000..f53315b6 --- /dev/null +++ b/src/main/java/com/assu/server/domain/map/dto/PartnerMapResponseDTO.java @@ -0,0 +1,41 @@ +package com.assu.server.domain.map.dto; + +import com.assu.server.domain.partner.entity.Partner; +import com.assu.server.domain.partnership.entity.Paper; +import com.assu.server.infra.s3.AmazonS3Manager; + +import java.time.LocalDate; + +public record PartnerMapResponseDTO( + Long partnerId, + String name, + String address, + boolean isPartnered, + Long partnershipId, + LocalDate partnershipStartDate, + LocalDate partnershipEndDate, + Double latitude, + Double longitude, + String profileUrl, + String phoneNumber +) { + public static PartnerMapResponseDTO of(Partner partner, Paper activePaper, AmazonS3Manager s3Manager) { + final String key = partner.getMember() != null ? partner.getMember().getProfileUrl() : null; + final String profileUrl = (key != null && !key.isBlank()) ? s3Manager.generatePresignedUrl(key) : null; + + assert partner.getMember() != null; + return new PartnerMapResponseDTO( + partner.getId(), + partner.getName(), + partner.getAddress() != null ? partner.getAddress() : partner.getDetailAddress(), + activePaper != null, + activePaper != null ? activePaper.getId() : null, + activePaper != null ? activePaper.getPartnershipPeriodStart() : null, + activePaper != null ? activePaper.getPartnershipPeriodEnd() : null, + partner.getLatitude(), + partner.getLongitude(), + profileUrl, + partner.getMember().getPhoneNum() + ); + } +} diff --git a/src/main/java/com/assu/server/domain/map/dto/PlaceSuggestionDTO.java b/src/main/java/com/assu/server/domain/map/dto/PlaceSuggestionDTO.java new file mode 100644 index 00000000..5708d907 --- /dev/null +++ b/src/main/java/com/assu/server/domain/map/dto/PlaceSuggestionDTO.java @@ -0,0 +1,14 @@ +package com.assu.server.domain.map.dto; + +public record PlaceSuggestionDTO( + String placeId, + String name, + String category, + String address, + String roadAddress, + String phone, + String placeUrl, + Double latitude, + Double longitude, + Integer distance +) {} 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 index fc2119ca..9837f544 100644 --- a/src/main/java/com/assu/server/domain/map/dto/SelectedPlacePayload.java +++ b/src/main/java/com/assu/server/domain/map/dto/SelectedPlacePayload.java @@ -8,7 +8,6 @@ @AllArgsConstructor @Builder public class SelectedPlacePayload { - private String placeId; private String name; private String address; diff --git a/src/main/java/com/assu/server/domain/map/dto/StoreMapResponseDTO.java b/src/main/java/com/assu/server/domain/map/dto/StoreMapResponseDTO.java new file mode 100644 index 00000000..2a4dafad --- /dev/null +++ b/src/main/java/com/assu/server/domain/map/dto/StoreMapResponseDTO.java @@ -0,0 +1,55 @@ +package com.assu.server.domain.map.dto; + +import com.assu.server.domain.store.entity.Store; +import com.assu.server.infra.s3.AmazonS3Manager; + +public record StoreMapResponseDTO( + Long storeId, + String name, + String address, + Integer rate, + boolean hasPartner, + Double latitude, + Double longitude, + String profileUrl, + String phoneNumber, + Long adminId1, + Long adminId2, + String adminName1, + String adminName2, + String benefit1, + String benefit2 +) { + public static StoreMapResponseDTO of( + Store store, + Long adminId1, Long adminId2, + String adminName1, String adminName2, + String benefit1, String benefit2, + AmazonS3Manager s3Manager + ) { + final boolean hasPartner = store.getPartner() != null; + final String key = (store.getPartner() != null && store.getPartner().getMember() != null) + ? store.getPartner().getMember().getProfileUrl() : null; + final String profileUrl = (key != null && !key.isBlank()) + ? s3Manager.generatePresignedUrl(key) : null; + final String phoneNumber = (store.getPartner() != null + && store.getPartner().getMember() != null + && store.getPartner().getMember().getPhoneNum() != null) + ? store.getPartner().getMember().getPhoneNum() : ""; + + return new StoreMapResponseDTO( + store.getId(), + store.getName(), + store.getAddress() != null ? store.getAddress() : store.getDetailAddress(), + store.getRate(), + hasPartner, + store.getLatitude(), + store.getLongitude(), + profileUrl, + phoneNumber, + adminId1, adminId2, + adminName1, adminName2, + benefit1, benefit2 + ); + } +} 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 0f1525fc..82047677 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 @@ -1,17 +1,18 @@ package com.assu.server.domain.map.service; +import com.assu.server.domain.map.dto.AdminMapResponseDTO; import com.assu.server.domain.map.dto.MapRequestDTO; -import com.assu.server.domain.map.dto.MapResponseDTO; +import com.assu.server.domain.map.dto.PartnerMapResponseDTO; +import com.assu.server.domain.map.dto.StoreMapResponseDTO; import java.util.List; public interface MapService { - List getAdmins(MapRequestDTO.ViewOnMapDTO viewport, Long memberId); - List getPartners(MapRequestDTO.ViewOnMapDTO viewport, Long memberId); - List getStores(MapRequestDTO.ViewOnMapDTO viewport, Long memberId); - List getStoresV2(MapRequestDTO.ViewOnMapDTO viewport, Long memberId); + List getAdmins(MapRequestDTO viewport, Long memberId); + List getPartners(MapRequestDTO viewport, Long memberId); + List getStores(MapRequestDTO viewport, Long memberId); - List searchStores(String keyword); - List searchPartner(String keyword, Long memberId); - List searchAdmin(String keyword, Long memberId); + List searchStores(String keyword); + List searchPartner(String keyword, Long memberId); + List searchAdmin(String keyword, Long memberId); } 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 73143a45..c4f75d72 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,45 +3,40 @@ 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.dto.AdminMapResponseDTO; 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.map.dto.PartnerMapResponseDTO; +import com.assu.server.domain.map.dto.StoreMapResponseDTO; 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.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; 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.UserPaper; import com.assu.server.domain.user.repository.UserPaperRepository; -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 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 org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; -import static reactor.core.publisher.Mono.when; - @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class MapServiceImpl implements MapService { private final AdminRepository adminRepository; @@ -49,255 +44,153 @@ public class MapServiceImpl implements MapService { private final StoreRepository storeRepository; private final PaperContentRepository paperContentRepository; private final PaperRepository paperRepository; - private final GeometryFactory geometryFactory; - private final GoodsRepository goodsRepository; private final AmazonS3Manager amazonS3Manager; private final UserPaperRepository userPaperRepository; @Override - public List getPartners(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) { - + public List getPartners(MapRequestDTO viewport, Long memberId) { String wkt = toWKT(viewport); - List partners = partnerRepository.findAllWithinViewport(wkt); - - return partners.stream().map(p -> { - Paper active = paperRepository.findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(memberId, p.getId(), ActivationStatus.ACTIVE) - .orElse(null); - - final String key = (p.getMember() != null) ? p.getMember().getProfileUrl() : null; - final String profileUrl = (key != null && !key.isBlank()) ? amazonS3Manager.generatePresignedUrl(key) : null; - - return MapResponseDTO.PartnerMapResponseDTO.builder() - .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()) - .profileUrl(profileUrl) - .phoneNumber(p.getMember().getPhoneNum()) - .build(); - }).toList(); + List partners = partnerRepository.findAllWithinViewportWithMember(wkt); + + if (partners.isEmpty()) { + return List.of(); + } + + List partnerIds = partners.stream().map(Partner::getId).toList(); + List papers = paperRepository.findByAdminIdAndPartnerIdInAndIsActivated(memberId, partnerIds, ActivationStatus.ACTIVE); + Map partnerIdToPaper = papers.stream() + .collect(Collectors.toMap(p -> p.getPartner().getId(), p -> p, (p1, p2) -> p1.getId() > p2.getId() ? p1 : p2)); + + return partners.stream() + .map(p -> PartnerMapResponseDTO.of(p, partnerIdToPaper.get(p.getId()), amazonS3Manager)) + .toList(); } @Override - public List getAdmins(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) { + public List getAdmins(MapRequestDTO viewport, Long memberId) { String wkt = toWKT(viewport); - List admins = adminRepository.findAllWithinViewport(wkt); - - return admins.stream().map(a -> { - Paper active = paperRepository.findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(a.getId(), memberId, ActivationStatus.ACTIVE) - .orElse(null); - - final String key = (a.getMember() != null) ? a.getMember().getProfileUrl() : null; - final String profileUrl = (key != null && !key.isBlank()) ? amazonS3Manager.generatePresignedUrl(key) : null; - - return MapResponseDTO.AdminMapResponseDTO.builder() - .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()) - .profileUrl(profileUrl) - .phoneNumber(a.getMember().getPhoneNum()) - .build(); - }).toList(); + List admins = adminRepository.findAllWithinViewportWithMember(wkt); + + if (admins.isEmpty()) { + return List.of(); + } + + List adminIds = admins.stream().map(Admin::getId).toList(); + List papers = paperRepository.findByAdminIdInAndPartnerIdAndIsActivated(adminIds, memberId, ActivationStatus.ACTIVE); + Map adminIdToPaper = papers.stream() + .collect(Collectors.toMap(p -> p.getAdmin().getId(), p -> p, (p1, p2) -> p1.getId() > p2.getId() ? p1 : p2)); + + return admins.stream() + .map(a -> AdminMapResponseDTO.of(a, adminIdToPaper.get(a.getId()), amazonS3Manager)) + .toList(); } + /** + * 뷰포트 내 매장 조회 (학생 혜택 기반). + * + * 조회 기준: + * 1. memberId(student_id)와 매치되는 모든 user_paper 조회 + * 2. 각 user_paper의 paper_id를 가진 paper 중 is_activated = ACTIVE인 것 + * 3. 그 중 뷰포트 내 store를 포함한 paper + * + * 위 기준을 만족하는 paper 중, 매장당 중복 없이 가장 최근 2건(paper+papercontent 쌍)을 가져와 + * adminId1/2, adminName1/2, benefit1/2를 채운 StoreMapResponseDTO 반환. + * papercontent의 note가 있으면 benefit 대신 note를 사용. + */ @Override - public List getStores(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) { + public List getStores(MapRequestDTO viewport, Long memberId) { final String wkt = toWKT(viewport); - // 1) 뷰포트 내 매장 조회 - final List stores = storeRepository.findAllWithinViewport(wkt); + // 1) 뷰포트 내 매장 조회 (Partner, Member fetch join) + final List stores = storeRepository.findAllWithinViewportWithPartner(wkt); + if (stores.isEmpty()) { + return List.of(); + } - // 2) 매장별 content는 "있으면 사용, 없으면 null" 전략 - return stores.stream().map(s -> { - final boolean hasPartner = (s.getPartner() != null); - - // 2-1) 유효한 paper_content만 조회 (없으면 null 허용) - final PaperContent content = paperContentRepository.findLatestValidByStoreIdNative( - s.getId(), - ActivationStatus.ACTIVE.name(), - OptionType.SERVICE.name(), - OptionType.DISCOUNT.name(), - CriterionType.PRICE.name(), - CriterionType.HEADCOUNT.name() - ).orElse(null); - - // 2-2) admin 정보 (null-safe) - final Long adminId = paperRepository.findTopPaperByStoreId(s.getId()) - .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null) - .orElse(null); - - String adminName = null; - if (adminId != null) { - final Admin admin = adminRepository.findById(adminId).orElse(null); - adminName = (admin != null ? admin.getName() : null); - } + // 2) 해당 학생의 활성 UserPaper 조회 (paper, store, admin fetch join 포함) + final List userPapers = userPaperRepository.findActivePartnershipsByStudentId(memberId, LocalDate.now()); + if (userPapers.isEmpty()) { + return stores.stream() + .map(s -> StoreMapResponseDTO.of(s, null, null, null, null, null, null, amazonS3Manager)) + .toList(); + } - // 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 && !key.isBlank()) ? 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()) - .adminId(adminId) - .adminName(adminName) - .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) - .cost(content != null ? content.getCost() : null) - .category(content != null ? content.getCategory() : null) - .discountRate(content != null ? content.getDiscount() : null) - .hasPartner(hasPartner) - .latitude(s.getLatitude()) - .longitude(s.getLongitude()) - .profileUrl(profileUrl) - .phoneNumber(phoneNumber) - .build(); - }).toList(); - } + // 3) 뷰포트 매장 ID Set (O(1) 조회용) + final Set storeIdSet = new HashSet<>(); + for (Store s : stores) storeIdSet.add(s.getId()); - @Override - public List getStoresV2(MapRequestDTO.ViewOnMapDTO viewport, Long memberId) { - final String wkt = toWKT(viewport); - final List stores = storeRepository.findAllWithinViewport(wkt); - final List userPapers = userPaperRepository.findActivePartnershipsByStudentId(memberId, java.time.LocalDate.now()); + // 4) UserPaper를 매장별로 그룹화: 뷰포트 내 매장 필터 + paper 중복 제거 + 매장당 최대 2건 + // findActivePartnershipsByStudentId 결과는 이미 paper.id DESC 정렬 상태 + final Map> papersByStore = new LinkedHashMap<>(); + final Set seenPaperIds = new HashSet<>(); + for (UserPaper up : userPapers) { + final Long storeId = up.getPaper().getStore().getId(); + if (!storeIdSet.contains(storeId)) continue; - // 모든 매장 ID 수집 - List storeIds = stores.stream().map(Store::getId).toList(); + final Long paperId = up.getPaper().getId(); + if (!seenPaperIds.add(paperId)) continue; // 이미 처리한 paper면 스킵 - // 빈 화면(매장 없음) 방어 코드 - if (storeIds.isEmpty()) { - return java.util.Collections.emptyList(); + final List list = papersByStore.computeIfAbsent(storeId, k -> new ArrayList<>(2)); + if (list.size() < 2) { + list.add(up.getPaper()); + } } - // 사용자가 가진 Paper ID 수집 (쿼리에 파라미터로 넘기기 위해 위치를 위로 올림!) - List userPaperIds = userPapers.stream() - .map(up -> up.getPaper().getId()) + // 5) 선택된 paper ID 목록으로 각 paper의 최신 PaperContent 1건씩 일괄 조회 + final List selectedPaperIds = papersByStore.values().stream() + .flatMap(List::stream) + .map(Paper::getId) .toList(); - // 일괄 조회 1: 모든 매장의 PaperContent (내 혜택 기준 매장당 최대 2개) - List allContents; - if (userPaperIds.isEmpty()) { - // 방어 코드: 사용자가 가진 혜택이 없다면 IN () 에러 방지를 위해 쿼리 실행 없이 빈 리스트 할당 - allContents = java.util.Collections.emptyList(); + final Map contentByPaperId; + if (selectedPaperIds.isEmpty()) { + contentByPaperId = Collections.emptyMap(); } else { - allContents = paperContentRepository.findLatestValidByStoreIdInNativeMax2( - storeIds, - ActivationStatus.ACTIVE.name(), - OptionType.SERVICE.name(), - OptionType.DISCOUNT.name(), - CriterionType.PRICE.name(), - CriterionType.HEADCOUNT.name(), - userPaperIds // ⭐️ 새로 추가된 파라미터 전달! - ); + contentByPaperId = paperContentRepository.findLatestByPaperIds(selectedPaperIds).stream() + .collect(Collectors.toMap( + pc -> pc.getPaper().getId(), + pc -> pc, + (a, b) -> a + )); } - // 일괄 조회 2: 모든 매장의 Paper와 Admin 정보 (Fetch Join) - List allPapers = paperRepository.findLatestPapersByStoreIds(storeIds); - - // 매장별 Paper 매핑 (매장당 최신 1개) - java.util.Map paperByStore = allPapers.stream() - .collect(java.util.stream.Collectors.toMap( - p -> p.getStore().getId(), - p -> p, - (existing, replacement) -> existing // 이미 키가 있으면 기존 값(최신) 유지 - )); - - // 매장별로 PaperContent 그룹화 - java.util.Map> contentsByStore = allContents.stream() - .collect(java.util.stream.Collectors.groupingBy( - pc -> pc.getPaper().getStore().getId() - )); - + // 6) 매장별 DTO 생성 return stores.stream().map(s -> { - final boolean hasPartner = (s.getPartner() != null); - - // ⭐️ DB에서 이미 '내 혜택 중 상위 2개'만 가져왔으므로 filter와 limit이 필요 없음! 꺼내기만 하면 끝. - List benefits = contentsByStore.getOrDefault(s.getId(), java.util.Collections.emptyList()); - - // 혜택 텍스트 생성 - String partner1 = null; - String benefit1 = null; - String partner2 = null; - String benefit2 = null; - - if (!benefits.isEmpty()) { - PaperContent content1 = benefits.get(0); - partner1 = content1.getPaper() != null && content1.getPaper().getPartner() != null - ? content1.getPaper().getPartner().getName() : null; - benefit1 = generateBenefitText(content1); - } + final List sPapers = papersByStore.getOrDefault(s.getId(), Collections.emptyList()); - if (benefits.size() > 1) { - PaperContent content2 = benefits.get(1); - partner2 = content2.getPaper() != null && content2.getPaper().getPartner() != null - ? content2.getPaper().getPartner().getName() : null; - benefit2 = generateBenefitText(content2); - } + Long adminId1 = null; String adminName1 = null; String benefit1 = null; + Long adminId2 = null; String adminName2 = null; String benefit2 = null; - Paper paper = paperByStore.get(s.getId()); - Long adminId = null; - String adminName = null; - if (paper != null && paper.getAdmin() != null) { - adminId = paper.getAdmin().getId(); - String name = paper.getAdmin().getName(); - adminName = (name != null) ? name : ""; // Null이면 빈 문자열로 덮어쓰기! + if (!sPapers.isEmpty()) { + final Paper p1 = sPapers.get(0); + if (p1.getAdmin() != null) { + adminId1 = p1.getAdmin().getId(); + adminName1 = p1.getAdmin().getName(); + } + benefit1 = resolveBenefit(contentByPaperId.get(p1.getId())); + } + if (sPapers.size() > 1) { + final Paper p2 = sPapers.get(1); + if (p2.getAdmin() != null) { + adminId2 = p2.getAdmin().getId(); + adminName2 = p2.getAdmin().getName(); + } + benefit2 = resolveBenefit(contentByPaperId.get(p2.getId())); } - // S3 presigned URL - final String key = (s.getPartner() != null && s.getPartner().getMember() != null) - ? s.getPartner().getMember().getProfileUrl() : null; - final String profileUrl = (key != null && !key.isBlank()) - ? amazonS3Manager.generatePresignedUrl(key) : null; - - // phoneNumber - final String phoneNumber = (s.getPartner() != null - && s.getPartner().getMember() != null - && s.getPartner().getMember().getPhoneNum() != null) - ? s.getPartner().getMember().getPhoneNum() - : ""; - - return MapResponseDTO.StoreMapResponseV2DTO.builder() - .storeId(s.getId()) - .adminId(adminId) - .adminName(adminName) - .name(s.getName()) - .address(s.getAddress() != null ? s.getAddress() : s.getDetailAddress()) - .rate(s.getRate()) - .hasPartner(hasPartner) - .latitude(s.getLatitude()) - .longitude(s.getLongitude()) - .profileUrl(profileUrl) - .phoneNumber(phoneNumber) - .partner1(partner1) - .benefit1(benefit1) - .partner2(partner2) - .benefit2(benefit2) - .build(); + return StoreMapResponseDTO.of(s, adminId1, adminId2, adminName1, adminName2, benefit1, benefit2, amazonS3Manager); }).toList(); } + /** note가 있으면 note를, 없으면 generateBenefitText 결과를 반환 */ + private String resolveBenefit(PaperContent content) { + if (content == null) return null; + if (content.getNote() != null && !content.getNote().isBlank()) { + return content.getNote(); + } + return generateBenefitText(content); + } + private String generateBenefitText(PaperContent content) { if (content == null) return null; @@ -331,147 +224,98 @@ private String generateBenefitText(PaperContent content) { } @Override - public List searchStores(String keyword) { - List stores = storeRepository.findByNameContainingIgnoreCaseOrderByIdDesc(keyword); - - return stores.stream().map(s -> { - boolean hasPartner = s.getPartner() != null; - PaperContent content = paperContentRepository.findTopByPaperStoreIdOrderByIdDesc(s.getId()) - .orElse(null); - - final String key = (s.getPartner() != null && s.getPartner().getMember() != null) ? s.getPartner().getMember().getProfileUrl() : null; - final String profileUrl = (key != null && !key.isBlank()) ? amazonS3Manager.generatePresignedUrl(key) : null; + public List searchStores(String keyword) { + List stores = storeRepository.findByNameContainingIgnoreCaseOrderByIdDescWithPartner(keyword); - Long adminId = paperRepository.findTopPaperByStoreId(s.getId()) - .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null) - .orElse(null); + if (stores.isEmpty()) { + return List.of(); + } - Admin admin = adminRepository.findById(adminId).orElse(null); + List storeIds = stores.stream().map(Store::getId).toList(); - String finalCategory = null; + // 매장당 최신 Paper 1건 (admin 정보용) + List papers = paperRepository.findByStoreIdIn(storeIds); + Map storeIdToPaper = papers.stream() + .collect(Collectors.toMap(p -> p.getStore().getId(), p -> p, (p1, p2) -> p1.getId() > p2.getId() ? p1 : p2)); - 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로 유지됩니다. - } - } + List adminIds = papers.stream() + .filter(p -> p.getAdmin() != null) + .map(p -> p.getAdmin().getId()) + .distinct() + .toList(); + List admins = adminIds.isEmpty() ? List.of() : adminRepository.findAllById(adminIds); + Map adminIdToAdmin = admins.stream() + .collect(Collectors.toMap(Admin::getId, a -> a)); + + // 매장당 최신 PaperContent 1건 (benefit 생성용) + List contents = paperContentRepository.findTopByStoreIdIn(storeIds); + Map storeIdToContent = contents.stream() + .collect(Collectors.toMap( + pc -> pc.getPaper().getStore().getId(), + pc -> pc, + (pc1, pc2) -> pc1.getId() > pc2.getId() ? pc1 : pc2 + )); - // 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 != 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) - .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) - .hasPartner(hasPartner) - .latitude(s.getLatitude()) - .longitude(s.getLongitude()) - .profileUrl(profileUrl) - .phoneNumber(phoneNumber) - .build(); + return stores.stream().map(s -> { + PaperContent content = storeIdToContent.get(s.getId()); + Paper paper = storeIdToPaper.get(s.getId()); + Long adminId = paper != null && paper.getAdmin() != null ? paper.getAdmin().getId() : null; + Admin admin = adminId != null ? adminIdToAdmin.get(adminId) : null; + + return StoreMapResponseDTO.of( + s, + adminId, null, + admin != null ? admin.getName() : null, null, + generateBenefitText(content), null, + amazonS3Manager + ); }).toList(); } @Override - public List searchPartner(String keyword, Long memberId) { - List partners = partnerRepository.searchPartnerByKeyword(keyword); - - return partners.stream().map(p -> { - Paper active = paperRepository - .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(memberId, p.getId(), ActivationStatus.ACTIVE) - .orElse(null); - - final String key = (p.getMember() != null) ? p.getMember().getProfileUrl() : null; - final String profileUrl = (key != null && !key.isBlank()) ? amazonS3Manager.generatePresignedUrl(key) : null; - - return MapResponseDTO.PartnerMapResponseDTO.builder() - .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()) - .profileUrl(profileUrl) - .phoneNumber(p.getMember().getPhoneNum()) - .build(); - }).toList(); + public List searchPartner(String keyword, Long memberId) { + List partners = partnerRepository.searchPartnerByKeywordWithMember(keyword); + + if (partners.isEmpty()) { + return List.of(); + } + + List partnerIds = partners.stream().map(Partner::getId).toList(); + List papers = paperRepository.findByAdminIdAndPartnerIdInAndIsActivated(memberId, partnerIds, ActivationStatus.ACTIVE); + Map partnerIdToPaper = papers.stream() + .collect(Collectors.toMap(p -> p.getPartner().getId(), p -> p, (p1, p2) -> p1.getId() > p2.getId() ? p1 : p2)); + + return partners.stream() + .map(p -> PartnerMapResponseDTO.of(p, partnerIdToPaper.get(p.getId()), amazonS3Manager)) + .toList(); } @Override - public List searchAdmin(String keyword, Long memberId) { - List admins = adminRepository.searchAdminByKeyword(keyword); - - return admins.stream().map(a -> { - Paper active = paperRepository - .findTopByAdmin_IdAndPartner_IdAndIsActivatedOrderByIdDesc(a.getId(), memberId, ActivationStatus.ACTIVE) - .orElse(null); - - final String key = (a.getMember() != null) ? a.getMember().getProfileUrl() : null; - final String profileUrl = (key != null && !key.isBlank()) ? amazonS3Manager.generatePresignedUrl(key) : null; - - return MapResponseDTO.AdminMapResponseDTO.builder() - .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()) - .profileUrl(profileUrl) - .phoneNumber(a.getMember().getPhoneNum()) - .build(); - }).toList(); + public List searchAdmin(String keyword, Long memberId) { + List admins = adminRepository.searchAdminByKeywordWithMember(keyword); + + if (admins.isEmpty()) { + return List.of(); + } + + List adminIds = admins.stream().map(Admin::getId).toList(); + List papers = paperRepository.findByAdminIdInAndPartnerIdAndIsActivated(adminIds, memberId, ActivationStatus.ACTIVE); + Map adminIdToPaper = papers.stream() + .collect(Collectors.toMap(p -> p.getAdmin().getId(), p -> p, (p1, p2) -> p1.getId() > p2.getId() ? p1 : p2)); + + return admins.stream() + .map(a -> AdminMapResponseDTO.of(a, adminIdToPaper.get(a.getId()), amazonS3Manager)) + .toList(); } - private String toWKT(MapRequestDTO.ViewOnMapDTO v) { + private String toWKT(MapRequestDTO 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() + v.lng1(), v.lat1(), + v.lng2(), v.lat2(), + v.lng3(), v.lat3(), + v.lng4(), v.lat4(), + v.lng1(), v.lat1() ); } - - private Point toPoint(Double lng, Double lat) { - if (lng == null || lat == null) return null; - Point p = geometryFactory.createPoint(new Coordinate(lng, lat)); - 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/map/service/PlaceSearchService.java b/src/main/java/com/assu/server/domain/map/service/PlaceSearchService.java index f16f4fd2..b1fff690 100644 --- a/src/main/java/com/assu/server/domain/map/service/PlaceSearchService.java +++ b/src/main/java/com/assu/server/domain/map/service/PlaceSearchService.java @@ -1,10 +1,10 @@ package com.assu.server.domain.map.service; -import com.assu.server.domain.map.dto.MapResponseDTO; +import com.assu.server.domain.map.dto.PlaceSuggestionDTO; import java.util.List; public interface PlaceSearchService { - List unifiedSearch(String query, Integer size); + 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 index 243d509a..1c384b16 100644 --- a/src/main/java/com/assu/server/domain/map/service/PlaceSearchServiceImpl.java +++ b/src/main/java/com/assu/server/domain/map/service/PlaceSearchServiceImpl.java @@ -1,22 +1,24 @@ package com.assu.server.domain.map.service; -import com.assu.server.domain.map.dto.MapResponseDTO; +import com.assu.server.domain.map.dto.PlaceSuggestionDTO; import com.assu.server.global.config.KakaoLocalClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.stream.Stream; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) 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) { + public List unifiedSearch(String query, Integer size) { int kSize = (size == null ? 15 : size); // 1) 주소로도 시도 → 좌표 얻기 (성공/실패 무관) @@ -39,50 +41,50 @@ public List unifiedSearch(String query, Integ // 2) 키워드 검색 (좌표가 있으면 바이어스) var kw = kakaoLocalClient.searchByKeyword(query, x, y, null, 1, kSize); - List kwList = convertKeyword(kw); + List kwList = convertKeyword(kw); // 3) 좌표가 있으면 카테고리 근접 검색 보강 (음식점/카페 등) - List nearby = Collections.emptyList(); + List nearby = Collections.emptyList(); if (x != null && y != null) { List cats = List.of("FD6", "CE7"); // 음식점/카페 (필요시 카테고리 추가) - List merged = new ArrayList<>(); + 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))); + merged.sort(Comparator.comparing(dto -> Optional.ofNullable(dto.distance()).orElse(Integer.MAX_VALUE))); nearby = merged; } // 4) 결과 합치기 (키워드 우선 → 근접 결과 추가, id로 dedupe) - Map dedupe = new LinkedHashMap<>(); + Map dedupe = new LinkedHashMap<>(); Stream.concat(kwList.stream(), nearby.stream()) - .forEach(dto -> dedupe.putIfAbsent(dto.getPlaceId(), dto)); + .forEach(dto -> dedupe.putIfAbsent(dto.placeId(), dto)); // 최종 상위 size 개 제한 return dedupe.values().stream().limit(kSize).toList(); } - private List convertKeyword(KakaoLocalClient.KakaoKeywordResp resp) { + private List convertKeyword(KakaoLocalClient.KakaoKeywordResp resp) { if (resp == null || resp.getDocuments() == null) return List.of(); - List out = new ArrayList<>(); + List out = new ArrayList<>(); for (var d : resp.getDocuments()) { - Double x = safeParse(d.getX()); - Double y = safeParse(d.getY()); + Double lng = safeParse(d.getX()); + Double lat = 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()); + out.add(new PlaceSuggestionDTO( + d.getId(), + d.getPlace_name(), + d.getCategory_group_name() != null ? d.getCategory_group_name() : d.getCategory_name(), + d.getAddress_name(), + d.getRoad_address_name(), + d.getPhone(), + d.getPlace_url(), + lat, + lng, + dist + )); } return out; } 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 cfd7e55f..e2ef74bf 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,10 +6,12 @@ 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.*; +@Tag(name = "Partner", description = "제휴업체 관련 API") @RestController @RequestMapping("/partner") @RequiredArgsConstructor @@ -18,11 +20,19 @@ public class PartnerController { private final PartnerService partnerService; @Operation( - summary = "어드민 추천 API", - description = "제휴하지 않은 어드민 중 두 곳을 랜덤으로 조회합니다." - ) + summary = "관리자 추천 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2591197c19ed80368a9edf1f6e92ea38)\n" + + "- 현재 로그인 한 제휴업체와 제휴하지 않은 관리자 중 최대 두 곳을 랜덤으로 조회합니다.\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `RandomAdminResponse` 객체(최대 두 개) 반환\n" + + " - `adminId` (Long): 관리자 ID\n" + + " - `adminAddress` (String): 관리자 주소\n" + + " - `adminDetailAddress` (String): 관리자 상세주소\n" + + " - `adminName` (String): 관리자 상호명\n" + + " - `adminUrl` (String): 관리자 카카오맵 URL\n" + + " - `adminPhone` (String): 관리자 전화번호\n") @GetMapping("/admin-recommend") - public BaseResponse randomAdminRecommend( + public BaseResponse randomAdminRecommend( @AuthenticationPrincipal PrincipalDetails pd ){ return BaseResponse.onSuccess(SuccessStatus._OK, partnerService.getRandomAdmin(pd.getId())); diff --git a/src/main/java/com/assu/server/domain/partner/converter/PartnerConverter.java b/src/main/java/com/assu/server/domain/partner/converter/PartnerConverter.java deleted file mode 100644 index ec567ad2..00000000 --- a/src/main/java/com/assu/server/domain/partner/converter/PartnerConverter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partner.converter; - -public class PartnerConverter { -} diff --git a/src/main/java/com/assu/server/domain/partner/dto/PartnerRequestDTO.java b/src/main/java/com/assu/server/domain/partner/dto/PartnerRequestDTO.java deleted file mode 100644 index a593ba8c..00000000 --- a/src/main/java/com/assu/server/domain/partner/dto/PartnerRequestDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.assu.server.domain.partner.dto; - -public class PartnerRequestDTO { -} 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 cdc2f2a7..940f63e2 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,32 +1,30 @@ package com.assu.server.domain.partner.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import com.assu.server.domain.admin.entity.Admin; import java.util.List; -public class PartnerResponseDTO { +public record PartnerResponseDTO ( + List admins +){ - @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; - private String adminUrl; - private String adminPhone; + public record AdminLiteDTO( + Long adminId, + String adminAddress, + String adminDetailAddress, + String adminName, + String adminUrl, + String adminPhone + ) { + public static AdminLiteDTO from(Admin admin) { + return new AdminLiteDTO( + admin.getId(), + admin.getOfficeAddress(), + admin.getDetailAddress(), + admin.getName(), + admin.getMember() != null ? admin.getMember().getProfileUrl() : null, + admin.getMember() != null ? admin.getMember().getPhoneNum() : null + ); + } } } 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 c0aecd2b..afb753e9 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 @@ -2,11 +2,7 @@ import com.assu.server.domain.member.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 org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -30,10 +26,13 @@ public class Partner { @JoinColumn(name = "id") private Member member; + @Column(name = "name", length = 255, nullable = false) private String name; + @Column(name = "address", length = 255, nullable = false) private String address; + @Column(name = "detail_address", length = 255) private String detailAddress; private String licenseUrl; 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 df9aa420..adcd6562 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,12 +1,9 @@ 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 org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import java.util.List; @@ -38,20 +35,22 @@ SELECT COUNT(*) Partner findUnpartneredActiveByAdminWithOffset(@Param("adminId") Long adminId, @Param("offset") int offset); - @Query(value = """ - SELECT p.* - FROM partner p + @Query(""" + SELECT DISTINCT p + FROM Partner p + LEFT JOIN FETCH p.member WHERE p.point IS NOT NULL - AND ST_Contains(ST_GeomFromText(:wkt, 4326), p.point) - """, nativeQuery = true) - List findAllWithinViewport(@Param("wkt") String wkt); + AND function('ST_Contains', function('ST_GeomFromText', :wkt, 4326), p.point) = true + """) + List findAllWithinViewportWithMember(@Param("wkt") String wkt); @Query(""" - select distinct p - from Partner p - where lower(p.name) like lower(concat('%', :keyword, '%')) + SELECT DISTINCT p + FROM Partner p + LEFT JOIN FETCH p.member + WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%')) """) - List searchPartnerByKeyword( + List searchPartnerByKeywordWithMember( @Param("keyword") String keyword ); 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 7edd9165..1958e1c9 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,5 @@ public interface PartnerService { - PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(Long partnerId); - + PartnerResponseDTO 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 f865fc28..6340f945 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 @@ -10,6 +10,7 @@ import com.assu.server.global.exception.DatabaseException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.concurrent.ThreadLocalRandom; @@ -17,13 +18,14 @@ @Service @RequiredArgsConstructor +@Transactional public class PartnerServiceImpl implements PartnerService { private final PartnerRepository partnerRepository; private final AdminRepository adminRepository; @Override - public PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(Long partnerId) { + public PartnerResponseDTO getRandomAdmin(Long partnerId) { Partner partner = partnerRepository.findById(partnerId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); @@ -42,19 +44,10 @@ public PartnerResponseDTO.RandomAdminResponseDTO getRandomAdmin(Long partnerId) 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()) - .adminUrl(a.getMember().getProfileUrl()) - .adminPhone(a.getMember().getPhoneNum()) - .build()) + .map(PartnerResponseDTO.AdminLiteDTO::from) .collect(Collectors.toList()); - return PartnerResponseDTO.RandomAdminResponseDTO.builder() - .admins(admins) - .build(); + return new PartnerResponseDTO(admins); } } 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 0017829b..ee7b07b2 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 @@ -5,8 +5,6 @@ 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.member.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; @@ -21,7 +19,7 @@ import lombok.RequiredArgsConstructor; @RestController -@Tag(name = "제휴 관련 내용 '조회' api", description = "상세 설명") +@Tag(name = "Paper", description = "제휴 제안서 조회 api") @RequiredArgsConstructor public class PaperController { 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 bbcf0b04..bf21a86b 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 @@ -17,9 +17,19 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import com.assu.server.domain.partnership.dto.AdminPartnershipCheckResponseDTO; +import com.assu.server.domain.partnership.dto.ManualPartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.ManualPartnershipResponseDTO; +import com.assu.server.domain.partnership.dto.PartnerPartnershipCheckResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipDetailResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipDraftRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipDraftResponseDTO; import com.assu.server.domain.partnership.dto.PartnershipFinalRequestDTO; -import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; -import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipStatusUpdateRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipStatusUpdateResponseDTO; +import com.assu.server.domain.partnership.dto.SuspendedPaperResponseDTO; +import com.assu.server.domain.partnership.dto.WritePartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.WritePartnershipResponseDTO; import com.assu.server.domain.partnership.service.PartnershipService; import com.assu.server.global.apiPayload.BaseResponse; import com.assu.server.global.apiPayload.code.status.SuccessStatus; @@ -31,8 +41,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; + @RestController -@Tag(name = "제휴 요청 api", description = "최종적으로 제휴를 요청할때 사용하는 api ") +@Tag(name = "Partnership", description = "제휴 제안 api") @RequiredArgsConstructor @RequestMapping("/partnership") public class PartnershipController { @@ -59,146 +70,374 @@ public class PartnershipController { " - 실패: 적절한 에러 코드 및 메시지" ) public ResponseEntity> finalPartnershipRequest( - @AuthenticationPrincipal PrincipalDetails pd,@RequestBody PartnershipFinalRequestDTO dto + @AuthenticationPrincipal PrincipalDetails pd, @RequestBody PartnershipFinalRequestDTO dto ) { - partnershipService.recordPartnershipUsage(dto,pd.getMember()); + partnershipService.recordPartnershipUsage(dto, pd.getMember()); return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus.USER_PAPER_REQUEST_SUCCESS, null)); } - @PatchMapping("/proposal") @Operation( - summary = "제휴 제안서 내용 수정 API", - description = "제공 서비스 종류(SERVICE, DISCOUNT), 서비스 제공 기준(PRICE, HEADCOUNT), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성해주세요." - ) - public BaseResponse updatePartnership( - @RequestBody PartnershipRequestDTO.WritePartnershipRequestDTO request, + summary = "제휴 제안서 초안 생성 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2fe1197c19ed8043a511cc8ea005d5b4)\n" + + "- 관리자로 로그인한 상태에서 제안서 초안을 생성합니다.\n" + + "- 제안서를 작성할 제휴업체 ID 입력.\n" + + "\n**Request Body:**\n" + + " - `CreateDraftRequest` 객체 (JSON, required)\n" + + " - `partnerId` (Long): 제휴 제안서를 작성할 제휴업체 ID\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `CreateDraftResponse` 객체 반환.\n" + + " - `paperId` (Long): 생성된 제안서 ID\n") + @PostMapping("/proposal/draft") + public BaseResponse createDraftPartnership( + @RequestBody PartnershipDraftRequestDTO request, @AuthenticationPrincipal PrincipalDetails pd - ){ - return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.updatePartnership(request, pd.getId())); + ) { + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createDraftPartnership(request, pd.getId())); } @Operation( summary = "제휴 제안서 수동 등록 API", - description = "제공 서비스 종류(SERVICE, DISCOUNT), 서비스 제공 기준(PRICE, HEADCOUNT), 서비스 제공 항목, 카테고리, 할인율을 상황에 맞게 작성하고, 계약서 이미지를 업로드하세요." - ) + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2591197c19ed804785d9f58f95223048)\n" + + "- 관리자로 로그인한 상황에서 내용이 있는 제휴 제안서를 생성합니다.\n" + + "- 계약서 이미지 MultipartFile을 입력.\n" + + "- 주소 입력 시 장소 검색용 API에서 반환된 Map 객체의 내용을 selectedPlace에 입력.\n" + + "- options의 optionType을 SERVICE/DISCOUNT 중 하나로 설정.\n" + + "- options의 criterionType을 PRICE/HEADCOUNT 중 하나로 설정.\n" + + "- 이외의 제휴 유형일 경우 anotherType을 true로 설정.\n" + + "- DB에 해당하는 store가 없다면 생성.\n" + + "- 해당하는 store가 INACTIVE 상태였다면 ACTIVE 상태로 변환.\n" + + "\n**Request Body:**\n" + + " - `ManualPartnershipRequest` 객체 (JSON, required): 제안서 내용\n" + + " - `storeName` (String): 가게 이름\n" + + " - `selectedPlace` (JSON): 선택된 장소\n" + + " - `placeId` (String): kakao place ID\n" + + " - `name` (String): kakao place 이름\n" + + " - `address` (String): 지번 주소\n" + + " - `roadAddress` (String): 도로명 주소\n" + + " - `latitude` (Double): 장소 위도\n" + + " - `longitude` (Double): 장소 경도\n" + + " - `storeDetailAddress` (String): 가게 상세주소\n" + + " - `partnershipPeriodStart` (LocalDate): 제휴 시작일\n" + + " - `partnershipPeriodEnd` (LocalDate): 제휴 마감일\n" + + " - `options` (JSON): 제휴 옵션\n" + + " - `optionType` (OptionType): 제공 서비스 종류 (서비스 제공, 할인)\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준 (금액, 인원)\n" + + " - `anotherType` (Boolean): 기타 제공 서비스\n" + + " - `people` (Integer): 서비스 제공 기준 인원 수\n" + + " - `cost` (Integer): 서비스 제공 기준 금액\n" + + " - `category` (String): 서비스 카테고리, 서비스 제공 항목이 여러 개 일 때 작성\n" + + " - `discountRate` (Long): 서비스 제공 인원 수\n" + + " - `note` (String): 기타 유형 제휴 옵션 문구\n" + + " - `goods` (JSON): 서비스 제공 항목\n" + + " - `goodsName` (String): 서비스 제공 항목명\n" + + " - `contractImage` (MultipartFile, required): 계약서 이미지 파일\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `ManualPartnershipResponse` 객체 반환.\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `storeCreated` (boolean): 가게가 DB에 생성되었는지 여부\n" + + " - `storeActivated` (boolean): 가게가 재활성화되었는지 여부\n" + + " - `status` (String): 제휴 제안서의 상태\n" + + " - `contractImageUrl` (String): 계약서 파일 URL\n" + + " - `partnership` (JSON): 제휴 제안서\n" + + " - `partnershipId` (Long): 제안서 ID\n" + + " - `partnershipPeriodStart` (LocalDate): 제휴 시작일\n" + + " - `partnershipPeriodEnd` (LocalDate): 제휴 마감일\n" + + " - `adminId` (Long): 관리자 ID\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `storeName` (String): 가게 이름\n" + + " - `adminName` (String): 관리자 이름\n" + + " - `isActivated` (ActivationStatus): 제안서 활성화 여부\n" + + " - `options` (JSON): 제휴 옵션\n" + + " - `optionType` (OptionType): 제공 서비스 종류 (서비스 제공, 할인)\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준 (금액, 인원)\n" + + " - `anotherType` (Boolean): 기타 제공 서비스\n" + + " - `people` (Integer): 서비스 제공 기준 인원 수\n" + + " - `cost` (Integer): 서비스 제공 기준 금액\n" + + " - `note` (String): 기타 유형 제휴 옵션 문구\n" + + " - `category` (String): 서비스 카테고리, 서비스 제공 항목이 여러 개 일 때 작성\n" + + " - `discountRate` (Long): 서비스 제공 인원 수\n" + + " - `goods` (JSON): 서비스 제공 항목\n" + + " - `goodsId` (Long): 서비스 제공 항목 ID\n" + + " - `goodsName` (String): 서비스 제공 항목명\n") @PostMapping(value = "/passivity", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public BaseResponse createManualPartnership( - @RequestPart("request") @Parameter PartnershipRequestDTO.ManualPartnershipRequestDTO request, + public BaseResponse createManualPartnership( + @RequestPart("request") @Parameter ManualPartnershipRequestDTO request, + @RequestPart(value = "contractImage") @Parameter( description = "계약서 이미지 파일", required = true, content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, schema = @Schema(type = "string", format = "binary")) - ) - MultipartFile contractImage, + ) MultipartFile contractImage, @AuthenticationPrincipal PrincipalDetails pd ) { return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createManualPartnership(request, pd.getId(), contractImage)); } @Operation( - summary = "제휴 중인 가게 조회 API", - description = "전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정해주세요." - ) - @GetMapping("/admin") - public BaseResponse> listForAdmin( - @RequestParam(name = "all", defaultValue = "false") boolean all, + summary = "제휴 제안서 내용 수정 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2371197c19ed80aa8468d2377ef8eac2)\n" + + "- 제안서 초안 또는 이미 작성된 제안서의 내용을 수정합니다.\n" + + "- options의 optionType을 SERVICE/DISCOUNT 중 하나로 설정\n" + + "- options의 criterionType을 PRICE/HEADCOUNT 중 하나로 설정\n" + + "- 이외의 제휴 유형일 경우 anotherType을 true로 설정\n" + + "\n**Request Body:**\n" + + " - `WritePartnershipRequest` 객체 (JSON, required): 수정 내용\n" + + " - `paperId` (String): 이메일 주소\n" + + " - `partnershipPeriodStart` (LocalDate): 제휴 시작일\n" + + " - `partnershipPeriodEnd` (LocalDate): 제휴 마감일\n" + + " - `options` (JSON): 제휴 옵션\n" + + " - `optionType` (OptionType): 제공 서비스 종류 (서비스 제공, 할인)\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준 (금액, 인원)\n" + + " - `anotherType` (Boolean): 기타 제공 서비스\n" + + " - `people` (Integer): 서비스 제공 기준 인원 수\n" + + " - `cost` (Integer): 서비스 제공 기준 금액\n" + + " - `category` (String): 서비스 카테고리, 서비스 제공 항목이 여러 개 일 때 작성\n" + + " - `discountRate` (Long): 서비스 제공 인원 수\n" + + " - `note` (String): 기타 유형 제휴 옵션 문구\n" + + " - `goods` (JSON): 서비스 제공 항목\n" + + " - `goodsName` (String): 서비스 제공 항목명\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `WritePartnershipResponse` 객체 반환\n" + + " - `partnershipId` (Long): 제안서 ID\n" + + " - `partnershipPeriodStart` (LocalDate): 제휴 시작일\n" + + " - `partnershipPeriodEnd` (LocalDate): 제휴 마감일\n" + + " - `adminId` (Long): 관리자 ID\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `storeName` (String): 가게 이름\n" + + " - `adminName` (String): 관리자 이름\n" + + " - `isActivated` (ActivationStatus): 제안서 활성화 여부\n" + + " - `options` (JSON): 제휴 옵션\n" + + " - `optionType` (OptionType): 제공 서비스 종류 (서비스 제공, 할인)\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준 (금액, 인원)\n" + + " - `anotherType` (Boolean): 기타 제공 서비스\n" + + " - `people` (Integer): 서비스 제공 기준 인원 수\n" + + " - `cost` (Integer): 서비스 제공 기준 금액\n" + + " - `note` (String): 기타 유형 제휴 옵션 문구\n" + + " - `category` (String): 서비스 카테고리, 서비스 제공 항목이 여러 개 일 때 작성\n" + + " - `discountRate` (Long): 서비스 제공 인원 수\n" + + " - `goods` (JSON): 서비스 제공 항목\n" + + " - `goodsId` (Long): 서비스 제공 항목 ID\n" + + " - `goodsName` (String): 서비스 제공 항목명\n") + @PatchMapping("/proposal") + public BaseResponse updatePartnership( + @RequestBody WritePartnershipRequestDTO request, @AuthenticationPrincipal PrincipalDetails pd - ) { - return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForAdmin(all, pd.getId())); + ){ + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.updatePartnership(request, pd.getId())); } @Operation( - summary = "제휴 중인 관리자 조회 API", - description = "전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정해주세요." - ) - @GetMapping("/partner") - public BaseResponse> listForPartner( - @RequestParam(name = "all", defaultValue = "false") boolean all, - @AuthenticationPrincipal PrincipalDetails pd + summary = "제휴 상태 업데이트 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/SUSPEND-ACTIVE-INACTIVE-2371197c19ed805ab509f552817e823a)\n" + + "- 제휴 상태를 변경합니다.\n" + + "- 적용할 상태 입력 (ACTIVE/SUSPEND/INACTIVE).\n" + + "\n**Parameters:**\n" + + " - `partnershipId` (Long, required): 상태를 적용할 제안서 ID\n" + + "\n**Request Body:**\n" + + " - `UpdateRequest` 객체 (JSON, required)\n" + + " - `status` (String): 제안서에 적용할 상태\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `UpdateResponse` 객체 반환.\n" + + " - `partnershipId` (Long): 생성된 제안서 ID\n"+ + " - `prevStatus` (String): 제안서의 이전 상태\n"+ + " - `newStatus` (String): 제안서의 이전 상태\n"+ + " - `changedAt` (LocalDateTime): 상태 변경 시간\n") + @PatchMapping("/{partnershipId}/status") + public BaseResponse updatePartnershipStatus( + @PathVariable("partnershipId") @Parameter(required = true) Long partnershipId, + @RequestBody PartnershipStatusUpdateRequestDTO request ) { - return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForPartner(all, pd.getId())); + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.updatePartnershipStatus(partnershipId, request)); } @Operation( summary = "제휴 상세조회 API", - description = "제휴 아이디를 입력하세요." - ) + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2371197c19ed80cdac8beb2ffddb2f61)\n" + + "- 제휴 제안서의 내용을 조회합니다.\n" + + "- 적용할 상태 입력 (ACTIVE/SUSPEND/INACTIVE).\n" + + "\n**Parameters:**\n" + + " - `partnershipId` (Long, required): 내용을 조회할 제안서 ID\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `GetPartnershipDetailResponse` 객체 반환.\n" + + " - `partnershipId` (Long): 제안서 ID\n"+ + " - `updatedAt` (LocalDateTime): 업데이트된 시간\n"+ + " - `partnershipPeriodStart` (LocalDate): 제휴 시작일\n"+ + " - `partnershipPeriodEnd` (LocalDate): 제휴 마감일\n"+ + " - `adminId` (Long): 관리자 ID\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `options` (JSON): 제휴 옵션\n" + + " - `optionType` (OptionType): 제공 서비스 종류 (서비스 제공, 할인)\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준 (금액, 인원)\n" + + " - `anotherType` (Boolean): 기타 제공 서비스\n" + + " - `people` (Integer): 서비스 제공 기준 인원 수\n" + + " - `cost` (Integer): 서비스 제공 기준 금액\n" + + " - `note` (String): 기타 유형 제휴 옵션 문구\n" + + " - `category` (String): 서비스 카테고리, 서비스 제공 항목이 여러 개 일 때 작성\n" + + " - `discountRate` (Long): 서비스 제공 인원 수\n" + + " - `goods` (JSON): 서비스 제공 항목\n" + + " - `goodsId` (Long): 서비스 제공 항목 ID\n" + + " - `goodsName` (String): 서비스 제공 항목명\n") @GetMapping("/{partnershipId}") - public BaseResponse getPartnership( - @PathVariable Long partnershipId + public BaseResponse getPartnership( + @PathVariable @Parameter(required = true) Long partnershipId ) { return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.getPartnership(partnershipId)); } @Operation( - summary = "제휴 상태 업데이트 API", - description = "제휴 ID와 바꾸고 싶은 상태를 입력하세요(SUSPEND/ACTIVE/INACTIVE/BLANK)" - ) - @PatchMapping("/{partnershipId}/status") - public BaseResponse updatePartnershipStatus( - @PathVariable("partnershipId") Long partnershipId, - @RequestBody PartnershipRequestDTO.UpdateRequestDTO request + summary = "제휴 제안서 삭제 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2fe1197c19ed80e58d30c469e4ba3146)\n" + + "- paperId와 관련된 모든 내용을 삭제합니다.\n" + + "- 성공 시 200(OK) 반환.\n" + + "\n**Parameters:**\n" + + " - `paperId` (Long, required): 삭제할 제안서 ID\n") + @DeleteMapping("/proposal/delete/{paperId}") + public BaseResponse deletePartnership( + @PathVariable @Parameter(required = true) Long paperId ) { - return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.updatePartnershipStatus(partnershipId, request)); + partnershipService.deletePartnership(paperId); + return BaseResponse.onSuccess(SuccessStatus._OK, null); } - @PostMapping("/proposal/draft") @Operation( - summary = "제휴 제안서 초안 생성 API", - description = "현재 로그인한 관리자(Admin)가 내용이 비어있는 제휴 제안서를 초안 상태로 생성합니다." - ) - public BaseResponse createDraftPartnership( - @RequestBody PartnershipRequestDTO.CreateDraftRequestDTO request, + summary = "제휴 중인 가게 조회 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/_-2241197c19ed81b1b9adf724adc4600c)\n" + + "- 현재 로그인한 관리자와 제휴 중인 가게를 조회합니다.\n" + + "- 전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정.\n" + + "\n**Parameters:**\n" + + " - `all` (boolean, required): 조회 옵션\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `WritePartnershipResponse` 객체 반환.\n" + + " - `partnershipId` (Long): 제안서 ID\n"+ + " - `partnershipPeriodStart` (LocalDate): 제휴 시작일\n"+ + " - `partnershipPeriodEnd` (LocalDate): 제휴 마감일\n"+ + " - `adminId` (Long): 관리자 ID\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `storeName` (String): 가게 이름\n" + + " - `adminName` (String): 관리자 이름\n" + + " - `isActivated` (ActivationStatus): 제안서 활성화 여부\n" + + " - `options` (JSON): 제휴 옵션\n" + + " - `optionType` (OptionType): 제공 서비스 종류 (서비스 제공, 할인)\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준 (금액, 인원)\n" + + " - `anotherType` (Boolean): 기타 제공 서비스\n" + + " - `people` (Integer): 서비스 제공 기준 인원 수\n" + + " - `cost` (Integer): 서비스 제공 기준 금액\n" + + " - `note` (String): 기타 유형 제휴 옵션 문구\n" + + " - `category` (String): 서비스 카테고리, 서비스 제공 항목이 여러 개 일 때 작성\n" + + " - `discountRate` (Long): 서비스 제공 인원 수\n" + + " - `goods` (JSON): 서비스 제공 항목\n" + + " - `goodsId` (Long): 서비스 제공 항목 ID\n" + + " - `goodsName` (String): 서비스 제공 항목명\n") + @GetMapping("/admin") + public BaseResponse> listForAdmin( + @RequestParam(name = "all", defaultValue = "false") boolean all, @AuthenticationPrincipal PrincipalDetails pd ) { - return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.createDraftPartnership(request, pd.getId())); + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForAdmin(all, pd.getId())); } - @DeleteMapping("/proposal/delete/{paperId}") @Operation( - summary = "제휴 제안서 삭제 API", - description = "특정 제휴 제안서(paperId)와 관련된 모든 데이터를 삭제합니다." - ) - public BaseResponse deletePartnership( - @PathVariable Long paperId + summary = "제휴 중인 관리자 조회 API", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/_-24f1197c19ed802784fddadbbd3ea2c6)\n" + + "- 현재 로그인한 제휴업체와 제휴 중인 관리자를 조회합니다.\n" + + "- 전체를 조회하려면 all을 true로, 가장 최근 두 건을 조회하려면 all을 false로 설정.\n" + + "\n**Parameters:**\n" + + " - `all` (boolean, required): 조회 옵션\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `WritePartnershipResponse` 객체 반환.\n" + + " - `partnershipId` (Long): 제안서 ID\n"+ + " - `partnershipPeriodStart` (LocalDate): 제휴 시작일\n"+ + " - `partnershipPeriodEnd` (LocalDate): 제휴 마감일\n"+ + " - `adminId` (Long): 관리자 ID\n" + + " - `partnerId` (Long): 제휴업체 ID\n" + + " - `storeId` (Long): 가게 ID\n" + + " - `storeName` (String): 가게 이름\n" + + " - `adminName` (String): 관리자 이름\n" + + " - `isActivated` (ActivationStatus): 제안서 활성화 여부\n" + + " - `options` (JSON): 제휴 옵션\n" + + " - `optionType` (OptionType): 제공 서비스 종류 (서비스 제공, 할인)\n" + + " - `criterionType` (CriterionType): 서비스 제공 기준 (금액, 인원)\n" + + " - `anotherType` (Boolean): 기타 제공 서비스\n" + + " - `people` (Integer): 서비스 제공 기준 인원 수\n" + + " - `cost` (Integer): 서비스 제공 기준 금액\n" + + " - `note` (String): 기타 유형 제휴 옵션 문구\n" + + " - `category` (String): 서비스 카테고리, 서비스 제공 항목이 여러 개 일 때 작성\n" + + " - `discountRate` (Long): 서비스 제공 인원 수\n" + + " - `goods` (JSON): 서비스 제공 항목\n" + + " - `goodsId` (Long): 서비스 제공 항목 ID\n" + + " - `goodsName` (String): 서비스 제공 항목명\n") + @GetMapping("/partner") + public BaseResponse> listForPartner( + @RequestParam(name = "all", defaultValue = "false") boolean all, + @AuthenticationPrincipal PrincipalDetails pd ) { - partnershipService.deletePartnership(paperId); - return BaseResponse.onSuccess(SuccessStatus._OK, null); + return BaseResponse.onSuccess(SuccessStatus._OK, partnershipService.listPartnershipsForPartner(all, pd.getId())); } - @GetMapping("/suspended") @Operation( summary = "대기 중인 제휴 계약서 조회 API", - description = "현재 로그인한 관리자(Admin)가 대기 중인 제휴 계약서를 모두 조회하여 리스트로 반환합니다." - ) - public BaseResponse> suspendPartnership( + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/_-24f1197c19ed802784fddadbbd3ea2c6)\n" + + "- 현재 로그인한 관리자와 제휴 중인 제안서 중 SUSPEND 상태인 제안서를 모두 조회합니다.\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `SuspendedPaper` 객체 반환.\n" + + " - `paperId` (Long): 제안서 ID\n"+ + " - `partnerName` (String): 제휴업체 이름\n"+ + " - `createdAt` (LocalDateTime): 제휴 생성 일자\n") + @GetMapping("/suspended") + 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, + summary = "채팅방 내 제휴 확인 API(관리자용)", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2fe1197c19ed8078af77d65bfcc09087)\n" + + "- 현재 로그인한 관리자와 파라미터로 받은 partnerId를 가진 제휴업체 간에 제휴를 조회합니다.\n" + + "- 비활성화 되지 않은 가장 최근 제휴 1건 조회.\n" + + "\n**Parameters:**\n" + + " - `partnerId` (Long, required): 제휴업체 ID\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `AdminPartnershipWithPartnerResponse` 객체 반환.\n" + + " - `paperId` (Long): 제안서 ID\n"+ + " - `isPartnered` (boolean): 제휴 여부\n"+ + " - `status` (String): 제휴 상태\n"+ + " - `partnerId` (Long): 제휴업체 ID\n"+ + " - `partnerName` (String): 제휴업체 이름\n"+ + " - `partnerAddress` (String): 제휴업체 주소\n") + @GetMapping("/check/admin/{partnerId}") + public BaseResponse checkAdminPartnership( + @PathVariable @Parameter(required = true) 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, + summary = "채팅방 내 제휴 확인 API(제휴업체용)", + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/2fe1197c19ed8078af77d65bfcc09087)\n" + + "- 현재 로그인한 제휴업체와 파라미터로 받은 adminId를 가진 관리자 간에 제휴를 조회합니다.\n" + + "- 비활성화 되지 않은 가장 최근 제휴 1건 조회.\n" + + "\n**Parameters:**\n" + + " - `adminId` (Long, required): 관리자 ID\n" + + "\n**Response:**\n" + + " - 성공 시 200(OK)과 `PartnerPartnershipWithAdminResponse` 객체 반환.\n" + + " - `paperId` (Long): 제안서 ID\n"+ + " - `isPartnered` (boolean): 제휴 여부\n"+ + " - `status` (String): 제휴 상태\n"+ + " - `adminId` (Long): 관리자 ID\n"+ + " - `adminName` (String): 관리자 이름\n"+ + " - `adminAddress` (String): 관리자 주소\n") + @GetMapping("/check/partner/{adminId}") + public BaseResponse checkPartnerPartnership( + @PathVariable @Parameter(required = true) 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 deleted file mode 100644 index 08bf6cdd..00000000 --- a/src/main/java/com/assu/server/domain/partnership/converter/PartnershipConverter.java +++ /dev/null @@ -1,269 +0,0 @@ -package com.assu.server.domain.partnership.converter; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; - -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.partnership.dto.PaperContentResponseDTO; -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.entity.enums.CriterionType; -import com.assu.server.domain.partnership.entity.enums.OptionType; -import com.assu.server.domain.store.entity.Store; - -public class PartnershipConverter { - - 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.BLANK) - .contractImageKey(null) - .build(); - } - - public static List toPaperContents( - PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO, - Paper paper - ) { - if (partnershipRequestDTO.getOptions() == null || partnershipRequestDTO.getOptions().isEmpty()) { - return Collections.emptyList(); - } - return partnershipRequestDTO.getOptions().stream() - .map(optionDto -> PaperContent.builder() - .note(optionDto.getNote()) // 일단 노트까지 받아서 변환 - .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(); - } - - - - - public static List> toGoodsBatches( - PartnershipRequestDTO.WritePartnershipRequestDTO partnershipRequestDTO - ) { - if (partnershipRequestDTO == null || partnershipRequestDTO.getOptions().isEmpty()) { - return Collections.emptyList(); - } - 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(); - } - - - 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()) - .note(o.getNote()) - .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<>(); - 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( - PartnershipResponseDTO.PartnershipOptionResponseDTO.builder() - .optionType(pc.getOptionType()) - .criterionType(pc.getCriterionType()) - .people(pc.getPeople()) - .note(note) - .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 ? 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(); - } - - 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()) - .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()); - paper.setIsActivated(ActivationStatus.SUSPEND); - } - - public static PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartnershipResultDTO( - Paper paper, - List contents, - List> 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); - String note = null; - if(pc.getNote()!= null){ - note = pc.getNote(); - } - 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()) - .note(note) - .category(pc.getCategory()) - .discountRate(pc.getDiscount()) - .goods(goodsResultDTO(goods)) - .build() - ); - } - } - - return PartnershipResponseDTO.GetPartnershipDetailResponseDTO.builder() - .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/AdminPartnershipCheckResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/AdminPartnershipCheckResponseDTO.java new file mode 100644 index 00000000..7be633a1 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/AdminPartnershipCheckResponseDTO.java @@ -0,0 +1,28 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.partner.entity.Partner; + +public record AdminPartnershipCheckResponseDTO( + Long paperId, + boolean isPartnered, + String status, + Long partnerId, + String partnerName, + String partnerAddress +) { + public static AdminPartnershipCheckResponseDTO of( + Partner partner, + Long paperId, + boolean isPartnered, + String status + ) { + return new AdminPartnershipCheckResponseDTO( + paperId, + isPartnered, + status, + partner.getId(), + partner.getName(), + partner.getAddress() + ); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/ManualPartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/ManualPartnershipRequestDTO.java new file mode 100644 index 00000000..b5194cb1 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/ManualPartnershipRequestDTO.java @@ -0,0 +1,42 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.map.dto.SelectedPlacePayload; +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 jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public record ManualPartnershipRequestDTO( + String storeName, + @NotNull SelectedPlacePayload selectedPlace, + String storeDetailAddress, + LocalDate partnershipPeriodStart, + LocalDate partnershipPeriodEnd, + List options +) { + public Paper toPaper(Admin admin, Store store, ActivationStatus status) { + return Paper.builder() + .admin(admin) + .store(store) + .partner(null) + .isActivated(status) + .partnershipPeriodStart(partnershipPeriodStart()) + .partnershipPeriodEnd(partnershipPeriodEnd()) + .build(); + } + + public List toPaperContents(Paper paper) { + if (options() == null || options().isEmpty()) return List.of(); + List list = new ArrayList<>(options().size()); + for (var o : options()) { + list.add(o.toPaperContent(paper)); + } + return list; + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/ManualPartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/ManualPartnershipResponseDTO.java new file mode 100644 index 00000000..f864a592 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/ManualPartnershipResponseDTO.java @@ -0,0 +1,29 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.store.entity.Store; + +public record ManualPartnershipResponseDTO( + Long storeId, + boolean storeCreated, + boolean storeActivated, + String status, + String contractImageUrl, + WritePartnershipResponseDTO partnership +) { + public static ManualPartnershipResponseDTO of( + Store store, + boolean storeCreated, + boolean storeActivated, + String contractImageUrl, + WritePartnershipResponseDTO partnership + ) { + return new ManualPartnershipResponseDTO( + store.getId(), + storeCreated, + storeActivated, + store.getIsActivate() == null ? null : store.getIsActivate().name(), + contractImageUrl, + partnership + ); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnerPartnershipCheckResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnerPartnershipCheckResponseDTO.java new file mode 100644 index 00000000..5b38ce7f --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnerPartnershipCheckResponseDTO.java @@ -0,0 +1,28 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.admin.entity.Admin; + +public record PartnerPartnershipCheckResponseDTO( + Long paperId, + boolean isPartnered, + String status, + Long adminId, + String adminName, + String adminAddress +) { + public static PartnerPartnershipCheckResponseDTO of( + Admin admin, + Long paperId, + boolean isPartnered, + String status + ) { + return new PartnerPartnershipCheckResponseDTO( + paperId, + isPartnered, + status, + admin.getId(), + admin.getName(), + admin.getOfficeAddress() + ); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDetailResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDetailResponseDTO.java new file mode 100644 index 00000000..ed04f31a --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDetailResponseDTO.java @@ -0,0 +1,72 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.common.entity.BaseEntity; +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 java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +public record PartnershipDetailResponseDTO( + Long partnershipId, + LocalDateTime updatedAt, + LocalDate partnershipPeriodStart, + LocalDate partnershipPeriodEnd, + Long adminId, + Long partnerId, + Long storeId, + List options +) { + public static PartnershipDetailResponseDTO of( + Paper paper, + List contents, + List> 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(PartnershipOptionResponseDTO.of(pc, goods)); + } + } + + return new PartnershipDetailResponseDTO( + paper.getId(), + mostRecentUpdatedAt, + paper.getPartnershipPeriodStart(), + paper.getPartnershipPeriodEnd(), + paper.getAdmin() != null ? paper.getAdmin().getId() : null, + paper.getPartner() != null ? paper.getPartner().getId() : null, + paper.getStore() != null ? paper.getStore().getId() : null, + optionDTOs + ); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDraftRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDraftRequestDTO.java new file mode 100644 index 00000000..44b0a188 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDraftRequestDTO.java @@ -0,0 +1,23 @@ +package com.assu.server.domain.partnership.dto; + +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 com.assu.server.domain.store.entity.Store; + +public record PartnershipDraftRequestDTO( + Long partnerId +) { + public Paper toDraftPaper(Admin admin, Partner partner, Store store) { + return Paper.builder() + .admin(admin) + .partner(partner) + .store(store) + .partnershipPeriodStart(null) + .partnershipPeriodEnd(null) + .isActivated(ActivationStatus.BLANK) + .contractImageKey(null) + .build(); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDraftResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDraftResponseDTO.java new file mode 100644 index 00000000..197c3118 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipDraftResponseDTO.java @@ -0,0 +1,11 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.partnership.entity.Paper; + +public record PartnershipDraftResponseDTO( + Long paperId +) { + public static PartnershipDraftResponseDTO of(Paper paper) { + return new PartnershipDraftResponseDTO(paper.getId()); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipGoodsRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipGoodsRequestDTO.java new file mode 100644 index 00000000..faaca6f8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipGoodsRequestDTO.java @@ -0,0 +1,6 @@ +package com.assu.server.domain.partnership.dto; + +public record PartnershipGoodsRequestDTO( + String goodsName +) { +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipGoodsResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipGoodsResponseDTO.java new file mode 100644 index 00000000..37b864c7 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipGoodsResponseDTO.java @@ -0,0 +1,19 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.partnership.entity.Goods; + +import java.util.List; + +public record PartnershipGoodsResponseDTO( + Long goodsId, + String goodsName +) { + public static PartnershipGoodsResponseDTO of(Goods goods) { + return new PartnershipGoodsResponseDTO(goods.getId(), goods.getBelonging()); + } + + public static List ofList(List goods) { + if (goods == null || goods.isEmpty()) return List.of(); + return goods.stream().map(PartnershipGoodsResponseDTO::of).toList(); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipOptionRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipOptionRequestDTO.java new file mode 100644 index 00000000..940aa8fa --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipOptionRequestDTO.java @@ -0,0 +1,48 @@ +package com.assu.server.domain.partnership.dto; + +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 java.util.ArrayList; +import java.util.List; + +public record PartnershipOptionRequestDTO( + OptionType optionType, + CriterionType criterionType, + Boolean anotherType, + Integer people, + Long cost, + String category, + Long discountRate, + String note, + List goods +) { + public PaperContent toPaperContent(Paper paper) { + return PaperContent.builder() + .note(note()) + .paper(paper) + .optionType(optionType()) + .criterionType(criterionType()) + .anotherType(anotherType()) + .people(people()) + .cost(cost()) + .category(category()) + .discount(discountRate()) + .build(); + } + + public List toGoods(PaperContent content) { + if (goods() == null || goods().isEmpty()) return List.of(); + List batch = new ArrayList<>(goods().size()); + for (var g : goods()) { + batch.add(Goods.builder() + .content(content) + .belonging(g.goodsName()) + .build()); + } + return batch; + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipOptionResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipOptionResponseDTO.java new file mode 100644 index 00000000..4bb036ee --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipOptionResponseDTO.java @@ -0,0 +1,35 @@ +package com.assu.server.domain.partnership.dto; + +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 java.util.List; + +public record PartnershipOptionResponseDTO( + OptionType optionType, + CriterionType criterionType, + Boolean anotherType, + Integer people, + Long cost, + String note, + String category, + Long discountRate, + List goods +) { + public static PartnershipOptionResponseDTO of(PaperContent pc, List goods) { + String note = pc.getNote() != null ? pc.getNote() : null; + return new PartnershipOptionResponseDTO( + pc.getOptionType(), + pc.getCriterionType(), + pc.getAnotherType(), + pc.getPeople(), + pc.getCost(), + note, + pc.getCategory(), + pc.getDiscount(), + PartnershipGoodsResponseDTO.ofList(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 deleted file mode 100644 index dfbc7edf..00000000 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipRequestDTO.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.assu.server.domain.partnership.dto; -import java.util.List; -import lombok.Getter; - -import com.assu.server.domain.common.enums.ActivationStatus; -import com.assu.server.domain.map.dto.SelectedPlacePayload; -import com.assu.server.domain.partnership.entity.enums.CriterionType; -import com.assu.server.domain.partnership.entity.enums.OptionType; -import jakarta.validation.constraints.NotNull; -import lombok.*; - -import java.time.LocalDate; - -public class PartnershipRequestDTO { - - @Getter - public static class WritePartnershipRequestDTO { - private Long paperId; // 제휴 제안서 아이디 - private LocalDate partnershipPeriodStart; - private LocalDate partnershipPeriodEnd; - 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 String note; - private List goods; // 서비스 제공 항목 - - } - - @Getter - public static class PartnershipGoodsRequestDTO { - private String goodsName; - } - - @Getter - @Setter - @NoArgsConstructor - public static class UpdateRequestDTO { - private String status; - } - - @Getter - @Setter - @NoArgsConstructor - public static class ManualPartnershipRequestDTO { - private String storeName; - @NotNull private SelectedPlacePayload selectedPlace; - private String storeDetailAddress; - private LocalDate partnershipPeriodStart; - 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 deleted file mode 100644 index 712273cf..00000000 --- a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipResponseDTO.java +++ /dev/null @@ -1,147 +0,0 @@ -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.*; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; - -public class PartnershipResponseDTO { - - @Getter - @Setter - @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 String storeName; - private String adminName; - private ActivationStatus isActivated; - 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 note; - private String category; - private Long discountRate; - - private List goods; - } - - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - @Builder - 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; - } - - @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 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; // 제휴 상태 - private Long partnerId; - private String partnerName; - private String partnerAddress; - } - - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class PartnerPartnershipWithAdminResponseDTO { - private Long paperId; - private boolean isPartnered; // 제휴 여부 - private String status; // 제휴 상태 - private Long adminId; - private String adminName; - private String adminAddress; - } - - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class GetPartnershipDetailResponseDTO { - private Long partnershipId; - private LocalDateTime updatedAt; - 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/dto/PartnershipStatusUpdateRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipStatusUpdateRequestDTO.java new file mode 100644 index 00000000..aa95ef9b --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipStatusUpdateRequestDTO.java @@ -0,0 +1,6 @@ +package com.assu.server.domain.partnership.dto; + +public record PartnershipStatusUpdateRequestDTO( + String status +) { +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/PartnershipStatusUpdateResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipStatusUpdateResponseDTO.java new file mode 100644 index 00000000..38835858 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/PartnershipStatusUpdateResponseDTO.java @@ -0,0 +1,26 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.common.enums.ActivationStatus; +import com.assu.server.domain.partnership.entity.Paper; + +import java.time.LocalDateTime; + +public record PartnershipStatusUpdateResponseDTO( + Long partnershipId, + String prevStatus, + String newStatus, + LocalDateTime changedAt +) { + public static PartnershipStatusUpdateResponseDTO of( + Paper paper, + ActivationStatus prevPaperStatus, + ActivationStatus nextPaperStatus + ) { + return new PartnershipStatusUpdateResponseDTO( + paper.getId(), + prevPaperStatus == null ? null : prevPaperStatus.name(), + nextPaperStatus.name(), + LocalDateTime.now() + ); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/SuspendedPaperResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/SuspendedPaperResponseDTO.java new file mode 100644 index 00000000..50d892ce --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/SuspendedPaperResponseDTO.java @@ -0,0 +1,21 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.partnership.entity.Paper; + +import java.time.LocalDateTime; + +public record SuspendedPaperResponseDTO( + Long paperId, + String partnerName, + LocalDateTime createdAt +) { + public static SuspendedPaperResponseDTO of(Paper paper) { + return new SuspendedPaperResponseDTO( + paper.getId(), + paper.getPartner() != null + ? paper.getPartner().getName() + : (paper.getStore() != null ? paper.getStore().getName() : "미등록"), + paper.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/WritePartnershipRequestDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/WritePartnershipRequestDTO.java new file mode 100644 index 00000000..6331de28 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/WritePartnershipRequestDTO.java @@ -0,0 +1,46 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.common.enums.ActivationStatus; +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 java.time.LocalDate; +import java.util.Collections; +import java.util.List; + +public record WritePartnershipRequestDTO( + Long paperId, + LocalDate partnershipPeriodStart, + LocalDate partnershipPeriodEnd, + List options +) { + public void updatePaper(Paper paper) { + paper.setPartnershipPeriodStart(partnershipPeriodStart()); + paper.setPartnershipPeriodEnd(partnershipPeriodEnd()); + paper.setIsActivated(ActivationStatus.SUSPEND); + } + + public List toPaperContents(Paper paper) { + if (options() == null || options().isEmpty()) return Collections.emptyList(); + return options().stream() + .map(opt -> opt.toPaperContent(paper)) + .toList(); + } + + public List> toGoodsBatches() { + if (options() == null || options().isEmpty()) return Collections.emptyList(); + return options().stream() + .map(optionDto -> { + if (optionDto.goods() == null || optionDto.goods().isEmpty()) { + return Collections.emptyList(); + } + return optionDto.goods().stream() + .map(goodsDto -> Goods.builder() + .belonging(goodsDto.goodsName()) + .build()) + .toList(); + }) + .toList(); + } +} diff --git a/src/main/java/com/assu/server/domain/partnership/dto/WritePartnershipResponseDTO.java b/src/main/java/com/assu/server/domain/partnership/dto/WritePartnershipResponseDTO.java new file mode 100644 index 00000000..82609319 --- /dev/null +++ b/src/main/java/com/assu/server/domain/partnership/dto/WritePartnershipResponseDTO.java @@ -0,0 +1,51 @@ +package com.assu.server.domain.partnership.dto; + +import com.assu.server.domain.common.enums.ActivationStatus; +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 java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public record WritePartnershipResponseDTO( + Long partnershipId, + LocalDate partnershipPeriodStart, + LocalDate partnershipPeriodEnd, + Long adminId, + Long partnerId, + Long storeId, + String storeName, + String adminName, + ActivationStatus isActivated, + List options +) { + public static WritePartnershipResponseDTO of( + Paper paper, + List contents, + List> goodsBatches + ) { + 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(PartnershipOptionResponseDTO.of(pc, goods)); + } + } + return new WritePartnershipResponseDTO( + paper.getId(), + paper.getPartnershipPeriodStart(), + paper.getPartnershipPeriodEnd(), + paper.getAdmin() != null ? paper.getAdmin().getId() : null, + paper.getPartner() != null ? paper.getPartner().getId() : null, + paper.getStore() != null ? paper.getStore().getId() : null, + paper.getStore().getName(), + paper.getAdmin().getName(), + paper.getIsActivated(), + optionDTOs + ); + } +} 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 eea1810e..771baaf5 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 @@ -9,7 +9,6 @@ import jakarta.persistence.*; import lombok.*; -import java.time.LocalDate; @Entity @Getter @@ -21,9 +20,8 @@ public class Paper extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Setter - private LocalDate partnershipPeriodStart; // LocalDate vs String + private LocalDate partnershipPeriodStart; @Setter private LocalDate partnershipPeriodEnd; @@ -32,7 +30,6 @@ public class Paper extends BaseEntity { @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 94cd9738..dea6f3b7 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,8 +1,8 @@ package com.assu.server.domain.partnership.entity; + import com.assu.server.domain.common.entity.BaseEntity; 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; @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.List; - @Entity @Getter @NoArgsConstructor @@ -28,11 +27,16 @@ public class PaperContent extends BaseEntity { private Paper paper; @Enumerated(EnumType.STRING) + @Column(name = "criterion_type", length = 30) private CriterionType criterionType; @Enumerated(EnumType.STRING) + @Column(name = "option_type", length = 30) private OptionType optionType; + private Boolean anotherType; + + @Column(name = "note", length = 255) private String note; private Integer people; 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 66f970b5..ea05acfc 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 @@ -13,6 +13,9 @@ public interface GoodsRepository extends JpaRepository { List findByContentId(Long contentId); + @Query("SELECT g FROM Goods g WHERE g.content.id IN :contentIds") + List findByContentIdIn(@Param("contentIds") List contentIds); + @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/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java index 0d55171a..25a3b287 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 @@ -11,8 +11,6 @@ public interface PaperContentRepository extends JpaRepository { - Optional findTopByPaperStoreIdOrderByIdDesc(Long storeId); - List findByPaperId(Long paperId); @Query(""" @@ -33,39 +31,6 @@ public interface PaperContentRepository extends JpaRepository findById(Long id); - @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") 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() - ); - @Query(value = """ WITH ranked_content AS ( SELECT pc.*, @@ -84,7 +49,7 @@ WITH ranked_content AS ( FROM paper_content pc JOIN paper p ON p.id = pc.paper_id WHERE p.store_id IN :storeIds - AND p.id IN :userPaperIds -- 대장님이 추가하신 핵심 조건! + AND p.id IN :userPaperIds AND p.is_activated = :active AND CURRENT_DATE BETWEEN p.partnership_period_start AND p.partnership_period_end AND ( @@ -108,4 +73,65 @@ List findLatestValidByStoreIdInNativeMax2( Optional findTopByPaperIdOrderByIdDesc(Long paperId); + @Query(value = """ +WITH ranked_content AS ( + SELECT pc.*, + ROW_NUMBER() OVER ( + PARTITION BY p.store_id + ORDER BY + CASE pc.option_type + WHEN :service THEN 0 ELSE 1 END, + CASE pc.criterion_type + WHEN :price THEN 0 + WHEN :headcount THEN 1 + ELSE 2 END, + pc.updated_at DESC, + pc.id DESC + ) AS rn + FROM paper_content pc + JOIN paper p ON p.id = pc.paper_id + WHERE p.store_id IN :storeIds + 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) + ) +) +SELECT * FROM ranked_content WHERE rn = 1 +""", nativeQuery = true) + List findLatestValidByStoreIdInNative( + @Param("storeIds") List storeIds, + @Param("active") String active, + @Param("service") String service, + @Param("discount") String discount, + @Param("price") String price, + @Param("headcount") String headcount + ); + + @Query(""" + SELECT pc + FROM PaperContent pc + WHERE pc.paper.store.id IN :storeIds + ORDER BY pc.id DESC + """) + List findTopByStoreIdIn(@Param("storeIds") List storeIds); + + /** + * 주어진 paper_id 목록에서 각 paper의 가장 최신 PaperContent를 1건씩 반환. + * paper_id 컬럼 인덱스를 활용한 ROW_NUMBER() 윈도우 함수 사용. + */ + @Query(value = """ + WITH ranked AS ( + SELECT pc.*, + ROW_NUMBER() OVER (PARTITION BY pc.paper_id ORDER BY pc.id DESC) AS rn + FROM paper_content pc + WHERE pc.paper_id IN :paperIds + ) + SELECT * FROM ranked WHERE rn = 1 + """, nativeQuery = true) + List findLatestByPaperIds(@Param("paperIds") List 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 index 60098687..e195e5d2 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 @@ -26,13 +26,9 @@ List findByStoreIdAndAdminIdAndStatus( @Param("adminId")Long adminId, @Param("status")ActivationStatus status); - // Admin 기준 (ACTIVE) List findByAdmin_IdAndIsActivated(Long adminId, ActivationStatus status, Sort sort); Page findByAdmin_IdAndIsActivated(Long adminId, ActivationStatus status, Pageable pageable); - 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); @@ -47,7 +43,6 @@ List findAllSuspendedByAdminWithNoPartner( // Partner 기준 (ACTIVE) 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); @Query(""" @@ -63,6 +58,37 @@ List findActivePapersByAdminIds(@Param("adminIds") List adminIds, List findByStoreIdAndAdminIdAndIsActivated(Long storeId, Long adminId, ActivationStatus isActivated); + @Query(""" + SELECT p FROM Paper p + WHERE p.admin.id = :adminId + AND p.partner.id IN :partnerIds + AND p.isActivated = :status + """) + List findByAdminIdAndPartnerIdInAndIsActivated( + @Param("adminId") Long adminId, + @Param("partnerIds") List partnerIds, + @Param("status") ActivationStatus status + ); + + @Query(""" + SELECT p FROM Paper p + WHERE p.admin.id IN :adminIds + AND p.partner.id = :partnerId + AND p.isActivated = :status + """) + List findByAdminIdInAndPartnerIdAndIsActivated( + @Param("adminIds") List adminIds, + @Param("partnerId") Long partnerId, + @Param("status") ActivationStatus status + ); + + @Query(""" + SELECT p FROM Paper p + WHERE p.store.id IN :storeIds + ORDER BY p.id DESC + """) + List findByStoreIdIn(@Param("storeIds") List storeIds); + // PaperRepository.java에 추가 @Query(""" SELECT p 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 52f36d2a..81a6abcf 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 @@ -71,10 +71,8 @@ public PaperResponseDTO getStorePaperContent(Long storeId, Member member){ // dto 변환 List contents = toContentResponseList(contentList); - return new PaperResponseDTO(contents, store.getName(), store.getId()); - } /** 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 1f8f4455..a9dd93ef 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 @@ -1,48 +1,43 @@ package com.assu.server.domain.partnership.service; import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.partnership.dto.AdminPartnershipCheckResponseDTO; +import com.assu.server.domain.partnership.dto.ManualPartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.ManualPartnershipResponseDTO; +import com.assu.server.domain.partnership.dto.PartnerPartnershipCheckResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipDetailResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipDraftRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipDraftResponseDTO; import com.assu.server.domain.partnership.dto.PartnershipFinalRequestDTO; -import com.assu.server.domain.partnership.dto.PartnershipRequestDTO; -import com.assu.server.domain.partnership.dto.PartnershipResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipStatusUpdateRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipStatusUpdateResponseDTO; +import com.assu.server.domain.partnership.dto.SuspendedPaperResponseDTO; +import com.assu.server.domain.partnership.dto.WritePartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.WritePartnershipResponseDTO; import org.springframework.web.multipart.MultipartFile; import java.util.List; public interface PartnershipService { - // 제휴 제안서 수정 - PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership( - PartnershipRequestDTO.WritePartnershipRequestDTO request, - Long memberId - ); - + WritePartnershipResponseDTO updatePartnership(WritePartnershipRequestDTO request, Long memberId); + void recordPartnershipUsage(PartnershipFinalRequestDTO dto, Member member); - // 제휴업체/관리자 맺은 제휴 리스트 - List listPartnershipsForAdmin(boolean all, Long partnerId); - List listPartnershipsForPartner(boolean all, Long adminId); + List listPartnershipsForAdmin(boolean all, Long partnerId); + List listPartnershipsForPartner(boolean all, Long adminId); - // 제휴 제안서 조회 - PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartnership(Long partnershipId); - List getSuspendedPapers(Long adminId); + PartnershipDetailResponseDTO getPartnership(Long partnershipId); + List getSuspendedPapers(Long adminId); - // 제휴 상태 업데이트 - PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request); + PartnershipStatusUpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipStatusUpdateRequestDTO request); - // 제휴 수동 등록 - PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership( - PartnershipRequestDTO.ManualPartnershipRequestDTO request, - Long adminId, - MultipartFile contractImage - ); + ManualPartnershipResponseDTO createManualPartnership(ManualPartnershipRequestDTO request, Long adminId, MultipartFile contractImage); - // 빈 제휴제안서 만들기 - PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(PartnershipRequestDTO.CreateDraftRequestDTO request, Long adminId); + PartnershipDraftResponseDTO createDraftPartnership(PartnershipDraftRequestDTO request, Long adminId); - // 제휴 계약서 삭제 void deletePartnership(Long paperId); - // 채팅방 내 제휴 계약서 상태 확인 - PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId); // 관리자가 조회 - PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId); // 제휴업체가 조회 + AdminPartnershipCheckResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId); + PartnerPartnershipCheckResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId); } diff --git a/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java b/src/main/java/com/assu/server/domain/partnership/service/PartnershipServiceImpl.java index b1452f15..482dde2e 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,33 +1,29 @@ package com.assu.server.domain.partnership.service; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import com.assu.server.domain.admin.entity.Admin; +import com.assu.server.domain.admin.repository.AdminRepository; 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.common.enums.ActivationStatus; import com.assu.server.domain.member.entity.Member; import com.assu.server.domain.notification.service.NotificationCommandService; -import com.assu.server.domain.partnership.converter.PartnershipConverter; -import com.assu.server.domain.partnership.dto.PartnershipFinalRequestDTO; -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.dto.AdminPartnershipCheckResponseDTO; +import com.assu.server.domain.partnership.dto.ManualPartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.ManualPartnershipResponseDTO; +import com.assu.server.domain.partnership.dto.PartnerPartnershipCheckResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipDetailResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipDraftRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipDraftResponseDTO; +import com.assu.server.domain.partnership.dto.PartnershipFinalRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipStatusUpdateRequestDTO; +import com.assu.server.domain.partnership.dto.PartnershipStatusUpdateResponseDTO; +import com.assu.server.domain.partnership.dto.SuspendedPaperResponseDTO; +import com.assu.server.domain.partnership.dto.WritePartnershipRequestDTO; +import com.assu.server.domain.partnership.dto.WritePartnershipResponseDTO; import com.assu.server.domain.partnership.entity.Goods; import com.assu.server.domain.partnership.entity.Paper; import com.assu.server.domain.partnership.entity.PaperContent; @@ -36,19 +32,24 @@ 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.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 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 lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.time.LocalDateTime; + import java.util.*; import java.util.stream.Collectors; - - @Service @Transactional @RequiredArgsConstructor @@ -60,7 +61,13 @@ public class PartnershipServiceImpl implements PartnershipService { private final NotificationCommandService notificationService; private final ChatService chatService; private final ChatRepository chatRepository; - + private final PaperRepository paperRepository; + private final PaperContentRepository paperContentRepository; + private final GoodsRepository goodsRepository; + private final AdminRepository adminRepository; + private final PartnerRepository partnerRepository; + private final StoreRepository storeRepository; + private final AmazonS3Manager amazonS3Manager; @Override @Transactional @@ -103,30 +110,17 @@ public void recordPartnershipUsage(PartnershipFinalRequestDTO dto, Member member // @Transactional 환경에서는 studentsToUpdate의 변경 사항(스탬프)이 자동으로 DB에 반영됩니다. } - - - - private final PaperRepository paperRepository; - private final PaperContentRepository paperContentRepository; - private final GoodsRepository goodsRepository; - - private final AdminRepository adminRepository; - private final PartnerRepository partnerRepository; - private final StoreRepository storeRepository; - - private final AmazonS3Manager amazonS3Manager; - @Override @Transactional - public PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership( - PartnershipRequestDTO.WritePartnershipRequestDTO request, + public WritePartnershipResponseDTO updatePartnership( + WritePartnershipRequestDTO request, Long memberId ) { if (request == null || memberId == null) { throw new DatabaseException(ErrorStatus._BAD_REQUEST); } - Paper paper = paperRepository.findById(request.getPaperId()) + Paper paper = paperRepository.findById(request.paperId()) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); Partner partner = partnerRepository.findById(memberId) @@ -138,19 +132,19 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership( Store store = storeRepository.findByPartner(partner) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_STORE)); - PartnershipConverter.updatePaperFromDto(paper, request); + request.updatePaper(paper); - List existingContents = paperContentRepository.findByPaperId(request.getPaperId()); + List existingContents = paperContentRepository.findByPaperId(request.paperId()); if (!existingContents.isEmpty()) { List contentIds = existingContents.stream().map(PaperContent::getId).toList(); goodsRepository.deleteAllByContentIds(contentIds); paperContentRepository.deleteAll(existingContents); } - List newContents = PartnershipConverter.toPaperContents(request, paper); + List newContents = request.toPaperContents(paper); newContents = newContents.isEmpty() ? newContents : paperContentRepository.saveAll(newContents); - List> requestGoodsBatches = PartnershipConverter.toGoodsBatches(request); + List> requestGoodsBatches = request.toGoodsBatches(); List> attachedGoodsBatches = new ArrayList<>(); List toPersist = new ArrayList<>(); @@ -173,11 +167,12 @@ public PartnershipResponseDTO.WritePartnershipResponseDTO updatePartnership( goodsRepository.saveAll(toPersist); } - return PartnershipConverter.writePartnershipResultDTO(paper, newContents, attachedGoodsBatches); + return WritePartnershipResponseDTO.of(paper, newContents, attachedGoodsBatches); } @Override - public List listPartnershipsForAdmin(boolean all, Long adminId) { + @Transactional(readOnly = true) + 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) @@ -191,7 +186,8 @@ public List listPartnerships } @Override - public List listPartnershipsForPartner(boolean all, Long partnerId) { + @Transactional(readOnly = true) + 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) @@ -205,8 +201,8 @@ public List listPartnerships } @Override - @Transactional - public PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartnership(Long partnershipId) { + @Transactional(readOnly = true) + public PartnershipDetailResponseDTO getPartnership(Long partnershipId) { Paper paper = paperRepository.findById(partnershipId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); @@ -216,40 +212,32 @@ public PartnershipResponseDTO.GetPartnershipDetailResponseDTO getPartnership(Lon .map(pc -> pc.getGoods() == null ? Collections.emptyList() : pc.getGoods()) .toList(); - return PartnershipConverter.getPartnershipResultDTO(paper, contents, goodsBatches); + return PartnershipDetailResponseDTO.of(paper, contents, goodsBatches); } @Override - @Transactional - public List getSuspendedPapers(Long adminId) { + @Transactional(readOnly = true) + public List getSuspendedPapers(Long adminId) { List suspendedPapers = paperRepository.findAllSuspendedByAdminWithNoPartner(ActivationStatus.SUSPEND, adminId); return suspendedPapers.stream() - .map(paper -> PartnershipResponseDTO.SuspendedPaperDTO.builder() - .paperId(paper.getId()) - .partnerName( - paper.getPartner() != null - ? paper.getPartner().getName() - : (paper.getStore() != null ? paper.getStore().getName() : "미등록") - ) - .createdAt(paper.getCreatedAt()) - .build()) + .map(SuspendedPaperResponseDTO::of) .toList(); } @Override @Transactional - public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipRequestDTO.UpdateRequestDTO request) { + public PartnershipStatusUpdateResponseDTO updatePartnershipStatus(Long partnershipId, PartnershipStatusUpdateRequestDTO request) { Paper paper = paperRepository.findById(partnershipId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PAPER)); - if(request == null || request.getStatus() == null){ + if(request == null || request.status() == null){ throw new DatabaseException(ErrorStatus._BAD_REQUEST); } ActivationStatus prev = paper.getIsActivated(); - ActivationStatus next = parseStatus(request.getStatus()); + ActivationStatus next = parseStatus(request.status()); paper.setIsActivated(next); @@ -284,18 +272,13 @@ public PartnershipResponseDTO.UpdateResponseDTO updatePartnershipStatus(Long par notificationService.sendChat(partnerId, chattingRoom.getId(), admin.getName(), guideMessage); } - return PartnershipResponseDTO.UpdateResponseDTO.builder() - .partnershipId(paper.getId()) - .prevStatus(prev == null ? null : prev.name()) - .newStatus(next.name()) - .changedAt(LocalDateTime.now()) - .build(); + return PartnershipStatusUpdateResponseDTO.of(paper, prev, next); } @Override @Transactional - public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnership( - PartnershipRequestDTO.ManualPartnershipRequestDTO request, + public ManualPartnershipResponseDTO createManualPartnership( + ManualPartnershipRequestDTO request, Long adminId, MultipartFile contractImage) { @@ -305,10 +288,10 @@ public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnersh Admin admin = adminRepository.findById(adminId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - String address = pickDisplayAddress(request.getSelectedPlace().getRoadAddress(), request.getSelectedPlace().getAddress()); + String address = pickDisplayAddress(request.selectedPlace().getRoadAddress(), request.selectedPlace().getAddress()); Store store = storeRepository - .findByNameAndAddressAndDetailAddress(request.getStoreName(), address, request.getStoreDetailAddress()) + .findByNameAndAddressAndDetailAddress(request.storeName(), address, request.storeDetailAddress()) .orElse(null); boolean created = false; @@ -316,9 +299,9 @@ public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnersh if (store == null) { store = Store.builder() - .name(request.getStoreName()) + .name(request.storeName()) .address(address) - .detailAddress(request.getStoreDetailAddress()) + .detailAddress(request.storeDetailAddress()) .rate(0) .isActivate(ActivationStatus.SUSPEND) .build(); @@ -329,12 +312,7 @@ public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnersh reactivated = true; } - Paper paper = PartnershipConverter.toPaperForManual( - admin, store, - request.getPartnershipPeriodStart(), - request.getPartnershipPeriodEnd(), - ActivationStatus.SUSPEND - ); + Paper paper = request.toPaper(admin, store, ActivationStatus.SUSPEND); paper = paperRepository.save(paper); if (contractImage != null && !contractImage.isEmpty()) { @@ -349,15 +327,15 @@ public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnersh } List savedContents = new ArrayList<>(); - if (request.getOptions() != null && !request.getOptions().isEmpty()) { - List contents = PartnershipConverter.toPaperContentsForManual(request.getOptions(), paper); + if (request.options() != null && !request.options().isEmpty()) { + List contents = request.toPaperContents(paper); savedContents = paperContentRepository.saveAll(contents); List toPersist = new ArrayList<>(); for (int i = 0; i < savedContents.size(); i++) { - var opt = request.getOptions().get(i); + var opt = request.options().get(i); var content = savedContents.get(i); - var batch = PartnershipConverter.toGoodsForContent(opt, content); + var batch = opt.toGoods(content); if (!batch.isEmpty()) toPersist.addAll(batch); } if (!toPersist.isEmpty()) goodsRepository.saveAll(toPersist); @@ -368,33 +346,26 @@ public PartnershipResponseDTO.ManualPartnershipResponseDTO createManualPartnersh .map(pc -> pc.getGoods() == null ? List.of() : pc.getGoods()) .toList(); - var partnership = PartnershipConverter.writePartnershipResultDTO(paper, contentsWithGoods, goodsBatches); + var partnership = WritePartnershipResponseDTO.of(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(); + : amazonS3Manager.generatePresignedUrl(paper.getContractImageKey()); + + return ManualPartnershipResponseDTO.of(store, created, reactivated, url, partnership); } @Override @Transactional - public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(PartnershipRequestDTO.CreateDraftRequestDTO request, Long adminId) { + public PartnershipDraftResponseDTO createDraftPartnership(PartnershipDraftRequestDTO request, Long adminId) { Admin admin = adminRepository.findById(adminId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); - Partner partner = partnerRepository.findById(request.getPartnerId()) + Partner partner = partnerRepository.findById(request.partnerId()) .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); + Paper draftPaper = request.toDraftPaper(admin, partner, store); paperRepository.save(draftPaper); notificationService.sendPartnerProposal(partner.getId(), draftPaper.getId(), admin.getName()); @@ -413,7 +384,7 @@ public PartnershipResponseDTO.CreateDraftResponseDTO createDraftPartnership(Part chatService.sendGuideMessage(guideMessageRequest); notificationService.sendChat(partner.getId(), chattingRoom.getId(), admin.getName(), guideMessage); - return PartnershipConverter.toCreateDraftResponseDTO(draftPaper); + return PartnershipDraftResponseDTO.of(draftPaper); } @Override @@ -451,12 +422,11 @@ public void deletePartnership(Long paperId) { storeRepository.delete(store); } } - } @Override - @Transactional - public PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId) { + @Transactional(readOnly = true) + public AdminPartnershipCheckResponseDTO checkPartnershipWithPartner(Long adminId, Long partnerId) { Partner partner = partnerRepository.findById(partnerId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_PARTNER)); @@ -478,19 +448,12 @@ public PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO checkPartne } } - return PartnershipResponseDTO.AdminPartnershipWithPartnerResponseDTO.builder() - .paperId(paperId) - .isPartnered(isPartnered) - .status(status) - .partnerId(partner.getId()) - .partnerName(partner.getName()) - .partnerAddress(partner.getAddress()) - .build(); + return AdminPartnershipCheckResponseDTO.of(partner, paperId, isPartnered, status); } @Override - @Transactional - public PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId) { + @Transactional(readOnly = true) + public PartnerPartnershipCheckResponseDTO checkPartnershipWithAdmin(Long partnerId, Long adminId) { Admin admin = adminRepository.findById(adminId) .orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ADMIN)); @@ -512,17 +475,10 @@ public PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO checkPartne } } - return PartnershipResponseDTO.PartnerPartnershipWithAdminResponseDTO.builder() - .paperId(paperId) - .isPartnered(isPartnered) - .status(status) - .adminId(admin.getId()) - .adminName(admin.getName()) - .adminAddress(admin.getOfficeAddress()) - .build(); + return PartnerPartnershipCheckResponseDTO.of(admin, paperId, isPartnered, status); } - private List buildPartnershipDTOs(List papers) { + private List buildPartnershipDTOs(List papers) { if (papers == null || papers.isEmpty()) return List.of(); List paperIds = papers.stream().map(Paper::getId).toList(); @@ -531,13 +487,13 @@ private List buildPartnershi Map> byPaperId = allContents.stream() .collect(Collectors.groupingBy(pc -> pc.getPaper().getId())); - List result = new ArrayList<>(papers.size()); + 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)); + result.add(WritePartnershipResponseDTO.of(p, contents, goodsBatches)); } return result; } 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 7114810e..aabeb715 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 @@ -115,7 +115,27 @@ AND ST_Contains(ST_GeomFromText(:wkt, 4326), s.point) """, nativeQuery = true) List findAllWithinViewport(@Param("wkt") String wkt); + @Query(""" + SELECT DISTINCT s + FROM Store s + LEFT JOIN FETCH s.partner p + LEFT JOIN FETCH p.member + WHERE s.point IS NOT NULL + AND function('ST_Contains', function('ST_GeomFromText', :wkt, 4326), s.point) = true + """) + List findAllWithinViewportWithPartner(@Param("wkt") String wkt); + List findByNameContainingIgnoreCaseOrderByIdDesc(String name); + + @Query(""" + SELECT DISTINCT s + FROM Store s + LEFT JOIN FETCH s.partner p + LEFT JOIN FETCH p.member + WHERE LOWER(s.name) LIKE LOWER(CONCAT('%', :name, '%')) + ORDER BY s.id DESC + """) + List findByNameContainingIgnoreCaseOrderByIdDescWithPartner(@Param("name") String name); Optional findByName(String name); Optional findById(Long id); Optional findByPartnerId(Long partnerId); 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 e18b4ecf..9d235c34 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,5 +1,4 @@ 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.store.dto.TodayBestResponseDTO; import com.assu.server.domain.user.dto.StudentResponseDTO; 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 1a973246..a1614c24 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 @@ -16,18 +16,29 @@ @Tag(name = "Suggestion", description = "제휴 건의 API") @RestController -@RequiredArgsConstructor // 파라미터가 있어야만 하는 생성자 -@RequestMapping("/suggestion") // suggestion 아래에서 시작 +@RequiredArgsConstructor +@RequestMapping("/suggestion") public class SuggestionController { private final SuggestionService suggestionService; - - @PostMapping @Operation( summary = "제휴 건의 API", - description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 현재 로그인한 학생(User)이 관리자에게 제휴를 건의합니다.\n" - ) + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/_-2241197c19ed81e68840d565af59b534)\n" + + "- 현재 로그인한 학생이 관리자에게 제휴를 건의합니다.\n" + + "- 성공 시 200(OK)과 Suggestion 객체 반환.\n" + + "\n**Request Body:**\n" + + " - `suggestionRequest` 객체 (JSON, required)\n" + + " - `adminId` (Long): 건의 대상 관리자 ID\n" + + " - `storeName` (String): 희망 가게 이름\n" + + " - `benefit` (String): 희망 혜택\n" + + "\n**Response:**\n" + + " - `suggestionId` (Long): 건의 ID\n" + + " - `userId` (Long): 제안인 ID\n" + + " - `adminId` (Long): 건의 대상 관리자 ID\n" + + " - `storeName` (String): 희망 가게 이름\n" + + " - `suggestionBenefit` (String): 희망 혜택\n") + @PostMapping public BaseResponse writeSuggestion( @RequestBody SuggestionRequestDTO.WriteSuggestionRequestDTO suggestionRequestDTO, @AuthenticationPrincipal PrincipalDetails pd @@ -35,22 +46,40 @@ public BaseResponse writeSugge return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.writeSuggestion(suggestionRequestDTO, pd.getId())); } - @GetMapping("/admin") @Operation( summary = "제휴 건의대상 조회 API", - description = "[v1.0 (2025-09-03)](https://www.notion.so/_-2241197c19ed81e68840d565af59b534) 현재 로그인한 학생(User)이 제휴를 건의할 수 있는 학생회(Admin)를 조회합니다.\n" - ) + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/_-2621197c19ed808c9627fcb7f58f4538)\n" + + "- 현재 로그인한 학생이 건의할 수 있는 관리자를 조회합니다.\n" + + "- 성공 시 200(OK)과 SuggestionAdmins 객체 반환.\n" + + "\n**Response:**\n" + + " - `adminId` (Long): 총학생회 ID\n" + + " - `adminName` (String): 총학생회 이름\n" + + " - `departId` (Long): 단과대학 학생회 ID\n" + + " - `departName` (String): 단과대학 학생회 이름\n" + + " - `majorId` (Long): 학부/학과 학생회 ID\n" + + " - `majorName` (String): 학부/학과 학생회 이름\n") + @GetMapping("/admin") public BaseResponse getSuggestionAdmins( @AuthenticationPrincipal PrincipalDetails pd ) { return BaseResponse.onSuccess(SuccessStatus._OK, suggestionService.getSuggestionAdmins(pd.getId())); } - @GetMapping("/list") @Operation( summary = "제휴 건의 조회 API", - description = "[v1.0 (2025-09-03)](https://www.notion.so/_-24c1197c19ed8083bf8be4b6a6a43f18) 현재 로그인한 학생회(Admin)가 받은 모든 제휴 건의를 조회합니다." - ) + description = "# [v1.3 (2026-01-04)](https://clumsy-seeder-416.notion.site/_-24c1197c19ed8083bf8be4b6a6a43f18)\n" + + "- 현재 로그인한 관리자가 받은 모든 제휴 건의를 조회합니다.\n" + + "- 제휴 건의는 작성일 기준 최신순으로 조회.\n" + + "- enrollmentStatus로 재학 상태 표시(ENROLLED, LEAVE, GRADUATED)\n" + + "- 성공 시 200(OK)과 Suggestion 객체 반환.\n" + + "\n**Response:**\n" + + " - `suggestionId` (Long): 건의 ID\n" + + " - `createdAt` (LocalDateTime): 건의 작성일\n" + + " - `storeName` (String): 희망 가게 이름\n" + + " - `content` (String): 건의 내용\n" + + " - `studentMajor` (Long): 건의자의 학부/학과\n" + + " - `enrollmentStatus` (EnrollmentStatus): 재학 상태\n") + @GetMapping("/list") public BaseResponse> getSuggestions( @AuthenticationPrincipal PrincipalDetails pd ) { 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 410462dc..f3890f8e 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 @@ -6,8 +6,8 @@ public class SuggestionRequestDTO { @Getter public static class WriteSuggestionRequestDTO{ - private Long adminId; // 건의 대상 - private String storeName; // 희망 가게 - private String benefit; // 희망 혜택 + private Long adminId; + 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 b628bd17..299cc9d8 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 @@ -16,11 +16,11 @@ public class SuggestionResponseDTO { @AllArgsConstructor @Builder public static class WriteSuggestionResponseDTO { - private Long suggestionId; // 제안 번호 - private Long userId; // 제안인 아이디 - private Long adminId; // 건의 대상 아이디 - private String storeName; // 희망 가게 이름 - private String suggestionBenefit; // 희망 혜택 + private Long suggestionId; + private Long userId; + private Long adminId; + private String storeName; + private String suggestionBenefit; } @Getter 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 146338be..75ea42d9 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,7 +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 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; @@ -24,13 +24,4 @@ List findAllSuggestionsWithStatus( @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); }