Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f25968c
refactor: 테스트 컨테이너 적용 (#138)
nayonsoso Jan 7, 2025
6464568
chore: 리팩터링 이슈 템플릿 추가 (#152)
nayonsoso Jan 15, 2025
dc1bdcd
feat: 추천 대학에서 로고 뿐만 아니라 background image도 포함 (#144)
Gyuhyeok99 Jan 16, 2025
428e72a
test: 대학 관련 통합테스트 코드 추가 (#148)
Gyuhyeok99 Jan 19, 2025
f7dd92d
refactor: 스프링 시큐리티 코드 리팩터링 (#154)
nayonsoso Jan 22, 2025
35bc2ac
test: 유저 관련 통합테스트 코드 추가 (#156)
Gyuhyeok99 Jan 23, 2025
d4e57c0
hotfix: 대학 추천 기능 정상화 (#159)
nayonsoso Jan 27, 2025
f26e75c
refactor: auth type 추가, 로그아웃 필터 로직 변경 (#167)
nayonsoso Jan 27, 2025
b37becd
test: 어학점수 관련 통합테스트 코드 추가 (#165)
Gyuhyeok99 Jan 28, 2025
68560a1
refactor: 게시글 관련 서비스 구조 리팩터링 (#168)
Gyuhyeok99 Jan 28, 2025
b66e610
test: 댓글 관련 통합테스트 코드 추가 (#173)
Gyuhyeok99 Jan 29, 2025
16b907f
refactor: 공통 추천 대학 로직 변경 (#175)
nayonsoso Jan 29, 2025
9654770
test: 게시글 전체 조회 관련 통합테스트 코드 추가 (#176)
Gyuhyeok99 Jan 30, 2025
89eca05
test: 지원서 관련 통합테스트 코드 추가 (#178)
Gyuhyeok99 Feb 4, 2025
2fc9d85
refactor: 요청의 인증 정보를 컨트롤러에서 원하는 형태로 받을 수 있도록 (#171)
nayonsoso Feb 5, 2025
ecc186a
refactor: TokenProvider 에서 각 토큰에 대한 로직을 캡슐화 (#183)
nayonsoso Feb 6, 2025
b9a4af8
feat: 애플 인증 구현 (#184)
nayonsoso Feb 7, 2025
25e9f40
refactor: 사용하지 않을 컬럼 제거 (#186)
Gyuhyeok99 Feb 8, 2025
2c8b200
refactor: 커뮤니티 관련 패키지 구조 개선 (#185)
Gyuhyeok99 Feb 8, 2025
0adbb4c
feat: 이메일 인증 구현 (#188)
nayonsoso Feb 8, 2025
306d8b6
refactor: 입력 형식과 관련된 검증은 Dto 내부에서 하게 한다. (#187)
Gyuhyeok99 Feb 9, 2025
75927c8
fix: 실패한 테스트 해결, 서브모듈 업데이트 (#193)
nayonsoso Feb 11, 2025
14bd0b9
feat: 관리자 role 추가 (#192)
Gyuhyeok99 Feb 12, 2025
ce1d88b
Merge release branch with ours strategy to sync with main
wibaek Feb 12, 2025
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
28 changes: 28 additions & 0 deletions .github/ISSUE_TEMPLATE/refactor_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
name: Refactor request
about: Suggest an refactor for this project
title: ''
labels: refactor
assignees: ''

---

## 어떤 부분을 리팩터링하려 하나요?

> 리팩터링하려는 부분에 대해 간결하게 설명해주세요

### AS-IS
- as-is
- as-is

### TO-BE
- to-be
- to-be

## 작업 상세 내용

- [ ] TODO
- [ ] TODO
- [ ] TODO

## 참고할만한 자료(선택)
13 changes: 10 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,24 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
implementation 'org.apache.commons:commons-lang3:3.12.0'
testImplementation 'org.mockito:mockito-core:3.3.3'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

// Lombok
compileOnly 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'com.h2database:h2:2.2.224'
testImplementation 'org.mockito:mockito-core:3.3.3'
testImplementation 'io.rest-assured:rest-assured:5.4.0'

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
// Testcontainers
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mysql'

annotationProcessor(
'com.querydsl:querydsl-apt:5.0.0:jakarta',
'jakarta.persistence:jakarta.persistence-api:3.1.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@ConfigurationPropertiesScan
@EnableScheduling
@EnableJpaAuditing
@EnableCaching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.example.solidconnection.application.dto.ApplyRequest;
import com.example.solidconnection.application.service.ApplicationQueryService;
import com.example.solidconnection.application.service.ApplicationSubmissionService;
import com.example.solidconnection.custom.resolver.AuthorizedUser;
import com.example.solidconnection.siteuser.domain.SiteUser;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -16,8 +18,6 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RequiredArgsConstructor
@RequestMapping("/application")
@RestController
Expand All @@ -29,30 +29,33 @@ public class ApplicationController {
// 지원서 제출하기 api
@PostMapping()
public ResponseEntity<ApplicationSubmissionResponse> apply(
Principal principal,
@Valid @RequestBody ApplyRequest applyRequest) {
boolean result = applicationSubmissionService.apply(principal.getName(), applyRequest);
@AuthorizedUser SiteUser siteUser,
@Valid @RequestBody ApplyRequest applyRequest
) {
boolean result = applicationSubmissionService.apply(siteUser, applyRequest);
return ResponseEntity
.status(HttpStatus.OK)
.body(new ApplicationSubmissionResponse(result));
}

@GetMapping
public ResponseEntity<ApplicationsResponse> getApplicants(
Principal principal,
@AuthorizedUser SiteUser siteUser,
@RequestParam(required = false, defaultValue = "") String region,
@RequestParam(required = false, defaultValue = "") String keyword) {
applicationQueryService.validateSiteUserCanViewApplicants(principal.getName());
ApplicationsResponse result = applicationQueryService.getApplicants(principal.getName(), region, keyword);
@RequestParam(required = false, defaultValue = "") String keyword
) {
applicationQueryService.validateSiteUserCanViewApplicants(siteUser);
ApplicationsResponse result = applicationQueryService.getApplicants(siteUser, region, keyword);
return ResponseEntity
.ok(result);
}

@GetMapping("/competitors")
public ResponseEntity<ApplicationsResponse> getApplicantsForUserCompetitors(
Principal principal) {
applicationQueryService.validateSiteUserCanViewApplicants(principal.getName());
ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(principal.getName());
@AuthorizedUser SiteUser siteUser
) {
applicationQueryService.validateSiteUserCanViewApplicants(siteUser);
ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(siteUser);
return ResponseEntity
.ok(result);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.example.solidconnection.application.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;

public record ApplyRequest(

@NotNull(message = "gpa score id를 입력해주세요.")
Long gpaScoreId,

@NotNull(message = "language test score id를 입력해주세요.")
Long languageTestScoreId,

@Valid
UniversityChoiceRequest universityChoiceRequest
) {
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.example.solidconnection.application.dto;

import jakarta.validation.constraints.NotNull;
import com.example.solidconnection.custom.validation.annotation.ValidUniversityChoice;

@ValidUniversityChoice
public record UniversityChoiceRequest(
@NotNull(message = "1지망 대학교를 입력해주세요.")
Long firstChoiceUniversityId,

Long secondChoiceUniversityId,
Long thirdChoiceUniversityId) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.example.solidconnection.cache.annotation.ThunderingHerdCaching;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.type.VerifyStatus;
import com.example.solidconnection.university.domain.University;
import com.example.solidconnection.university.domain.UniversityInfoForApply;
Expand All @@ -34,7 +33,6 @@ public class ApplicationQueryService {

private final ApplicationRepository applicationRepository;
private final UniversityInfoForApplyRepository universityInfoForApplyRepository;
private final SiteUserRepository siteUserRepository;
private final UniversityFilterRepositoryImpl universityFilterRepository;

@Value("${university.term}")
Expand All @@ -49,9 +47,7 @@ public class ApplicationQueryService {
* */
@Transactional(readOnly = true)
@ThunderingHerdCaching(key = "application:query:{1}:{2}", cacheManager = "customCacheManager", ttlSec = 86400)
public ApplicationsResponse getApplicants(String email, String regionCode, String keyword) {
SiteUser siteUser = siteUserRepository.getByEmail(email);

public ApplicationsResponse getApplicants(SiteUser siteUser, String regionCode, String keyword) {
// 국가와 키워드와 지역을 통해 대학을 필터링한다.
List<University> universities
= universityFilterRepository.findByRegionCodeAndKeywords(regionCode, List.of(keyword));
Expand All @@ -64,9 +60,7 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin
}

@Transactional(readOnly = true)
public ApplicationsResponse getApplicantsByUserApplications(String email) {
SiteUser siteUser = siteUserRepository.getByEmail(email);

public ApplicationsResponse getApplicantsByUserApplications(SiteUser siteUser) {
Application userLatestApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term);
List<University> userAppliedUniversities = Arrays.asList(
Optional.ofNullable(userLatestApplication.getFirstChoiceUniversity())
Expand All @@ -91,8 +85,7 @@ public ApplicationsResponse getApplicantsByUserApplications(String email) {
// 학기별로 상태가 관리된다.
// 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다.
@Transactional(readOnly = true)
public void validateSiteUserCanViewApplicants(String email) {
SiteUser siteUser = siteUserRepository.getByEmail(email);
public void validateSiteUserCanViewApplicants(SiteUser siteUser) {
VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term).getVerifyStatus();
if (verifyStatus != VerifyStatus.APPROVED) {
throw new CustomException(APPLICATION_NOT_APPROVED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import com.example.solidconnection.score.repository.GpaScoreRepository;
import com.example.solidconnection.score.repository.LanguageTestScoreRepository;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.type.VerifyStatus;
import com.example.solidconnection.university.domain.UniversityInfoForApply;
import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository;
Expand All @@ -19,7 +18,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

Expand All @@ -38,7 +39,6 @@ public class ApplicationSubmissionService {

private final ApplicationRepository applicationRepository;
private final UniversityInfoForApplyRepository universityInfoForApplyRepository;
private final SiteUserRepository siteUserRepository;
private final GpaScoreRepository gpaScoreRepository;
private final LanguageTestScoreRepository languageTestScoreRepository;

Expand All @@ -48,10 +48,8 @@ public class ApplicationSubmissionService {
// 학점 및 어학성적이 모두 유효한 경우에만 지원서 등록이 가능하다.
// 기존에 있던 status field 우선 APRROVED로 입력시킨다.
@Transactional
public boolean apply(String email, ApplyRequest applyRequest) {
SiteUser siteUser = siteUserRepository.getByEmail(email);
public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) {
UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest();
validateUniversityChoices(universityChoiceRequest);

Long gpaScoreId = applyRequest.gpaScoreId();
Long languageTestScoreId = applyRequest.languageTestScoreId();
Expand Down Expand Up @@ -119,23 +117,4 @@ private void validateUpdateLimitNotExceed(Application application) {
throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED);
}
}

// 입력값 유효성 검증
private void validateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) {
Set<Long> uniqueUniversityIds = new HashSet<>();
uniqueUniversityIds.add(universityChoiceRequest.firstChoiceUniversityId());
if (universityChoiceRequest.secondChoiceUniversityId() != null) {
addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.secondChoiceUniversityId());
}
if (universityChoiceRequest.thirdChoiceUniversityId() != null) {
addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.thirdChoiceUniversityId());
}
}

private void addUniversityChoice(Set<Long> uniqueUniversityIds, Long universityId) {
boolean notAdded = !uniqueUniversityIds.add(universityId);
if (notAdded) {
throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.example.solidconnection.auth.client;

import com.example.solidconnection.auth.dto.oauth.AppleTokenDto;
import com.example.solidconnection.auth.dto.oauth.AppleUserInfoDto;
import com.example.solidconnection.config.client.AppleOAuthClientProperties;
import com.example.solidconnection.custom.exception.CustomException;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.security.PublicKey;
import java.util.Objects;

import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_AUTHORIZATION_FAILED;
import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN;

/*
* 애플 인증을 위한 OAuth2 클라이언트
* https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens
* */
@Component
@RequiredArgsConstructor
public class AppleOAuthClient {

private final RestTemplate restTemplate;
private final AppleOAuthClientProperties properties;
private final AppleOAuthClientSecretProvider clientSecretProvider;
private final ApplePublicKeyProvider publicKeyProvider;

public AppleUserInfoDto processOAuth(String code) {
String idToken = requestIdToken(code);
PublicKey applePublicKey = publicKeyProvider.getApplePublicKey(idToken);
return new AppleUserInfoDto(parseEmailFromToken(applePublicKey, idToken));
}

public String requestIdToken(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> formData = buildFormData(code);

try {
ResponseEntity<AppleTokenDto> response = restTemplate.exchange(
properties.tokenUrl(),
HttpMethod.POST,
new HttpEntity<>(formData, headers),
AppleTokenDto.class
);
return Objects.requireNonNull(response.getBody()).idToken();
} catch (Exception e) {
throw new CustomException(APPLE_AUTHORIZATION_FAILED, e.getMessage());
}
}

private MultiValueMap<String, String> buildFormData(String code) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("client_id", properties.clientId());
formData.add("client_secret", clientSecretProvider.generateClientSecret());
formData.add("code", code);
formData.add("grant_type", "authorization_code");
formData.add("redirect_uri", properties.redirectUrl());
return formData;
}

private String parseEmailFromToken(PublicKey applePublicKey, String idToken) {
try {
return Jwts.parser()
.setSigningKey(applePublicKey)
.parseClaimsJws(idToken)
.getBody()
.get("email", String.class);
} catch (Exception e) {
throw new CustomException(INVALID_APPLE_ID_TOKEN);
}
}
}
Loading