Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8c85b47
refactor/336 입금 환전 내역 수정
yoostill Oct 14, 2025
393dd85
refactor/336 입금 환전 내역 수정
yoostill Oct 14, 2025
c869590
refactor/336 작가 수익 테스트 수정
yoostill Oct 14, 2025
ce4504d
refactor/336 대시보드 메인현황 팔로우수 추가
yoostill Oct 14, 2025
f436577
refactor/336 대시보드 메인현황 팔로우수 테스트 케이스 작성
yoostill Oct 14, 2025
4d13af0
refactor/336 대시보드 팔로우 작가 조회
yoostill Oct 14, 2025
3f4ccc9
refactor/336 대시보드 팔로우 작가 조회
yoostill Oct 14, 2025
6b20d46
pull 충돌 해결
yoostill Oct 14, 2025
b65e507
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
yoostill Oct 14, 2025
0b4d76d
refactor/336 Response 수정
yoostill Oct 14, 2025
d96f468
refactor/354리뷰 mock 제거 실제 db연동
yoostill Oct 14, 2025
bd948e0
refactor/354 레파지토리에 찜 기능 조회 추가
yoostill Oct 14, 2025
19b26b6
refactor/354 찜 테스트 기능 추가 및 테스트 오류 수정
yoostill Oct 14, 2025
89e3598
push전 충돌잡기
yoostill Oct 15, 2025
8e887ea
Merge branch 'develop' into refactir/354
yoostill Oct 15, 2025
ecf514f
refactor/354 찜 기능 수정 및 팔로우 기능 수정
yoostill Oct 15, 2025
6d94068
refactor/354 찜 기능 수정 및 팔로우 기능 수정
yoostill Oct 15, 2025
87642d2
refactor/354 찜 기능 수정 및 팔로우 기능 수정
yoostill Oct 15, 2025
e503676
merge: resolve conflicts
yoostill Oct 15, 2025
ce31604
refactor/367 이미지 사용 우선 순위 변경
yoostill Oct 15, 2025
b7ee52d
refactor/367 상품명 정렬 수정
yoostill Oct 15, 2025
9d1b5dc
refactor/367 교환 요청 조회 정렬 메모리->db 정렬로 변경
yoostill Oct 15, 2025
ccd719d
refactor/367 메인현황-상품명 정렬 추가,
yoostill Oct 15, 2025
fd4fb60
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
yoostill Oct 15, 2025
fec3a5a
refactor/367 관리자대시보드-카테고리 조회 제거
yoostill Oct 15, 2025
44851be
refactor/367 관리자대시보드-사용자관리 수수료율 정리 수정
yoostill Oct 15, 2025
c5d9b2a
refactor/367 관리자대시보드-전체 펀딩 목록보기 Resoponse간소화
yoostill Oct 15, 2025
d40e491
refactor/367 관리자대시보드-전체 펀딩 목록보기 Resoponse간소화
yoostill Oct 15, 2025
3dfda67
refactor/367 관리자대시보드-입점 승인 정렬 수정
yoostill Oct 15, 2025
eb82d72
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
yoostill Oct 15, 2025
ba9396d
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
yoostill Oct 15, 2025
1f0dbc7
refactor/367 에러코드 추가
yoostill Oct 16, 2025
80cc3d5
refactor/367 관리자 대시보드 null처리 추가
yoostill Oct 16, 2025
8f69215
refactor/
yoostill Oct 16, 2025
c0e96e0
fix/405 관리자 대시보드 메인현황에서 로딩 지연 문제 해결
yoostill Oct 16, 2025
5093066
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
yoostill Oct 19, 2025
82c07f7
fix/405 관리자 대시보드 메인현황에서 로딩 지연 문제 해결
yoostill Oct 19, 2025
6631ed0
fix/405 gpt 에러해결
yoostill Oct 19, 2025
ddbd569
fix/405 gpt 에러해결
yoostill Oct 19, 2025
f45b341
fix/405 gpt 에러해결
yoostill Oct 19, 2025
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
Expand Up @@ -87,7 +87,7 @@ public AdminOverviewResponse getOverview(AdminOverviewRequest request) {
long totalUsers = userRepository.count();
long totalProducts = productRepository.count();
long totalFundings = fundingRepository.count();
long artistCount = userRepository.findAll().stream().filter(User::isArtist).count();
long artistCount = userRepository.countArtists(); // 최적화: findAll() 대신 COUNT 쿼리 사용

// 2. 오늘 날짜 기준 통계
LocalDate today = LocalDate.now();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ public ProductListResponse findProducts(
));
}

