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
22 changes: 22 additions & 0 deletions data/generators/redis_matching_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,20 @@ def create_search_indexes(self):
NumericField('$.brandId', as_name='brandId'),
TagField('$.brandName', as_name='brandName'),
TagField('$.categories[*]', as_name='categories'),
# 이산형 태그
TagField('$.preferredFashionTags[*]', as_name='preferredFashionTags'),
TagField('$.preferredBeautyTags[*]', as_name='preferredBeautyTags'),
TagField('$.preferredContentTags[*]', as_name='preferredContentTags'),
# 연속형 태그
NumericField('$.minCreatorHeight', as_name='minCreatorHeight'),
NumericField('$.maxCreatorHeight', as_name='maxCreatorHeight'),
TagField('$.preferredBodyTypeTags[*]', as_name='preferredBodyTypeTags'),
TagField('$.preferredTopSizeTags[*]', as_name='preferredTopSizeTags'),
TagField('$.preferredBottomSizeTags[*]', as_name='preferredBottomSizeTags'),
TagField('$.preferredContentsAverageViewsTags[*]', as_name='preferredContentsAverageViewsTags'),
TagField('$.preferredContentsAgeTags[*]', as_name='preferredContentsAgeTags'),
TagField('$.preferredContentsGenderTags[*]', as_name='preferredContentsGenderTags'),
TagField('$.preferredContentsLengthTags[*]', as_name='preferredContentsLengthTags'),
)
},
{
Expand All @@ -382,9 +393,20 @@ def create_search_indexes(self):
'schema': (
NumericField('$.campaignId', as_name='campaignId'),
TagField('$.categories[*]', as_name='categories'),
# 이산형 태그
TagField('$.preferredFashionTags[*]', as_name='preferredFashionTags'),
TagField('$.preferredBeautyTags[*]', as_name='preferredBeautyTags'),
TagField('$.preferredContentTags[*]', as_name='preferredContentTags'),
# 연속형 태그
NumericField('$.minCreatorHeight', as_name='minCreatorHeight'),
NumericField('$.maxCreatorHeight', as_name='maxCreatorHeight'),
TagField('$.preferredBodyTypeTags[*]', as_name='preferredBodyTypeTags'),
TagField('$.preferredTopSizeTags[*]', as_name='preferredTopSizeTags'),
TagField('$.preferredBottomSizeTags[*]', as_name='preferredBottomSizeTags'),
TagField('$.preferredContentsAverageViewsTags[*]', as_name='preferredContentsAverageViewsTags'),
TagField('$.preferredContentsAgeTags[*]', as_name='preferredContentsAgeTags'),
TagField('$.preferredContentsGenderTags[*]', as_name='preferredContentsGenderTags'),
TagField('$.preferredContentsLengthTags[*]', as_name='preferredContentsLengthTags'),
)
},
{
Expand Down
34 changes: 17 additions & 17 deletions data/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ def _create_master_account(self):
def generate_all(self, user_count=50, brand_count=20, campaign_count=30,
room_count=20, messages_per_room=10, applies_per_campaign=3,
reset=True):
if reset:
self._clear_existing_data()
self._create_master_account()
# if reset:
# self._clear_existing_data()
# self._create_master_account()

print("[시작] 더미 데이터 생성 시작...\n")
print(f"생성할 데이터:")
Expand All @@ -117,26 +117,26 @@ def generate_all(self, user_count=50, brand_count=20, campaign_count=30,

try:
# 시드 데이터 먼저 생성 (태그, 약관 등)
seed_gen = SeedGenerator(self.connection)
seed_gen.generate_all()
# seed_gen = SeedGenerator(self.connection)
# seed_gen.generate_all()

user_gen = UserGenerator(self.connection)
user_gen.generate_all(user_count)
# user_gen = UserGenerator(self.connection)
# user_gen.generate_all(user_count)

brand_gen = BrandGenerator(self.connection)
brand_gen.generate_all(brand_count)
# brand_gen = BrandGenerator(self.connection)
# brand_gen.generate_all(brand_count)

campaign_gen = CampaignGenerator(self.connection)
campaign_gen.generate_all(campaign_count)
# campaign_gen = CampaignGenerator(self.connection)
# campaign_gen.generate_all(campaign_count)

# 캠페인 생성 후 협찬 데이터 생성
brand_gen.generate_sponsors()
# # 캠페인 생성 후 협찬 데이터 생성
# brand_gen.generate_sponsors()

tag_gen = TagGenerator(self.connection)
tag_gen.generate_all()
# tag_gen = TagGenerator(self.connection)
# tag_gen.generate_all()

business_gen = BusinessGenerator(self.connection)
business_gen.generate_all(applies_per_campaign)
# business_gen = BusinessGenerator(self.connection)
# business_gen.generate_all(applies_per_campaign)

#chat_gen = ChatGenerator(self.connection)
#chat_gen.generate_all(room_count, messages_per_room)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ public class MatchServiceImpl implements MatchService {
private static final Logger LOG = LoggerFactory.getLogger(MatchServiceImpl.class);
private static final int TOP_MATCH_COUNT = 10;

private static final Map<String, List<String>> USER_TYPE_TAG_MAP = Map.of(
"유연한 연출가", List.of("기획중심", "구조탄탄", "디테일중심"),
"섬세한 설계자", List.of("연출유연", "트렌드 적용", "브랜드 이해도"),
"재치있는 스토리텔러", List.of("스토리감각", "소통형콘텐츠", "일상공감"),
"도전적인 실험가", List.of("콘셉트실험", "포맷도전", "새로움 추구")
);

private final RedisDocumentHelper redisDocumentHelper;

private final BrandRepository brandRepository;
Expand All @@ -87,13 +94,9 @@ public class MatchServiceImpl implements MatchService {
private final UserRepository userRepository;
private final UserMatchingDetailRepository userMatchingDetailRepository;

/**
* 매칭 검사는 다음을 하나의 트랜잭션으로 처리한다.
* - 기존 UserMatchingDetail 폐기
* - 새 UserMatchingDetail 생성 (creatorType + snsUrl만)
* - TagUser 전량 교체 저장 (나머지 정보 전부 user_tag로)
* - 브랜드/캠페인 매칭 히스토리 갱신
*/
// ******* //
// 매칭 검사 //
// ******* //
Comment on lines +97 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

match 메서드의 트랜잭션 범위와 동작을 설명하던 기존 Javadoc 주석이 제거되었습니다. 해당 주석은 메서드의 역할을 이해하는 데 매우 유용했습니다. 코드의 명확성과 유지보수성을 위해 이전 Javadoc을 복원하거나, 최소한 메서드의 핵심 동작을 설명하는 주석을 유지하는 것을 제안합니다.

@Override
@Transactional
public MatchResponseDto match(Long userId, MatchRequestDto requestDto) {
Expand Down Expand Up @@ -125,15 +128,18 @@ public MatchResponseDto match(Long userId, MatchRequestDto requestDto) {
List<CampaignMatchResult> campaignResults = findMatchingCampaignResults(userDoc, userId);
saveMatchHistory(userId, brandResults, campaignResults);

String username = userRepository.findById(userId)
.map(User::getName)
String userNickname = userRepository.findById(userId)
.map(User::getNickname)
.orElse("사용자");

List<String> userTypeTag = USER_TYPE_TAG_MAP.getOrDefault(userType, List.of());

return MatchResponseDto.builder()
.username(username)
.username(userNickname)
.userType(userType)
.userTypeImage("https://ui-avatars.com/api/?name=" + userType + "&background=6366f1&color=fff&size=200")
.typeTag(typeTag)
.userTypeTag(userTypeTag)
.highMatchingBrandList(brandListDto)
.build();
}
Expand Down Expand Up @@ -353,14 +359,63 @@ private String determineUserType(UserTagDocument userDoc) {
int fashionCount = safeSize(userDoc.getFashionTags());
int beautyCount = safeSize(userDoc.getBeautyTags());
int contentCount = safeSize(userDoc.getContentTags());
int total = fashionCount + beautyCount + contentCount;

if (fashionCount >= beautyCount && fashionCount >= contentCount) {
if (total == 0) {
return "유연한 연출가";
} else if (beautyCount >= contentCount) {
return "트렌드 리더";
} else {
return "콘텐츠 크리에이터";
}

// 체형/사이즈 디테일 입력 여부
boolean hasBodyDetail = userDoc.getHeightTag() != null
|| userDoc.getBodyTypeTag() != null
|| userDoc.getTopSizeTag() != null
|| userDoc.getBottomSizeTag() != null;

// SNS 시청자 정보 입력 여부
boolean hasSnsAudience = safeSize(userDoc.getContentsAgeTags()) > 0
|| safeSize(userDoc.getContentsGenderTags()) > 0
|| safeSize(userDoc.getContentsLengthTags()) > 0
|| safeSize(userDoc.getAverageContentsViewsTags()) > 0;

// 각 카테고리가 전체에서 차지하는 비율
double fashionRatio = (double) fashionCount / total;
double beautyRatio = (double) beautyCount / total;
double contentRatio = (double) contentCount / total;

// 3개 카테고리 중 비어있지 않은 카테고리 수
int filledCategories = (fashionCount > 0 ? 1 : 0)
+ (beautyCount > 0 ? 1 : 0)
+ (contentCount > 0 ? 1 : 0);

// 도전적인 실험가: 3개 카테고리 모두 보유 + 특정 카테고리 쏠림 없음 (최대 비율 50% 이하)
double maxRatio = Math.max(fashionRatio, Math.max(beautyRatio, contentRatio));
if (filledCategories == 3 && maxRatio <= 0.5) {
return "도전적인 실험가";
}

// 재치있는 스토리텔러: 콘텐츠 태그가 가장 많거나, SNS 시청자 정보가 있으면서 콘텐츠 비중이 높은 경우
if (contentRatio >= fashionRatio && contentRatio >= beautyRatio) {
if (hasSnsAudience || contentCount >= beautyCount + fashionCount) {
return "재치있는 스토리텔러";
}
}

// 유연한 연출가: 패션 태그가 가장 많고 체형 디테일까지 입력한 경우
if (fashionRatio >= beautyRatio && fashionRatio >= contentRatio && hasBodyDetail) {
return "유연한 연출가";
}

// 섬세한 설계자: 뷰티 비중이 높거나 패션+뷰티 균형
if (beautyRatio >= fashionRatio && beautyRatio >= contentRatio) {
return "섬세한 설계자";
}

// 패션이 높지만 체형 디테일 없음 → 섬세한 설계자 (트렌드/브랜드 중심)
if (fashionRatio >= contentRatio && !hasBodyDetail) {
return "섬세한 설계자";
}

return "유연한 연출가";
}

private List<String> determineTypeTags(UserTagDocument userDoc) {
Expand All @@ -385,10 +440,10 @@ private List<String> determineTypeTags(UserTagDocument userDoc) {
// Redis에서 매칭 요청 //
// **************** //
private List<BrandMatchResult> findMatchingBrandResults(UserTagDocument userDoc, Long userId) {
List<BrandTagDocument> allBrandDocs = redisDocumentHelper.findAllBrandTagDocuments();
List<BrandTagDocument> candidateBrandDocs = redisDocumentHelper.findCandidateBrands(userDoc);

if (allBrandDocs.isEmpty()) {
LOG.info("No brand tag documents found in Redis");
if (candidateBrandDocs.isEmpty()) {
LOG.info("No candidate brand documents found via FT.SEARCH");
return List.of();
}

Expand All @@ -398,7 +453,7 @@ private List<BrandMatchResult> findMatchingBrandResults(UserTagDocument userDoc,

Set<Long> recruitingBrandIds = getRecruitingBrandIds();

return allBrandDocs.stream()
return candidateBrandDocs.stream()
.map(brandDoc -> new BrandMatchResult(
brandDoc,
MatchScoreCalculator.calculateBrandMatchScore(userDoc, brandDoc),
Expand All @@ -411,23 +466,23 @@ private List<BrandMatchResult> findMatchingBrandResults(UserTagDocument userDoc,
}

private List<CampaignMatchResult> findMatchingCampaignResults(UserTagDocument userDoc, Long userId) {
List<CampaignTagDocument> allCampaignDocs = redisDocumentHelper.findAllCampaignTagDocuments();
List<CampaignTagDocument> candidateCampaignDocs = redisDocumentHelper.findCandidateCampaigns(userDoc);

if (allCampaignDocs.isEmpty()) {
LOG.info("No campaign tag documents found in Redis");
if (candidateCampaignDocs.isEmpty()) {
LOG.info("No candidate campaign documents found via FT.SEARCH");
return List.of();
}

Set<Long> likedCampaignIds = campaignLikeRepository.findByUserId(userId).stream()
.map(like -> like.getCampaign().getId())
.collect(Collectors.toSet());

List<Long> campaignIds = allCampaignDocs.stream()
List<Long> campaignIds = candidateCampaignDocs.stream()
.map(CampaignTagDocument::getCampaignId)
.toList();
Map<Long, Long> applyCountMap = getApplyCountMapForCampaignIds(campaignIds);

return allCampaignDocs.stream()
return candidateCampaignDocs.stream()
.filter(campaignDoc -> campaignDoc.getRecruitEndDate() == null
|| campaignDoc.getRecruitEndDate().isAfter(LocalDateTime.now()))
.map(campaignDoc -> new CampaignMatchResult(
Expand Down
Loading
Loading