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
Expand Up @@ -31,11 +31,6 @@ public record ClubInformationUpdateRequestDto(
@Size(max = 20, message = "๋™์•„๋ฆฌ ์ฃผ์ œ๋Š” ์ตœ๋Œ€ 20์ž์ž…๋‹ˆ๋‹ค.")
String clubTopic,

@Schema(description = "๋™์•„๋ฆฌ ์ด๋ชจ์ง€", example = "๐Ÿ’ป", requiredMode = REQUIRED)
@NotBlank(message = "๋™์•„๋ฆฌ ์ด๋ชจ์ง€๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
@Size(max = 10, message = "๋™์•„๋ฆฌ ์ด๋ชจ์ง€๋Š” ์ตœ๋Œ€ 10์ž์ž…๋‹ˆ๋‹ค.")
String clubEmoji,

@Schema(
description = "ํ•œ ์ค„ ์†Œ๊ฐœ (์ตœ๋Œ€ 30์ž)",
example = "์ฝ”๋”ฉ ๋™์•„๋ฆฌ์ž…๋‹ˆ๋‹ค.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ public record ClubInformationUpdateRequestedEvent(
String requestedCategory,
String currentTopic,
String requestedTopic,
String requestedEmoji,
String currentDescription,
String requestedDescription,
String currentFullIntroduction,
String requestedFullIntroduction,
String currentImageUrl,
String currentCategoryEmoji,
List<String> requestedImageUrls
) {

Expand All @@ -36,12 +35,11 @@ public static ClubInformationUpdateRequestedEvent from(ClubInformationUpdateRequ
request.getClubCategory().getDescription(),
request.getClub().getTopic(),
request.getClubTopic(),
request.getClubEmoji(),
request.getClub().getDescription(),
request.getShortDescription(),
request.getClub().getIntroduce(),
request.getFullIntroduction(),
request.getClub().getImageUrl(),
request.getClub().getCategoryEmoji(),
request.getImages().stream()
.map(image -> image.getImageUrl())
.toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ public class ClubInformationUpdateRequest extends BaseEntity {
@Column(name = "club_topic", length = 20, nullable = false)
private String clubTopic;

@NotNull
@Column(name = "club_emoji", length = 10, nullable = false)
private String clubEmoji;

@NotNull
@Column(name = "short_description", length = 30, nullable = false)
private String shortDescription;
Expand All @@ -92,7 +88,6 @@ private ClubInformationUpdateRequest(
String clubName,
ClubCategory clubCategory,
String clubTopic,
String clubEmoji,
String shortDescription,
String fullIntroduction,
UpdateRequestStatus status
Expand All @@ -103,7 +98,6 @@ private ClubInformationUpdateRequest(
this.clubName = clubName;
this.clubCategory = clubCategory;
this.clubTopic = clubTopic;
this.clubEmoji = clubEmoji;
this.shortDescription = shortDescription;
this.fullIntroduction = fullIntroduction;
this.status = status != null ? status : UpdateRequestStatus.PENDING;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ public void requestInformationUpdate(Integer clubId, ClubInformationUpdateReques
.clubName(request.clubName())
.clubCategory(request.clubCategory())
.clubTopic(request.clubTopic())
.clubEmoji(request.clubEmoji())
.shortDescription(request.shortDescription())
.fullIntroduction(request.fullIntroduction())
.status(ClubInformationUpdateRequest.UpdateRequestStatus.PENDING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import gg.agit.konect.domain.university.dto.UniversitiesResponse;

import gg.agit.konect.global.auth.annotation.PublicApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "(Normal) University: ๋Œ€ํ•™", description = "๋Œ€ํ•™ API")
@Tag(name = "(Normal) University: ๋Œ€ํ•™๊ต", description = "๋Œ€ํ•™๊ต API")
@RequestMapping("/universities")
public interface UniversityApi {

@Operation(summary = "๋Œ€ํ•™ ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค.", description = """
- ์‘๋‹ต๊ฐ’์€ ์ด๋ฆ„ ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋ฉ๋‹ˆ๋‹ค
@Operation(summary = "๋Œ€ํ•™๊ต ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค.", description = """
- ์‘๋‹ต๊ฐ’์€ ์ด๋ฆ„ ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋ฉ๋‹ˆ๋‹ค.
- query ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋Œ€ํ•™๊ต ์ด๋ฆ„, ์ดˆ์„ฑ, ์•ฝ์นญ ๊ฒ€์ƒ‰์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
""")
@GetMapping
@PublicApi
ResponseEntity<UniversitiesResponse> getUniversities();
ResponseEntity<UniversitiesResponse> getUniversities(
@RequestParam(name = "query", required = false) String query
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public class UniversityController implements UniversityApi {
private final UniversityService universityService;

@Override
public ResponseEntity<UniversitiesResponse> getUniversities() {
UniversitiesResponse response = universityService.getUniversities();
public ResponseEntity<UniversitiesResponse> getUniversities(String query) {
UniversitiesResponse response = universityService.getUniversities(query);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package gg.agit.konect.domain.university.service;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import gg.agit.konect.domain.university.model.University;

@Component
public class UniversitySearchMatcher {

private static final int HANGUL_SYLLABLE_START = 0xAC00;
private static final int HANGUL_SYLLABLE_END = 0xD7A3;
private static final int HANGUL_SYLLABLE_INTERVAL = 588;
private static final int UNIVERSITY_SUFFIX_LENGTH = "๋Œ€ํ•™๊ต".length();

private static final List<String> CHOSEONG = List.of(
"ใ„ฑ", "ใ„ฒ", "ใ„ด", "ใ„ท", "ใ„ธ", "ใ„น", "ใ…", "ใ…‚", "ใ…ƒ", "ใ……",
"ใ…†", "ใ…‡", "ใ…ˆ", "ใ…‰", "ใ…Š", "ใ…‹", "ใ…Œ", "ใ…", "ใ…Ž"
);

private static final Map<String, String> COMPATIBILITY_JAMO_CLUSTERS = Map.ofEntries(
Map.entry("ใ„ณ", "ใ„ฑใ……"),
Map.entry("ใ„ต", "ใ„ดใ…ˆ"),
Map.entry("ใ„ถ", "ใ„ดใ…Ž"),
Map.entry("ใ„บ", "ใ„นใ„ฑ"),
Map.entry("ใ„ป", "ใ„นใ…"),
Map.entry("ใ„ผ", "ใ„นใ…‚"),
Map.entry("ใ„ฝ", "ใ„นใ……"),
Map.entry("ใ„พ", "ใ„นใ…Œ"),
Map.entry("ใ„ฟ", "ใ„นใ…"),
Map.entry("ใ…€", "ใ„นใ…Ž"),
Map.entry("ใ…„", "ใ…‚ใ……")
);

private static final Map<String, List<String>> UNIVERSITY_ALIASES = Map.ofEntries(
Map.entry("๊ฐ€ํ†จ๋ฆญ๋Œ€ํ•™๊ต", List.of("๊ฐ€๋Œ€")),
Map.entry("๊ฑด๊ตญ๋Œ€ํ•™๊ต", List.of("๊ฑด๋Œ€")),
Map.entry("๊ฒฝ๋ถ๋Œ€ํ•™๊ต", List.of("๊ฒฝ๋Œ€", "๊ฒฝ๋ถ๋Œ€")),
Map.entry("๊ฒฝํฌ๋Œ€ํ•™๊ต", List.of("๊ฒฝํฌ๋Œ€")),
Map.entry("๊ณ ๋ ค๋Œ€ํ•™๊ต", List.of("๊ณ ๋Œ€")),
Map.entry("๊ด‘์ฃผ๊ณผํ•™๊ธฐ์ˆ ์›", List.of("๊ด‘์ฃผ๊ณผ๊ธฐ์›", "์ง€์ŠคํŠธ", "gist")),
Map.entry("๋‹จ๊ตญ๋Œ€ํ•™๊ต", List.of("๋‹จ๋Œ€")),
Map.entry("๋Œ€๊ตฌ๊ฒฝ๋ถ๊ณผํ•™๊ธฐ์ˆ ์›", List.of("๋Œ€๊ฒฝ๊ณผ๊ธฐ์›", "๋””์ง€์ŠคํŠธ", "dgist")),
Map.entry("๋™๊ตญ๋Œ€ํ•™๊ต", List.of("๋™๋Œ€")),
Map.entry("๋ถ€์‚ฐ๋Œ€ํ•™๊ต", List.of("๋ถ€๋Œ€", "๋ถ€์‚ฐ๋Œ€")),
Map.entry("์„œ๊ฐ•๋Œ€ํ•™๊ต", List.of("์„œ๊ฐ•๋Œ€")),
Map.entry("์„œ์šธ๊ณผํ•™๊ธฐ์ˆ ๋Œ€ํ•™๊ต", List.of("๊ณผ๊ธฐ๋Œ€", "์„œ์šธ๊ณผ๊ธฐ๋Œ€")),
Map.entry("์„œ์šธ๋Œ€ํ•™๊ต", List.of("์„ค๋Œ€", "์„œ์šธ๋Œ€")),
Map.entry("์„œ์šธ์‹œ๋ฆฝ๋Œ€ํ•™๊ต", List.of("์‹œ๋ฆฝ๋Œ€", "์„œ์šธ์‹œ๋ฆฝ๋Œ€")),
Map.entry("์„ฑ๊ท ๊ด€๋Œ€ํ•™๊ต", List.of("์„ฑ๋Œ€")),
Map.entry("์—ฐ์„ธ๋Œ€ํ•™๊ต", List.of("์—ฐ๋Œ€")),
Map.entry("์šธ์‚ฐ๊ณผํ•™๊ธฐ์ˆ ์›", List.of("์šธ์‚ฐ๊ณผ๊ธฐ์›", "์œ ๋‹ˆ์ŠคํŠธ", "unist")),
Map.entry("์œก๊ตฐ์‚ฌ๊ด€ํ•™๊ต", List.of("์œก์‚ฌ")),
Map.entry("์ดํ™”์—ฌ์ž๋Œ€ํ•™๊ต", List.of("์ด๋Œ€", "์ดํ™”์—ฌ๋Œ€")),
Map.entry("์ „๋‚จ๋Œ€ํ•™๊ต", List.of("์ „๋Œ€", "์ „๋‚จ๋Œ€")),
Map.entry("์ค‘์•™๋Œ€ํ•™๊ต", List.of("์ค‘๋Œ€")),
Map.entry("์ถฉ๋‚จ๋Œ€ํ•™๊ต", List.of("์ถฉ๋Œ€", "์ถฉ๋‚จ๋Œ€")),
Map.entry("์ถฉ๋ถ๋Œ€ํ•™๊ต", List.of("์ถฉ๋ถ๋Œ€")),
Map.entry("ํฌํ•ญ๊ณต๊ณผ๋Œ€ํ•™๊ต", List.of("ํฌ๊ณต", "ํฌ์Šคํ…", "postech")),
Map.entry("ํ•œ๊ตญ๊ณตํ•™๋Œ€ํ•™๊ต", List.of("ํ•œ๊ณต๋Œ€", "ํ•œ๊ตญ๊ณต๋Œ€")),
Map.entry("ํ•œ๊ตญ๊ณผํ•™๊ธฐ์ˆ ์›", List.of("์นด์ด์ŠคํŠธ", "kaist")),
Map.entry("ํ•œ๊ตญ๊ตํ†ต๋Œ€ํ•™๊ต", List.of("๊ตํ†ต๋Œ€", "ํ•œ๊ตญ๊ตํ†ต๋Œ€")),
Map.entry("ํ•œ๊ตญ๊ธฐ์ˆ ๊ต์œก๋Œ€ํ•™๊ต", List.of("ํ•œ๊ธฐ๋Œ€", "์ฝ”๋ฆฌ์•„ํ…", "koreatech")),
Map.entry("ํ•œ๊ตญ์™ธ๊ตญ์–ด๋Œ€ํ•™๊ต", List.of("์™ธ๋Œ€", "ํ•œ๊ตญ์™ธ๋Œ€")),
Map.entry("ํ•œ๊ตญ์ฒด์œก๋Œ€ํ•™๊ต", List.of("ํ•œ์ฒด๋Œ€")),
Map.entry("ํ•œ๊ตญํ•ญ๊ณต๋Œ€ํ•™๊ต", List.of("ํ•ญ๊ณต๋Œ€", "ํ•œ๊ตญํ•ญ๊ณต๋Œ€")),
Map.entry("ํ•œ๊ตญํ•ด์–‘๋Œ€ํ•™๊ต", List.of("ํ•ด์–‘๋Œ€", "ํ•œ๊ตญํ•ด์–‘๋Œ€")),
Map.entry("ํ•ด๊ตฐ์‚ฌ๊ด€ํ•™๊ต", List.of("ํ•ด์‚ฌ")),
Map.entry("ํ™์ต๋Œ€ํ•™๊ต", List.of("ํ™๋Œ€"))
);

public boolean matches(University university, String query) {
return matches(university.getKoreanName(), query);
}

public boolean matches(String universityName, String query) {
if (!StringUtils.hasText(query)) {
return true;
}

String normalizedQuery = normalize(query);
return getSearchTokens(universityName)
.anyMatch(token -> token.contains(normalizedQuery));
}

private Stream<String> getSearchTokens(String universityName) {
Set<String> aliases = getDefaultAliases(universityName);
aliases.addAll(UNIVERSITY_ALIASES.getOrDefault(universityName, List.of()));

List<String> normalizedAliases = aliases.stream()
.map(this::normalize)
.toList();

return Stream.concat(
normalizedAliases.stream(),
normalizedAliases.stream().map(this::getChoseong)
).distinct();
}

private Set<String> getDefaultAliases(String universityName) {
String withoutWhitespace = universityName.replaceAll("\\s", "");
String withoutCampus = withoutWhitespace.replaceAll("(์„œ์šธ|์„ธ์ข…|๊ธ€๋กœ๋ฒŒ|ERICA|WISE)์บ ํผ์Šค$", "");
Set<String> aliases = new HashSet<>();

aliases.add(withoutWhitespace);
aliases.add(withoutCampus);

if (withoutCampus.endsWith("๋Œ€ํ•™๊ต")) {
aliases.add(withoutCampus.substring(0, withoutCampus.length() - UNIVERSITY_SUFFIX_LENGTH) + "๋Œ€");
}

if (withoutCampus.startsWith("๊ตญ๋ฆฝ")) {
String withoutNational = withoutCampus.substring(2);
aliases.add(withoutNational);

if (withoutNational.endsWith("๋Œ€ํ•™๊ต")) {
aliases.add(withoutNational.substring(0, withoutNational.length() - UNIVERSITY_SUFFIX_LENGTH) + "๋Œ€");
}
}

return aliases;
}

private String normalize(String value) {
return expandCompatibilityJamoClusters(value)
.replaceAll("\\s", "")
.toLowerCase();
}

private String expandCompatibilityJamoClusters(String value) {
StringBuilder builder = new StringBuilder();

value.codePoints()
.mapToObj(Character::toString)
.forEach(character -> builder.append(COMPATIBILITY_JAMO_CLUSTERS.getOrDefault(character, character)));

return builder.toString();
}

private String getChoseong(String value) {
StringBuilder builder = new StringBuilder();

value.codePoints()
.forEach(codePoint -> builder.append(toChoseong(codePoint)));

return builder.toString();
}

private String toChoseong(int codePoint) {
if (codePoint < HANGUL_SYLLABLE_START || codePoint > HANGUL_SYLLABLE_END) {
return Character.toString(codePoint);
}

return CHOSEONG.get((codePoint - HANGUL_SYLLABLE_START) / HANGUL_SYLLABLE_INTERVAL);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
public class UniversityService {

private final UniversityRepository universityRepository;
private final UniversitySearchMatcher universitySearchMatcher;

public UniversitiesResponse getUniversities() {
public UniversitiesResponse getUniversities(String query) {
List<University> universities = universityRepository.findAllByOrderByKoreanNameAsc();
return UniversitiesResponse.from(universities);
List<University> filteredUniversities = universities.stream()
.filter(university -> universitySearchMatcher.matches(university, query))
.toList();

return UniversitiesResponse.from(filteredUniversities);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import gg.agit.konect.domain.upload.dto.ImageUploadResponse;
import gg.agit.konect.domain.upload.enums.UploadTarget;
import gg.agit.konect.global.auth.annotation.UserId;
import gg.agit.konect.global.auth.annotation.PublicApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

Expand All @@ -25,15 +25,15 @@ public interface UploadApi {
- ์‘๋‹ต์˜ fileUrl์„ ๊ธฐ์กด ๋„๋ฉ”์ธ API์˜ imageUrl๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

## ์—๋Ÿฌ
- MISSING_ACCESS_TOKEN (401): ์•ก์„ธ์Šค ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
- INVALID_REQUEST_BODY (400): ํŒŒ์ผ์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ์š”์ฒญ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ๊ฒฝ์šฐ
- INVALID_FILE_CONTENT_TYPE (400): ์ง€์›ํ•˜์ง€ ์•Š๋Š” Content-Type ์ธ ๊ฒฝ์šฐ
- PAYLOAD_TOO_LARGE (413): ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ•œ ๊ฒฝ์šฐ
- TOO_MANY_REQUESTS (429): ์—…๋กœ๋“œ ์š”์ฒญ ํšŸ์ˆ˜๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ•œ ๊ฒฝ์šฐ
- FAILED_UPLOAD_FILE (500): S3 ์—…๋กœ๋“œ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ
""")
@PostMapping(value = "/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PublicApi
ResponseEntity<ImageUploadResponse> uploadImage(
@UserId Integer userId,
@RequestPart("file") MultipartFile file,
@RequestParam(value = "target") UploadTarget target
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import gg.agit.konect.domain.upload.dto.ImageUploadResponse;
import gg.agit.konect.domain.upload.enums.UploadTarget;
import gg.agit.konect.domain.upload.service.UploadService;
import gg.agit.konect.global.auth.annotation.UserId;
import gg.agit.konect.global.ratelimit.annotation.RateLimit;
import lombok.RequiredArgsConstructor;

Expand All @@ -17,10 +16,10 @@ public class UploadController implements UploadApi {

private final UploadService uploadService;

@RateLimit(maxRequests = 20, timeWindowSeconds = 60, keyExpression = "#userId")
// ๋น„๋กœ๊ทธ์ธ ๊ณต๊ฐœ ์—…๋กœ๋“œ๋Š” ๊ณ„์ • ์‹๋ณ„์ž๊ฐ€ ์—†์–ด ํ”„๋ก์‹œ๊ฐ€ ๋ณต์›ํ•œ ํด๋ผ์ด์–ธํŠธ IP ๊ธฐ์ค€์œผ๋กœ ์‹œ๊ฐ„๋‹น ์ด๋Ÿ‰์„ ์ œํ•œํ•œ๋‹ค.
@RateLimit(maxRequests = 60, timeWindowSeconds = 3600, keyExpression = "#clientIp")
@Override
public ResponseEntity<ImageUploadResponse> uploadImage(
@UserId Integer userId,
MultipartFile file,
UploadTarget target
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public record WebsiteClubDetailResponse(
@Schema(description = "๋™์•„๋ฆฌ๋ช…", example = "BCSD Lab", requiredMode = REQUIRED)
String name,

@Schema(description = "๋™์•„๋ฆฌ ๋กœ๊ณ  ์ด๋ฏธ์ง€ URL", requiredMode = REQUIRED)
String imageUrl,
@Schema(description = "๋™์•„๋ฆฌ ๋ถ„๊ณผ ์ด๋ชจ์ง€", requiredMode = REQUIRED)
String categoryEmoji,

@Schema(description = "๋ถ„๊ณผ ์ฝ”๋“œ", example = "ACADEMIC", requiredMode = REQUIRED)
ClubCategory category,
Expand Down Expand Up @@ -71,7 +71,7 @@ public static WebsiteClubDetailResponse of(WebClub club, Long universityClubCoun
return new WebsiteClubDetailResponse(
club.getId(),
club.getName(),
club.getImageUrl(),
club.getCategoryEmoji(),
club.getClubCategory(),
club.getClubCategory().getDescription(),
club.getTopic(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ public record ClubResponse(
@Schema(description = "๋™์•„๋ฆฌ๋ช…", example = "BCSD Lab", requiredMode = REQUIRED)
String name,

@Schema(description = "๋™์•„๋ฆฌ ๋กœ๊ณ  ์ด๋ฏธ์ง€ URL", requiredMode = REQUIRED)
String imageUrl,
@Schema(description = "๋™์•„๋ฆฌ ๋ถ„๊ณผ ์ด๋ชจ์ง€", requiredMode = REQUIRED)
String categoryEmoji,

@Schema(description = "๋ถ„๊ณผ ์ฝ”๋“œ", example = "ACADEMIC", requiredMode = REQUIRED)
ClubCategory category,
Expand All @@ -118,7 +118,7 @@ public static ClubResponse from(WebClub club) {
return new ClubResponse(
club.getId(),
club.getName(),
club.getImageUrl(),
club.getCategoryEmoji(),
club.getClubCategory(),
club.getClubCategory().getDescription(),
club.getTopic(),
Expand Down
Loading
Loading