Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.cotato.cokerton_7th.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
Expand All @@ -10,7 +9,6 @@
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
@ConditionalOnExpression("!'${cloud.aws.credentials.access-key:}'.isEmpty()")
public class S3Config {

@Value("${cloud.aws.credentials.access-key}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.cotato.cokerton_7th.global.controller;

import com.cotato.cokerton_7th.global.service.S3Service;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.Map;

@Tag(name = "Image", description = "이미지 업로드 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/images")
public class ImageController {

private final S3Service s3Service;

@Operation(summary = "이미지 업로드", description = "이미지를 S3에 업로드하고 URL을 반환합니다.")
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Map<String, String>> uploadImage(
@RequestPart("file") MultipartFile file,
@RequestParam(value = "dirName", defaultValue = "images") String dirName) {

String imageUrl = s3Service.upload(file, dirName);

return ResponseEntity.ok(Map.of("imageUrl", imageUrl));
}

@Operation(summary = "이미지 삭제", description = "S3에서 이미지를 삭제합니다.")
@DeleteMapping
public ResponseEntity<Void> deleteImage(@RequestParam("imageUrl") String imageUrl) {

s3Service.delete(imageUrl);

return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
Expand All @@ -15,7 +14,6 @@

@Service
@RequiredArgsConstructor
@ConditionalOnExpression("!'${cloud.aws.credentials.access-key:}'.isEmpty()")
public class S3Service {

private final S3Client s3Client;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public ResponseEntity<DataResponse<Void>> saveInterests(
summary = "내가 선택한 관심사만 조회합니다.",
description = "로그인한 유저가 이전에 저장했던 관심사 리스트를 반환합니다.")
@GetMapping("/me")
public ResponseEntity<DataResponse<List<MyInterestResponse>>> getMyInterests(
public ResponseEntity<DataResponse<List<CategoryResponse>>> getMyInterests(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
// 세션에서 유저 ID 추출
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,32 @@ public void saveInterests(Long memberId, List<Long> subCategoryIds) {
}

@Transactional(readOnly = true)
public List<MyInterestResponse> getMySelectedInterests(Long memberId) {
// member_interest 테이블에서 해당 유저의 데이터만 조회
public List<CategoryResponse> getMySelectedInterests(Long memberId) {
// 1. 유저가 선택한 모든 관심사(MemberInterest)를 가져옵니다.
List<MemberInterest> myInterests = memberInterestRepository.findAllByMemberId(memberId);

// 2. 소분류가 속한 대분류(Category)를 기준으로 그룹화합니다.
return myInterests.stream()
.map(mi -> new MyInterestResponse(
mi.getSubCategory().getId(),
mi.getSubCategory().getSubCategoryName()
))
.collect(Collectors.groupingBy(mi -> mi.getSubCategory().getCategory()))
.entrySet().stream()
.map(entry -> {
Category category = entry.getKey();

// 3. 해당 대분류에 속한 소분류들을 SubCategoryResponse 리스트로 변환합니다.
List<CategoryResponse.SubCategoryResponse> subCategories = entry.getValue().stream()
.map(mi -> new CategoryResponse.SubCategoryResponse(
mi.getSubCategory().getId(),
mi.getSubCategory().getSubCategoryName()
))
.toList();

// 4. 최종 CategoryResponse로 반환합니다.
return new CategoryResponse(
category.getId(),
category.getCategoryName(),
subCategories
);
})
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -19,31 +22,46 @@ public class MatchingService {
private final CategoryRepository categoryRepository;

public MatchingResponse matchOppositeUser(Long myId) {
// 1. 내가 고른 카테고리 ID들 추출
List<Long> myCategoryIds = memberInterestRepository.findAllByMemberId(myId).stream()
.map(mi -> mi.getSubCategory().getCategory().getId())
.distinct()
.toList();

// 2. 전체 카테고리 중 내가 안 고른 카테고리 ID들 필터링
List<Long> otherCategoryIds = categoryRepository.findAll().stream()
.map(Category::getId)
.filter(id -> !myCategoryIds.contains(id))
.toList();

// 3. 내가 안 고른 카테고리의 소분류를 가진 '다른 유저'를 랜덤 조회
MemberInterest matchedInterest = memberInterestRepository
.findRandomByExcludeCategories(myId, otherCategoryIds)
.orElseThrow(() -> new IllegalArgumentException("현재 매칭 가능한 유저가 없습니다."));

// 4. 내 취향 중 아무거나 하나 (대표 취향) 가져오기
String myRandomInterest = memberInterestRepository.findAllByMemberId(myId).get(0)
.getSubCategory().getSubCategoryName();

return new MatchingResponse(
matchedInterest.getMember().getName(),
matchedInterest.getSubCategory().getSubCategoryName(),
myRandomInterest
);
// 1. 내가 고른 모든 관심사 데이터 가져오기 (방어 코드 포함)
List<MemberInterest> myInterests = memberInterestRepository.findAllByMemberId(myId);

if (myInterests.isEmpty()) {
throw new IllegalArgumentException("관심사를 먼저 등록해 주세요.");
}

// 2. 내가 고른 카테고리 ID들 중 '딱 1개만' 랜덤으로 추출
List<Long> allMyCategoryIds = myInterests.stream()
.map(mi -> mi.getSubCategory().getCategory().getId())
.distinct()
.collect(Collectors.toCollection(ArrayList::new)); // shuffle을 위해 가변 리스트로 변환

Collections.shuffle(allMyCategoryIds);
Long myRandomCategoryId = allMyCategoryIds.get(0);

// 3. 전체 카테고리 중 '선택된 그 1개'를 제외한 나머지 필터링
// (이제 이 '나머지' 카테고리를 가진 유저와 매칭됩니다)
List<Long> otherCategoryIds = categoryRepository.findAll().stream()
.map(Category::getId)
.filter(id -> !id.equals(myRandomCategoryId))
.toList();

// 4. 내가 안 고른 카테고리의 소분류를 가진 '다른 유저'를 랜덤 조회
MemberInterest matchedInterest = memberInterestRepository
.findRandomByExcludeCategories(myId, otherCategoryIds)
.orElseThrow(() -> new IllegalArgumentException("현재 매칭 가능한 유저가 없습니다."));

// 5. 내 취향 중 아까 랜덤으로 뽑았던 카테고리에 해당하는 소분류 이름 가져오기
// (데이터 일관성을 위해 1번에서 뽑은 카테고리의 이름을 사용합니다)
String myRandomInterestName = myInterests.stream()
.filter(mi -> mi.getSubCategory().getCategory().getId().equals(myRandomCategoryId))
.findFirst()
.map(mi -> mi.getSubCategory().getSubCategoryName())
.orElse(myInterests.get(0).getSubCategory().getSubCategoryName());

return new MatchingResponse(
matchedInterest.getMember().getName(),
matchedInterest.getSubCategory().getSubCategoryName(),
myRandomInterestName
);
}
}
8 changes: 4 additions & 4 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oau
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response

# AWS S3 (환경 변수가 설정되지 않으면 S3 기능이 비활성화됩니다)
cloud.aws.credentials.access-key=${AWS_ACCESS_KEY:}
cloud.aws.credentials.secret-key=${AWS_SECRET_KEY:}
cloud.aws.s3.bucket=${S3_BUCKET:}
# AWS S3
cloud.aws.credentials.access-key=${AWS_ACCESS_KEY}
cloud.aws.credentials.secret-key=${AWS_SECRET_KEY}
cloud.aws.s3.bucket=${S3_BUCKET}
cloud.aws.region.static=${AWS_REGION:ap-northeast-2}