// QueryDSL로 엔티티 조회 + THUMBNAIL join
// QueryDSL로 엔티티 조회 + 이미지 eager loading
var query = queryFactory
.select(p)
.from(p)
.leftJoin(p.images, img).on(img.fileType.eq(FileType.THUMBNAIL))
.leftJoin(p.images, img).fetchJoin()
.where(builder)
.distinct();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@

public interface ProductRepository extends JpaRepository<Product, Long>, ProductCustomRepository, JpaSpecificationExecutor<Product> {
Optional<Product> findByProductUuid(UUID productUuid);


// 태그 및 이미지 정보를 포함한 상품 조회 (추천 시스템용)
@Query("SELECT DISTINCT p FROM Product p " +
"LEFT JOIN FETCH p.productTags pt " +
"LEFT JOIN FETCH pt.tag " +
"LEFT JOIN FETCH p.images " +
"WHERE p.productUuid = :productUuid")
Optional<Product> findByProductUuidWithTags(@Param("productUuid") UUID productUuid);

// 재고 감소용 - Pessimistic Write Lock (동시성 제어)
@Lock(LockModeType.PESSIMISTIC_WRITE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,117 +59,182 @@ public class RecommendationController {
example = """
{
"resultCode": "200",
"msg": "5개 상품을 추천했습니다",
"data": {
"recommendations": [
{
"rank": 1,
"matchScore": 2.4,
"product": {
"productUuid": "550e8400-...",
"imageUrl": "https://...",
"brandName": "문구브랜드",
"name": "부드러운 4B 연필",
"price": 15000,
"discountRate": 10,
"discountPrice": 13500,
"rating": 4.5
},
"matchedTags": [
{"name": "부드러운", "yourScore": 0.9},
{"name": "실용적인", "yourScore": 0.8}
],
"reason": "'부드러운·실용적인·데일리' 선호와 일치"
}
]
}
"msg": "3개 상품을 추천했습니다",
"data": [
{
"rank": 1,
"matchScore": 2.4,
"product": {
"productUuid": "550e8400-...",
"imageUrl": "https://...",
"brandName": "문구브랜드",
"name": "부드러운 4B 연필",
"price": 15000,
"discountRate": 10,
"discountPrice": 13500,
"rating": 4.5
},
"matchedTags": [
{"name": "부드러운", "yourScore": 0.9},
{"name": "실용적인", "yourScore": 0.8}
],
"reason": "'부드러운·실용적인·데일리' 선호와 일치"
}
]
}
"""
)
)
)
}
)
public ResponseEntity<RsData<MatchResponse>> matchProducts(
public ResponseEntity<RsData<List<RecommendedItem>>> matchProducts(
@Valid @RequestBody PreferenceRequest request) {

try {
log.info("===== 추천 요청 시작 =====");
log.info("🔵 요청 전체: {}", request);
log.info("선호 태그: {}", request.preferences());
log.info("가격: {}-{}", request.minPrice(), request.maxPrice());

// 1. 상위 선호 태그 추출 (점수 0.3 이상)

// 1. 입력값 유효성 검증
if (request.preferences() == null || request.preferences().isEmpty()) {
log.warn("선호 태그가 비어있음");
return ResponseEntity.ok(RsData.of("400",
"선호 태그 정보가 필요합니다",
List.of()));
}

// 가격 범위 검증
int minPrice = Optional.ofNullable(request.minPrice()).orElse(0);
int maxPrice = Optional.ofNullable(request.maxPrice()).orElse(9_999_999);

if (minPrice < 0 || maxPrice < 0) {
log.warn("잘못된 가격 범위: min={}, max={}", minPrice, maxPrice);
return ResponseEntity.ok(RsData.of("400",
"가격은 0 이상이어야 합니다",
List.of()));
}

if (minPrice > maxPrice) {
log.warn("최소 가격이 최대 가격보다 큼: min={}, max={}", minPrice, maxPrice);
return ResponseEntity.ok(RsData.of("400",
"최소 가격은 최대 가격보다 클 수 없습니다",
List.of()));
}

// 2. 상위 선호 태그 추출 (점수 0.3 이상)
List<String> topTagNames = request.preferences().entrySet().stream()
.filter(entry -> entry.getValue() >= 0.3) // 0.5 → 0.3으로 낮춤
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.map(Map.Entry::getKey)
.toList();

log.info("선호도 0.3 이상 태그: {}", topTagNames);

if (topTagNames.isEmpty()) {
log.warn("선호도 0.3 이상인 태그가 없음");
return ResponseEntity.ok(RsData.of("200", "조건에 맞는 상품이 없습니다",
new MatchResponse(List.of())));
return ResponseEntity.ok(RsData.of("200",
"선호도가 충분히 높은 태그가 없습니다. 테스트를 다시 진행해주세요.",
List.of()));
}

// 3. 태그명 → 태그ID 변환
List<Long> tagIds;
try {
tagIds = tagDictionary.toIds(topTagNames);
log.info("변환된 태그 ID: {}", tagIds);
} catch (IllegalArgumentException e) {
log.error("태그 변환 중 오류: {}", e.getMessage());
return ResponseEntity.ok(RsData.of("400",
"잘못된 태그 정보입니다: " + e.getMessage(),
List.of()));
} catch (Exception e) {
log.error("태그 변환 중 예상치 못한 오류", e);
return ResponseEntity.ok(RsData.of("500",
"태그 처리 중 오류가 발생했습니다",
List.of()));
}

// 2. 태그명 → 태그ID 변환
List<Long> tagIds = tagDictionary.toIds(topTagNames);

log.info("변환된 태그 ID: {}", tagIds);


if (tagIds.isEmpty()) {
log.warn("유효한 태그 ID를 찾을 수 없음. 입력된 태그명: {}", topTagNames);
return ResponseEntity.ok(RsData.of("200", "조건에 맞는 상품이 없습니다 (태그를 찾을 수 없음)",
new MatchResponse(List.of())));
return ResponseEntity.ok(RsData.of("200",
"해당하는 상품 태그를 찾을 수 없습니다",
List.of()));
}
// 3. 후보 상품 조회 (충분한 후보군 확보 후 상위 3개 선택)

// 4. 후보 상품 조회 (충분한 후보군 확보 후 상위 3개 선택)
int page = 0; // 첫 페이지만
int size = 50; // 50개 후보 조회 → 점수 계산 → 상위 3개 선택
int minPrice = Optional.ofNullable(request.minPrice()).orElse(0);
int maxPrice = Optional.ofNullable(request.maxPrice()).orElse(9_999_999);


log.info("후보군 조회: {}개 (이 중 최적 3개 선택)", size);
log.info("가격 필터: {}원 ~ {}원", minPrice, maxPrice);

Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createDate"));

// 기존 findProducts 메서드 활용 (태그 필터링 포함)
ProductListResponse response = productRepository.findProducts(
null, // categoryId: 전체 카테고리
tagIds, // 선호 태그로 필터링
minPrice, // 최소 가격
maxPrice, // 최대 가격
null, // deliveryType: 전체
"newest", // 신상품순
pageable
);

log.info("findProducts 결과: {}개 상품", response.products().size());

ProductListResponse response;
try {
response = productRepository.findProducts(
null, // categoryId: 전체 카테고리
tagIds, // 선호 태그로 필터링
minPrice, // 최소 가격
maxPrice, // 최대 가격
null, // deliveryType: 전체
"newest", // 신상품순
pageable
);
log.info("findProducts 결과: {}개 상품", response.products().size());
} catch (Exception e) {
log.error("상품 조회 중 오류 발생", e);
return ResponseEntity.ok(RsData.of("500",
"상품 조회 중 오류가 발생했습니다",
List.of()));
}

if (response.products().isEmpty()) {
log.warn("조건에 맞는 상품이 없음 (findProducts 단계)");
return ResponseEntity.ok(RsData.of("200",
"조건에 맞는 상품이 없습니다. 가격 범위를 조정해보세요.",
List.of()));
}

// ProductInfo → Product 변환이 필요하므로, UUID로 다시 조회 (태그 포함)
List<Product> filtered = response.products().stream()
.map(productInfo -> {
Optional<Product> product = productRepository.findByProductUuidWithTags(productInfo.productUuid());
if (product.isEmpty()) {
log.warn("상품 UUID로 조회 실패: {}", productInfo.productUuid());
}
return product;
})
.filter(Optional::isPresent)
.map(Optional::get)
.toList();

log.info("UUID 재조회 결과: {}개 상품", filtered.size());

List<Product> filtered;
try {
filtered = response.products().stream()
.map(productInfo -> {
try {
Optional<Product> product = productRepository.findByProductUuidWithTags(productInfo.productUuid());
if (product.isEmpty()) {
log.warn("상품 UUID로 조회 실패: {}", productInfo.productUuid());
}
return product;
} catch (Exception e) {
log.error("상품 UUID 조회 중 오류: {}", productInfo.productUuid(), e);
return Optional.<Product>empty();
}
})
.filter(Optional::isPresent)
.map(Optional::get)
.toList();

log.info("UUID 재조회 결과: {}개 상품", filtered.size());
} catch (Exception e) {
log.error("상품 상세 조회 중 오류 발생", e);
return ResponseEntity.ok(RsData.of("500",
"상품 상세 정보 조회 중 오류가 발생했습니다",
List.of()));
}

if (filtered.isEmpty()) {
log.warn("조건에 맞는 상품이 없음");
return ResponseEntity.ok(RsData.of("200", "조건에 맞는 상품이 없습니다",
new MatchResponse(List.of())));
log.warn("조건에 맞는 상품이 없음 (UUID 재조회 단계)");
return ResponseEntity.ok(RsData.of("200",
"조건에 맞는 상품이 없습니다",
List.of()));
}
// 4. 매칭 스코어 계산 및 정렬

// 5. 매칭 스코어 계산 및 정렬
List<RecommendedItem> ranked;
try {
log.info("매칭 스코어 계산 시작...");
Expand All @@ -179,40 +244,67 @@ public ResponseEntity<RsData<MatchResponse>> matchProducts(
request.specPrefs()
);
log.info("✅ 매칭 스코어 계산 완료: {}개", ranked.size());
} catch (IllegalArgumentException e) {
log.error("❌ 매칭 스코어 계산 중 잘못된 입력: {}", e.getMessage());
return ResponseEntity.ok(RsData.of("400",
"추천 계산 중 오류: " + e.getMessage(),
List.of()));
} catch (NullPointerException e) {
log.error("❌ 매칭 스코어 계산 중 null 값 발견", e);
return ResponseEntity.ok(RsData.of("500",
"추천 계산 중 데이터 오류가 발생했습니다",
List.of()));
} catch (Exception e) {
log.error("❌ 매칭 스코어 계산 중 오류 발생", e);
return ResponseEntity.ok(RsData.of("500",
"추천 시스템 오류: " + e.getMessage(),
new MatchResponse(List.of())));
return ResponseEntity.ok(RsData.of("500",
"추천 계산 중 오류가 발생했습니다",
List.of()));
}

if (ranked == null || ranked.isEmpty()) {
log.warn("매칭 스코어 계산 결과가 비어있음");
return ResponseEntity.ok(RsData.of("200",
"추천할 수 있는 상품이 없습니다",
List.of()));
}
// 5. 상위 3개만 추출 (프론트 요구사항)

// 6. 상위 3개만 추출 (프론트 요구사항)
final int RECOMMENDATION_LIMIT = 3;
List<RecommendedItem> topN = ranked.stream()
.limit(RECOMMENDATION_LIMIT)
.toList();

log.info("===== 최종 추천: {}개 상품 (고정) =====", topN.size());

MatchResponse matchResponse = new MatchResponse(topN);
RsData<MatchResponse> result = RsData.of("200",
topN.size() + "개 상품을 추천했습니다",
matchResponse);


RsData<List<RecommendedItem>> result = RsData.of("200",
topN.size() + "개 상품을 추천했습니다",
topN);

log.info("🎉 응답 반환 준비 완료");
log.info("Response: resultCode={}, msg={}, data.size={}",
log.info("Response: resultCode={}, msg={}, data.size={}",
result.resultCode(), result.msg(), topN.size());

log.info("🔵 최종 응답 JSON 미리보기: {}", result);

return ResponseEntity.ok(result);


} catch (IllegalArgumentException e) {
log.error("❌ 잘못된 입력값 오류", e);
return ResponseEntity.ok(RsData.of("400",
"잘못된 요청입니다: " + e.getMessage(),
List.of()));
} catch (NullPointerException e) {
log.error("❌ Null 값 참조 오류", e);
return ResponseEntity.ok(RsData.of("500",
"데이터 처리 중 오류가 발생했습니다",
List.of()));
} catch (Exception e) {
log.error("❌❌❌ RecommendationController에서 예상치 못한 오류 발생 ❌❌❌", e);
log.error("오류 타입: {}", e.getClass().getName());
log.error("오류 메시지: {}", e.getMessage());
return ResponseEntity.ok(RsData.of("500",
"추천 시스템에 문제가 발생했습니다: " + e.getMessage(),
new MatchResponse(List.of())));

return ResponseEntity.ok(RsData.of("500",
"추천 시스템에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.",
List.of()));
}
}
}
Loading