diff --git a/src/main/java/com/specialwarriors/conal/github/service/GitHubService.java b/src/main/java/com/specialwarriors/conal/github/service/GitHubService.java index 8a8e9de..e32a204 100644 --- a/src/main/java/com/specialwarriors/conal/github/service/GitHubService.java +++ b/src/main/java/com/specialwarriors/conal/github/service/GitHubService.java @@ -29,86 +29,86 @@ public class GitHubService { public Mono updateRepoContribution(String owner, String repo) { return getContributors(owner, repo) - .flatMapMany(contributors -> { - String contributorKey = buildContributorsKey(owner, repo); - List logins = contributors.stream() - .map(GitHubContributor::login) - .collect(Collectors.toList()); - - return reactiveRedisTemplate.delete(contributorKey) - .thenMany(Flux.fromIterable(logins)) - .flatMap(login -> reactiveRedisTemplate.opsForList() - .rightPush(contributorKey, login)) - .then(reactiveRedisTemplate.expire(contributorKey, TTL)) - .thenMany(Flux.fromIterable(contributors)); - }) - .flatMap(contributor -> updateContributorScore(owner, repo, contributor)) - .then(); + .flatMapMany(contributors -> { + String contributorKey = buildContributorsKey(owner, repo); + List logins = contributors.stream() + .map(GitHubContributor::login) + .collect(Collectors.toList()); + + return reactiveRedisTemplate.delete(contributorKey) + .thenMany(Flux.fromIterable(logins)) + .flatMap(login -> reactiveRedisTemplate.opsForList() + .rightPush(contributorKey, login)) + .then(reactiveRedisTemplate.expire(contributorKey, TTL)) + .thenMany(Flux.fromIterable(contributors)); + }) + .flatMap(contributor -> updateContributorScore(owner, repo, contributor)) + .then(); } private Mono> getContributors(String owner, String repo) { return githubWebClient.get() - .uri("/repos/{owner}/{repo}/contributors", owner, repo) - .retrieve() - .bodyToFlux(GitHubContributor.class) - .collectList(); + .uri("/repos/{owner}/{repo}/contributors", owner, repo) + .retrieve() + .bodyToFlux(GitHubContributor.class) + .collectList(); } private Mono updateContributorScore(String owner, String repo, - GitHubContributor contributor) { + GitHubContributor contributor) { String login = contributor.login(); return Mono.zip( - getCommitCount(owner, repo, login), - getPullRequestCount(owner, repo, login), - getMergedPullRequestCount(owner, repo, login), - getIssueCount(owner, repo, login) + getCommitCount(owner, repo, login), + getPullRequestCount(owner, repo, login), + getMergedPullRequestCount(owner, repo, login), + getIssueCount(owner, repo, login) ).flatMap(tuple -> { long commit = tuple.getT1(); long pr = tuple.getT2(); long mpr = tuple.getT3(); long issue = tuple.getT4(); - long score = commit + pr + mpr + issue; + double score = commit * 0.1 + pr * 0.2 + mpr + issue * 0.2; String detailKey = buildDetailKey(owner, repo, login); return reactiveRedisTemplate.opsForHash().putAll(detailKey, Map.of( - "commit", String.valueOf(commit), - "pr", String.valueOf(pr), - "mpr", String.valueOf(mpr), - "issue", String.valueOf(issue), - "score", String.valueOf(score) - )) - .then(reactiveRedisTemplate.expire(detailKey, TTL)); + "commit", String.valueOf(commit), + "pr", String.valueOf(pr), + "mpr", String.valueOf(mpr), + "issue", String.valueOf(issue), + "score", String.valueOf(score) + )) + .then(reactiveRedisTemplate.expire(detailKey, TTL)); }).then(); } private Mono getCommitCount(String owner, String repo, String login) { return getAllCommits(owner, repo, 1) - .filter(commit -> { - Map author = (Map) commit.get("author"); + .filter(commit -> { + Map author = (Map) commit.get("author"); - return author != null && login.equalsIgnoreCase((String) author.get("login")); - }) - .count(); + return author != null && login.equalsIgnoreCase((String) author.get("login")); + }) + .count(); } private Flux getAllCommits(String owner, String repo, int page) { return githubWebClient.get() - .uri(uriBuilder -> uriBuilder - .path("/repos/{owner}/{repo}/commits") - .queryParam("per_page", PER_PAGE) - .queryParam("page", page) - .build(owner, repo)) - .retrieve() - .bodyToFlux(Map.class) - .collectList() - .flatMapMany(list -> list.size() < PER_PAGE ? Flux.fromIterable(list) - : Flux.fromIterable(list).concatWith(getAllCommits(owner, repo, page + 1))); + .uri(uriBuilder -> uriBuilder + .path("/repos/{owner}/{repo}/commits") + .queryParam("per_page", PER_PAGE) + .queryParam("page", page) + .build(owner, repo)) + .retrieve() + .bodyToFlux(Map.class) + .collectList() + .flatMapMany(list -> list.size() < PER_PAGE ? Flux.fromIterable(list) + : Flux.fromIterable(list).concatWith(getAllCommits(owner, repo, page + 1))); } private Mono getPullRequestCount(String owner, String repo, String login) { @@ -127,14 +127,14 @@ private Mono getIssueCount(String owner, String repo, String login) { } private Mono queryGithubIssueCount(String owner, String repo, String login, - String queryType) { + String queryType) { String query = String.format("repo:%s/%s+%s+author:%s", owner, repo, queryType, login); return githubWebClient.get() - .uri(uriBuilder -> uriBuilder.path("/search/issues").queryParam("q", query).build()) - .retrieve() - .bodyToMono(Map.class) - .map(map -> ((Number) map.get("total_count")).longValue()); + .uri(uriBuilder -> uriBuilder.path("/search/issues").queryParam("q", query).build()) + .retrieve() + .bodyToMono(Map.class) + .map(map -> ((Number) map.get("total_count")).longValue()); } private String buildDetailKey(String owner, String repo, String login) { @@ -154,23 +154,23 @@ public Mono> getContributorsFromRedis(String owner, String repo) { String key = "contributors:" + owner + ":" + repo; return reactiveRedisTemplate.opsForList() - .range(key, 0, -1) - .collectList(); + .range(key, 0, -1) + .collectList(); } /** * 특정 contributor에 대한 상세 점수 정보 가져오기 */ public Mono> getContributorDetailFromRedis(String owner, String repo, - String contributor) { + String contributor) { String detailKey = "detail:" + owner + ":" + repo + ":" + contributor; return reactiveRedisTemplate.opsForHash() - .entries(detailKey) - .collectMap( - e -> e.getKey().toString(), - e -> e.getValue().toString() - ); + .entries(detailKey) + .collectMap( + e -> e.getKey().toString(), + e -> e.getValue().toString() + ); } } diff --git a/src/main/java/com/specialwarriors/conal/github_repo/controller/GithubRepoController.java b/src/main/java/com/specialwarriors/conal/github_repo/controller/GithubRepoController.java index dd53e5b..37c8dc9 100644 --- a/src/main/java/com/specialwarriors/conal/github_repo/controller/GithubRepoController.java +++ b/src/main/java/com/specialwarriors/conal/github_repo/controller/GithubRepoController.java @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; @@ -39,7 +40,7 @@ public String showCreateForm(@SessionAttribute Long userId, Model model) { // 저장 (POST) @PostMapping public String createGitHubRepo(@SessionAttribute Long userId, - @ModelAttribute GithubRepoCreateRequest request) { + @ModelAttribute GithubRepoCreateRequest request) { GithubRepoCreateResponse response = githubRepoService.createGithubRepo(userId, request); gitHubService.updateRepoContribution(response.owner(), response.repo()).subscribe(); @@ -50,7 +51,7 @@ public String createGitHubRepo(@SessionAttribute Long userId, // 목록 조회 (GET) @GetMapping public String getGithubRepos(@SessionAttribute Long userId, - @RequestParam(defaultValue = "0") int page, Model model) { + @RequestParam(defaultValue = "0") int page, Model model) { GithubRepoPageResponse response = githubRepoService.getGithubRepoInfos(userId, page); model.addAttribute("repositories", response); @@ -62,7 +63,7 @@ public String getGithubRepos(@SessionAttribute Long userId, // 단일 조회 (GET) @GetMapping("/{repositoryId}") public String getRepositoryId(@SessionAttribute Long userId, - @PathVariable long repositoryId, Model model) { + @PathVariable long repositoryId, Model model) { GithubRepoGetResponse response = githubRepoService.getGithubRepoInfo(userId, repositoryId); model.addAttribute("repoInfo", response); @@ -70,9 +71,9 @@ public String getRepositoryId(@SessionAttribute Long userId, return "repo/detail"; } - @PostMapping("/{repositoryId}") + @DeleteMapping("/{repositoryId}") public String deleteRepository(@SessionAttribute Long userId, - @PathVariable long repositoryId) { + @PathVariable long repositoryId) { githubRepoService.deleteRepo(userId, repositoryId); diff --git a/src/main/java/com/specialwarriors/conal/github_repo/service/GithubRepoService.java b/src/main/java/com/specialwarriors/conal/github_repo/service/GithubRepoService.java index 88c81e6..880be04 100644 --- a/src/main/java/com/specialwarriors/conal/github_repo/service/GithubRepoService.java +++ b/src/main/java/com/specialwarriors/conal/github_repo/service/GithubRepoService.java @@ -11,13 +11,14 @@ import com.specialwarriors.conal.github_repo.dto.response.GithubRepoPageResponse; import com.specialwarriors.conal.github_repo.exception.GithubRepoException; import com.specialwarriors.conal.github_repo.repository.GithubRepoRepository; -import com.specialwarriors.conal.github_repo.util.UrlUtil; import com.specialwarriors.conal.notification.domain.NotificationAgreement; import com.specialwarriors.conal.notification.enums.NotificationType; import com.specialwarriors.conal.notification.repository.NotificationAgreementRepository; import com.specialwarriors.conal.user.domain.User; import com.specialwarriors.conal.user.service.UserQuery; +import com.specialwarriors.conal.util.UrlUtil; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; @@ -75,25 +76,40 @@ private void validateCreateRequest(GithubRepoCreateRequest request) { if (request.name().isEmpty()) { throw new GeneralException(GithubRepoException.NOT_FOUND_GITHUBREPO_NAME); } + if (!GITHUB_URL_PATTERN.matcher(request.url()).matches()) { throw new GeneralException(GithubRepoException.INVALID_GITHUBREPO_URL); } - if (request.emails().isEmpty()) { + + long validEmailCount = request.emails().stream() + .filter(Objects::nonNull) + .filter(email -> !email.trim().isEmpty()) + .count(); + + if (validEmailCount == 0) { throw new GeneralException(GithubRepoException.NOT_FOUND_GITHUBREPO_EMAIL); } - if (request.emails().size() > 5) { + if (validEmailCount > 5) { throw new GeneralException(GithubRepoException.EXCEED_GITHUBREPO_EMAIL); } + for (String email : request.emails()) { - if (!EMAIL_PATTERN.matcher(email).matches()) { - throw new GeneralException(GithubRepoException.INVALID_GITHUBREPO_EMAIL); + + if (Objects.nonNull(email)) { + String trimmed = email.trim(); + if (!trimmed.isEmpty() && !EMAIL_PATTERN.matcher(trimmed).matches()) { + throw new GeneralException(GithubRepoException.INVALID_GITHUBREPO_EMAIL); + } } + } + if (request.endDate() == null) { throw new GeneralException(GithubRepoException.INVALID_GITHUBREPO_DURATION); } } + private List createAndSaveContributors(Set emails) { List contributors = emails.stream() diff --git a/src/main/java/com/specialwarriors/conal/util/UrlUtil.java b/src/main/java/com/specialwarriors/conal/util/UrlUtil.java index b851c76..f06c023 100644 --- a/src/main/java/com/specialwarriors/conal/util/UrlUtil.java +++ b/src/main/java/com/specialwarriors/conal/util/UrlUtil.java @@ -1,4 +1,4 @@ -package com.specialwarriors.conal.github_repo.util; +package com.specialwarriors.conal.util; import com.specialwarriors.conal.common.exception.GeneralException; import com.specialwarriors.conal.github_repo.exception.GithubRepoException; diff --git a/src/main/resources/templates/main/home.html b/src/main/resources/templates/main/home.html index 1de7c61..0aa2785 100644 --- a/src/main/resources/templates/main/home.html +++ b/src/main/resources/templates/main/home.html @@ -153,6 +153,7 @@
+
diff --git a/src/main/resources/templates/repo/list.html b/src/main/resources/templates/repo/list.html deleted file mode 100644 index 133c0fc..0000000 --- a/src/main/resources/templates/repo/list.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - GitHub 저장소 목록 - - -

GitHub 저장소 목록

- -
-

-
- - - - -
- -
- -
- -
-

현재 페이지:

-
- - - \ No newline at end of file diff --git a/src/test/java/com/specialwarriors/conal/ConalApplicationTests.java b/src/test/java/com/specialwarriors/conal/ConalApplicationTests.java index 41c651a..d2763a7 100644 --- a/src/test/java/com/specialwarriors/conal/ConalApplicationTests.java +++ b/src/test/java/com/specialwarriors/conal/ConalApplicationTests.java @@ -27,4 +27,4 @@ class ConalApplicationTests { @Test void contextLoads() { } -} +} \ No newline at end of file