Skip to content

Commit 19e23dd

Browse files
authored
[COZY-669] feat : 일치율 계산 알고리즘 요구사항 변경 (#378)
* [COZY-669] feat: 일치율 계산기 인터페이스 * [COZY-669] feat: 정책에 따른 규칙별 감점계산 인터페이스 * [COZY-669] feat: 소음 관련 감점 계산기 구현 * [COZY-669] feat: 청결 관련 감점 계산기 구 * [COZY-669] feat: 생활패턴 관련 감점 계산기 구 * [COZY-669] feat: 성격 관련 감점 계산기 구 * [COZY-669] feat: 생활 환경 관련 감점 계산기 구 * [COZY-669] feat: 그룹을 정의하기 위한 enum클래스 * [COZY-669] feat: MemberStat Key를 위한 enum 클래스 * [COZY-669] add: 일치율 계산을 위한 헬퍼 메소드 추가 * [COZY-669] feat: MemberMatchRateCalculatorV2 구현 * [COZY-669] feat: MemberMatchRateCalculatorV2 구현 * [COZY-669] fix: MemberMatchRateCalculatorV2 적용 * [COZY-669] rename: 기존 계산기 삭제 * [COZY-669] chore: test 빌드를 위한 기존 테스트 코드 수정 * [COZY-669] test: 구현체 별 테스트 코드 추가
1 parent 164f49a commit 19e23dd

25 files changed

+1081
-160
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
4+
5+
public interface MatchRateCalculator {
6+
int calculateMatchRate(Lifestyle lifestyle1, Lifestyle lifestyle2);
7+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.calculator.penalty.Group;
4+
import com.cozymate.cozymate_server.domain.memberstat.calculator.penalty.PenaltyCalculator;
5+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
6+
import java.util.EnumMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.stream.Collectors;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.stereotype.Component;
12+
13+
@Component
14+
@Slf4j
15+
public class MemberMatchRateCalculatorV2 implements MatchRateCalculator {
16+
17+
private final Map<Group, PenaltyCalculator> chain;
18+
19+
public MemberMatchRateCalculatorV2(List<PenaltyCalculator> calculators) {
20+
this.chain = calculators.stream()
21+
.collect(Collectors.toMap(
22+
PenaltyCalculator::getGroup,
23+
c -> c,
24+
(a, b) -> a,
25+
() -> new EnumMap<>(Group.class)
26+
));
27+
}
28+
29+
@Override
30+
public int calculateMatchRate(Lifestyle a, Lifestyle b) {
31+
double penalty = 0.0;
32+
for (PenaltyCalculator calculator : chain.values()) {
33+
penalty += calculator.calculatePenalty(a, b);
34+
// log.info("{} 부분 계산 완료", calculator.group().name());
35+
}
36+
double score = 100.0 - penalty;
37+
return (int) Math.round(Math.max(0.0, Math.min(100.0, score)));
38+
}
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator.penalty;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
4+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.enums.StatKey;
5+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.util.QuestionAnswerMapper;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class CharacterCalculator implements PenaltyCalculator {
10+
11+
private static final double W_ITEM_SHARING = 4.0;
12+
private static final double W_INTIMACY = 4.0;
13+
14+
private static final int GAP_ITEM_SHARING = QuestionAnswerMapper.gapOf(StatKey.SHARING_STATUS);
15+
private static final int GAP_INTIMACY = QuestionAnswerMapper.gapOf(StatKey.INTIMACY);
16+
17+
@Override
18+
public Group getGroup() {
19+
return Group.D_CHARACTER;
20+
}
21+
22+
@Override
23+
public double calculatePenalty(Lifestyle a, Lifestyle b) {
24+
double p = 0.0;
25+
p += scoreDiff(a.getItemSharing(), b.getItemSharing(), GAP_ITEM_SHARING, W_ITEM_SHARING);
26+
p += scoreDiff(a.getIntimacy(), b.getIntimacy(), GAP_INTIMACY, W_INTIMACY);
27+
return p;
28+
}
29+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator.penalty;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
4+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.enums.StatKey;
5+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.util.QuestionAnswerMapper;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class CleannessCalculator implements PenaltyCalculator {
10+
11+
private static final double WEIGHT_CLEAN_SENS = 10.0;
12+
private static final double WEIGHT_EAT = 5.0;
13+
private static final double WEIGHT_CLEAN_FREQ = 5.0;
14+
private static final double WEIGHT_SMOKING = 2.0;
15+
16+
private static final int GAP_CLEAN_SENS = QuestionAnswerMapper.gapOf(StatKey.CLEANNESS_SENSITIVITY);
17+
private static final int GAP_EAT = QuestionAnswerMapper.gapOf(StatKey.EATING_STATUS);
18+
private static final int GAP_CLEAN_FREQ = QuestionAnswerMapper.gapOf(StatKey.CLEANING_FREQUENCY);
19+
20+
@Override
21+
public Group getGroup() {
22+
return Group.B_CLEANNESS;
23+
}
24+
@Override
25+
public double calculatePenalty(Lifestyle a, Lifestyle b) {
26+
double p = 0.0;
27+
p += scoreDiff(a.getCleannessSensitivity(), b.getCleannessSensitivity(),
28+
GAP_CLEAN_SENS, WEIGHT_CLEAN_SENS);
29+
30+
p += scoreDiff(a.getEatingFrequency(), b.getEatingFrequency(),
31+
GAP_EAT, WEIGHT_EAT);
32+
33+
p += scoreDiff(a.getCleaningFrequency(), b.getCleaningFrequency(),
34+
GAP_CLEAN_FREQ, WEIGHT_CLEAN_FREQ);
35+
36+
p += smokingPenaltyB(a.getSmokingStatus(), b.getSmokingStatus(),
37+
WEIGHT_SMOKING);
38+
39+
return p;
40+
}
41+
42+
/**
43+
* 흡연 규칙 흡&흡 = 0, 비&비 = 0 - 흡&비: 연초=2, 전자담배=1
44+
*/
45+
private double smokingPenaltyB(int x, int y, double weight) {
46+
boolean isSmokeX = x != 0, isSmokeY = y != 0;
47+
if ((isSmokeX && isSmokeY) || (!isSmokeX && !isSmokeY)) {
48+
return 0.0;
49+
}
50+
51+
int smoker = isSmokeX ? x : y;
52+
return (smoker == 1) ? weight : weight / 2;
53+
}
54+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator.penalty;
2+
3+
public enum Group {
4+
A_NOISE, B_CLEANNESS, C_PATTERN, D_CHARACTER, E_ENV
5+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator.penalty;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
4+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.enums.StatKey;
5+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.util.QuestionAnswerMapper;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class LifePatternCalculator implements PenaltyCalculator {
10+
11+
private static final double WEIGHT_WAKEUP = 10.0;
12+
private static final double WEIGHT_TURN_OFF = 5.0;
13+
private static final double WEIGHT_SLEEPING = 5.0;
14+
private static final double WEIGHT_LIFE_PATTERN = 3.0;
15+
private static final double WEIGHT_DRINKING = 2.0;
16+
private static final int GAP_DRINKING = QuestionAnswerMapper.gapOf(StatKey.DRINKING_FREQUENCY);
17+
@Override
18+
public Group getGroup() {
19+
return Group.C_PATTERN;
20+
}
21+
@Override
22+
public double calculatePenalty(Lifestyle a, Lifestyle b) {
23+
double p = 0.0;
24+
p += scoreHour(a.getWakeUpTime(), b.getWakeUpTime(), WEIGHT_WAKEUP);
25+
p += scoreHour(a.getTurnOffTime(), b.getTurnOffTime(), WEIGHT_TURN_OFF);
26+
p += scoreHour(a.getSleepingTime(), b.getSleepingTime(), WEIGHT_SLEEPING);
27+
p += scoreAnyDiff(a.getLifePattern(), b.getLifePattern(), WEIGHT_LIFE_PATTERN);
28+
p += scoreDiff(a.getDrinkingFrequency(), b.getDrinkingFrequency(),
29+
GAP_DRINKING, WEIGHT_DRINKING);
30+
return p;
31+
}
32+
33+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator.penalty;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
4+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.enums.StatKey;
5+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.util.QuestionAnswerMapper;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class LivingEnvironmentCalculator implements PenaltyCalculator {
10+
11+
private static final double WEIGHT_COOL = 3.0;
12+
private static final double WEIGHT_SMOKING = 7.0;
13+
private static final double WEIGHT_HEAT = 3.0;
14+
15+
private static final int GAP_COOL = QuestionAnswerMapper.gapOf(StatKey.COOLING_INTENSITY);
16+
private static final int GAP_HEAT = QuestionAnswerMapper.gapOf(StatKey.HEATING_INTENSITY);
17+
18+
private static final double RATIO_VAPE = 2.0 / 7.0;
19+
private static final double RATIO_IQOS = 4.0 / 7.0;
20+
private static final double RATIO_CIGARETTE = 1.0;
21+
private static final double RATIO_SMOKER_MISMATCH = 2.0 / 7.0;
22+
23+
@Override
24+
public Group getGroup() {
25+
return Group.E_ENV;
26+
}
27+
@Override
28+
public double calculatePenalty(Lifestyle a, Lifestyle b) {
29+
double p = 0.0;
30+
31+
p += smokingPenaltyE(a.getSmokingStatus(), b.getSmokingStatus());
32+
33+
p += scoreDiff(a.getCoolingIntensity(), b.getCoolingIntensity(), GAP_COOL, WEIGHT_COOL);
34+
p += scoreDiff(a.getHeatingIntensity(), b.getHeatingIntensity(), GAP_HEAT, WEIGHT_HEAT);
35+
36+
return p;
37+
}
38+
39+
/*
40+
* 흡연(E) 규칙 (가중치 스케일링 버전)
41+
* - 값: 0=비, 1=연초, 2=궐련형 전자담배(HNB), 3=액상형 전자담배
42+
* - 비 & 비: 0점
43+
* - 흡 & 흡: 타입이 다르면 2점(= weight * 2/7), 같으면 0점
44+
* - 비 & 흡: 액=2/7, 궐=4/7, 연=1.0 비율로 weight에 비례해 감점
45+
*/
46+
private double smokingPenaltyE(int x, int y) {
47+
boolean h1 = x != 0, h2 = y != 0;
48+
49+
// 비 & 비
50+
if (!h1 && !h2) {
51+
return 0.0;
52+
}
53+
54+
// 흡 & 흡
55+
if (h1 && h2) {
56+
return scoreDiff(x, y, 2, WEIGHT_SMOKING * RATIO_SMOKER_MISMATCH);
57+
}
58+
59+
// 비 & 흡
60+
int smoker = h1 ? x : y;
61+
return switch (smoker) {
62+
case 3 -> WEIGHT_SMOKING * RATIO_VAPE;
63+
case 2 -> WEIGHT_SMOKING * RATIO_IQOS;
64+
case 1 -> WEIGHT_SMOKING * RATIO_CIGARETTE;
65+
default -> 0.0;
66+
};
67+
}
68+
69+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator.penalty;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
4+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.enums.StatKey;
5+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.util.QuestionAnswerMapper;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class NoiseCalculator implements PenaltyCalculator {
10+
11+
private static final double WEIGHT_NOISE_SENS = 10.0;
12+
private static final double WEIGHT_GAME = 5.0;
13+
private static final double WEIGHT_STUDY = 5.0;
14+
private static final double WEIGHT_CALL = 5.0;
15+
private static final double WEIGHT_SLEEPING_HABIT = 5.0;
16+
17+
private static final int SLEEPING_NONE_MASK = 1;
18+
private static final int SLEEPING_COUNT_CAP = 4;
19+
20+
private static final int GAP_NOISE_SENS = QuestionAnswerMapper.gapOf(StatKey.NOISE_SENSITIVITY);
21+
private static final int GAP_GAME = QuestionAnswerMapper.gapOf(StatKey.GAMING_STATUS);
22+
private static final int GAP_STUDY = QuestionAnswerMapper.gapOf(StatKey.STUDYING_STATUS);
23+
private static final int GAP_CALL = QuestionAnswerMapper.gapOf(StatKey.CALLING_STATUS);
24+
25+
@Override
26+
public Group getGroup() {
27+
return Group.A_NOISE;
28+
}
29+
@Override
30+
public double calculatePenalty(Lifestyle a, Lifestyle b) {
31+
double p = 0.0;
32+
p += scoreDiff(a.getNoiseSensitivity(), b.getNoiseSensitivity(),
33+
GAP_NOISE_SENS, WEIGHT_NOISE_SENS);
34+
p += scoreDiff(a.getPlayingGameFrequency(), b.getPlayingGameFrequency(),
35+
GAP_GAME, WEIGHT_GAME);
36+
p += scoreDiff(a.getStudyingFrequency(), b.getStudyingFrequency(),
37+
GAP_STUDY, WEIGHT_STUDY);
38+
p += scoreDiff(a.getPhoneCallingFrequency(), b.getPhoneCallingFrequency(),
39+
GAP_CALL, WEIGHT_CALL);
40+
p += scoreSleepingHabitPenalty(a.getSleepingHabit(), b.getSleepingHabit()
41+
);
42+
return p;
43+
}
44+
45+
private double scoreSleepingHabitPenalty(int x, int y) {
46+
double w = Math.max(1.0, WEIGHT_SLEEPING_HABIT);
47+
48+
if (isOnlyNone(x) && isOnlyNone(y)) {
49+
return 0.0; // 둘 다 없음
50+
}
51+
52+
boolean hasX = !isOnlyNone(x) && countHabitsExcludingNone(x) > 0;
53+
boolean hasY = !isOnlyNone(y) && countHabitsExcludingNone(y) > 0;
54+
55+
if (hasX && hasY) {
56+
return 1.0;
57+
}
58+
59+
int n = countHabitsExcludingNone(hasX ? x : y);
60+
int capped = Math.min(n, SLEEPING_COUNT_CAP);
61+
return 1.0 + (capped / (double) SLEEPING_COUNT_CAP) * (w - 1.0);
62+
}
63+
64+
private boolean isOnlyNone(int mask) {
65+
return mask == SLEEPING_NONE_MASK;
66+
}
67+
68+
private int countHabitsExcludingNone(int mask) {
69+
return Integer.bitCount(mask & ~SLEEPING_NONE_MASK);
70+
}
71+
72+
73+
}
74+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.cozymate.cozymate_server.domain.memberstat.calculator.penalty;
2+
3+
import com.cozymate.cozymate_server.domain.memberstat.memberstat.Lifestyle;
4+
5+
public interface PenaltyCalculator {
6+
Group getGroup();
7+
double calculatePenalty(Lifestyle a, Lifestyle b);
8+
9+
/**
10+
* 값이 다르면 가중치 전부를 감점.
11+
*
12+
* @param x 첫 번째 값
13+
* @param y 두 번째 값
14+
* @param weight 감점 가중치(예: 5.0)
15+
* @return 동일하면 0.0, 다르면 weight
16+
*/
17+
default double scoreAnyDiff(int x, int y, double weight) {
18+
return x == y ? 0.0 : weight;
19+
}
20+
21+
/**
22+
* 값의 차이에 비례해 선형으로 감점
23+
* - 공식: penalty = min(diff, gap) / gap * weight
24+
* - 차이가 0이면 0.0, 차이가 gap 이상이면 weight(풀 감점).
25+
* - 예: 척도형(0~4)에서 "4칸 차이면 풀 감점" → gap=4
26+
* @param x 첫 번째 값
27+
* @param y 두 번째 값
28+
* @param gap 풀 감점이 되는 기준 차이(>0)
29+
* @param weight 감점 가중치(예: 10.0)
30+
* @return 0.0 ~ weight
31+
*/
32+
default double scoreDiff(int x, int y, int gap, double weight) {
33+
int diff = Math.abs(x - y);
34+
if (diff == 0) {
35+
return 0.0;
36+
}
37+
if (diff >= gap) {
38+
return weight;
39+
}
40+
return (diff / (double) gap) * weight;
41+
}
42+
43+
/**
44+
* 시간(0~23)의 차이로 감점한다.
45+
* - 원형 거리 d = min((h2 - h1 + 24) % 24, (h1 - h2 + 24) % 24)
46+
* - 규칙: d=0 → 0.0, d=1 → weight*0.3, d=2 → weight*0.7, d>=3 → weight
47+
* - 예: 취침/기상/소등 시간 비교 등
48+
* @param x 첫 번째 시간(0~23)
49+
* @param y 두 번째 시간(0~23)
50+
* @param weight 감점 가중치(예: 10.0)
51+
* @return 0.0 ~ weight
52+
*/
53+
default double scoreHour(int x, int y, double weight) {
54+
int d = Math.min((x - y + 24) % 24, (y - x + 24) % 24);
55+
if (d == 0) {
56+
return 0.0;
57+
}
58+
if (d == 1) {
59+
return weight * 0.3;
60+
}
61+
if (d == 2) {
62+
return weight * 0.7;
63+
}
64+
return weight;
65+
}
66+
67+
}
68+

0 commit comments

Comments
 (0)