Skip to content

Commit 8c076fe

Browse files
authored
Fix: 태그 생성 시 MySQL 대소문자 오류 해결 (#389)
* Refactor: 예약 신청 시 낙관적인 락 * Fix: 태그 이름 유니크 조건 제외, MySql Case-Insensitive 고려
1 parent 9e5dfbc commit 8c076fe

File tree

4 files changed

+64
-37
lines changed

4 files changed

+64
-37
lines changed

back/src/main/java/com/back/domain/mentoring/mentoring/entity/Mentoring.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ public void updateTags(List<Tag> tags) {
5252

5353
if (tags != null) {
5454
tags.forEach(tag ->
55-
this.mentoringTags.add(new MentoringTag(this, tag))
55+
this.mentoringTags.add(
56+
MentoringTag.builder()
57+
.mentoring(this)
58+
.tag(tag)
59+
.build()
60+
)
5661
);
5762
}
5863
}

back/src/main/java/com/back/domain/mentoring/mentoring/entity/Tag.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@Getter
1212
@NoArgsConstructor
1313
public class Tag extends BaseEntity {
14-
@Column(length = 50, nullable = false, unique = true)
14+
@Column(length = 50, nullable = false)
1515
private String name;
1616

1717
@Builder

back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringService.java

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import java.io.IOException;
2525
import java.util.ArrayList;
2626
import java.util.List;
27-
import java.util.Set;
27+
import java.util.Map;
2828
import java.util.stream.Collectors;
2929

3030
@Service
@@ -123,33 +123,51 @@ private List<Tag> getOrCreateTags(List<String> tagNames) {
123123
return new ArrayList<>();
124124
}
125125

126-
// 기존 태그 조회
127-
List<Tag> existingTags = tagRepository.findByNameIn(tagNames);
128-
129-
Set<String> existingNames = existingTags.stream()
130-
.map(Tag::getName)
131-
.collect(Collectors.toSet());
126+
// 1. 동일한 사용자의 태그 중복 제거
127+
List<String> distinctNames = tagNames.stream()
128+
.map(String::trim)
129+
.filter(name -> !name.isEmpty())
130+
.distinct()
131+
.toList();
132132

133-
// 신규 태그 생성
134-
List<Tag> newTags = createNewTags(tagNames, existingNames);
133+
// 2. 기존 태그 조회 - MySQL collation에 의해 대소문자 무시
134+
Map<String, Tag> tagMap = tagRepository.findByNameIn(distinctNames).stream()
135+
.collect(Collectors.toMap(
136+
Tag::getName,
137+
tag -> tag, (t1, t2) -> t1)
138+
);
135139

136-
// 기존 태그 + 신규 태그
137-
List<Tag> allTags = new ArrayList<>(existingTags);
138-
allTags.addAll(newTags);
140+
return buildTagList(distinctNames, tagMap);
141+
}
139142

140-
return allTags;
143+
/**
144+
* 기존 태그 + 신규 태그
145+
* - 정확히 일치하는 건 재사용 (대소문자 구분)
146+
* - 없을 경우 신규 태그 생성
147+
*/
148+
private List<Tag> buildTagList(List<String> distinctNames, Map<String, Tag> tagMap) {
149+
List<Tag> result = new ArrayList<>();
150+
for (String name : distinctNames) {
151+
Tag tag = tagMap.getOrDefault(name, createTagSafely(name));
152+
if (tag != null) {
153+
result.add(tag);
154+
}
155+
}
156+
return result;
141157
}
142158

143-
private List<Tag> createNewTags(List<String> tagNames, Set<String> existingNames) {
144-
List<Tag> newTags = tagNames.stream()
145-
.filter(name -> !existingNames.contains(name))
146-
.map(name -> Tag.builder().name(name).build())
147-
.toList();
159+
private Tag createTagSafely(String name) {
160+
// 생성 전 재조회로 중복 방지
161+
Tag existing = tagRepository.findByNameIn(List.of(name)).stream()
162+
.filter(tag -> tag.getName().equals(name))
163+
.findFirst()
164+
.orElse(null);
148165

149-
if (!newTags.isEmpty()) {
150-
tagRepository.saveAll(newTags);
166+
if (existing != null) {
167+
return existing;
151168
}
152-
return newTags;
169+
170+
return tagRepository.save(Tag.builder().name(name).build());
153171
}
154172

155173

back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,28 @@ public ReservationResponse getReservation(Member member, Long reservationId) {
7171

7272
@Transactional
7373
public ReservationResponse createReservation(Mentee mentee, ReservationRequest reqDto) {
74-
Mentoring mentoring = mentoringStorage.findMentoring(reqDto.mentoringId());
75-
MentorSlot mentorSlot = mentoringStorage.findMentorSlot(reqDto.mentorSlotId());
74+
try {
75+
Mentoring mentoring = mentoringStorage.findMentoring(reqDto.mentoringId());
76+
MentorSlot mentorSlot = mentoringStorage.findMentorSlot(reqDto.mentorSlotId());
7677

77-
DateTimeValidator.validateStartTimeNotInPast(mentorSlot.getStartDateTime());
78-
validateMentorSlotStatus(mentorSlot, mentee);
79-
validateOverlappingTimeForMentee(mentee, mentorSlot);
78+
DateTimeValidator.validateStartTimeNotInPast(mentorSlot.getStartDateTime());
79+
validateMentorSlotStatus(mentorSlot, mentee);
80+
validateOverlappingTimeForMentee(mentee, mentorSlot);
8081

81-
Reservation reservation = Reservation.builder()
82-
.mentoring(mentoring)
83-
.mentee(mentee)
84-
.mentorSlot(mentorSlot)
85-
.preQuestion(reqDto.preQuestion())
86-
.build();
87-
reservationRepository.save(reservation);
82+
Reservation reservation = Reservation.builder()
83+
.mentoring(mentoring)
84+
.mentee(mentee)
85+
.mentorSlot(mentorSlot)
86+
.preQuestion(reqDto.preQuestion())
87+
.build();
88+
reservationRepository.save(reservation);
8889

89-
mentorSlot.updateStatus(MentorSlotStatus.PENDING);
90+
mentorSlot.updateStatus(MentorSlotStatus.PENDING);
9091

91-
return ReservationResponse.from(reservation);
92+
return ReservationResponse.from(reservation);
93+
} catch (OptimisticLockException e) {
94+
throw new ServiceException(ReservationErrorCode.CONCURRENT_RESERVATION_CONFLICT);
95+
}
9296
}
9397

9498
@Transactional

0 commit comments

Comments
 (0)