From 62ea0bd991ffc29db285f1a88f2644a43a803137 Mon Sep 17 00:00:00 2001 From: sanghun Date: Fri, 21 Feb 2025 01:07:16 +0900 Subject: [PATCH 01/11] =?UTF-8?q?fix=20:=20application.yml=EB=A1=9C?= =?UTF-8?q?=EB=B6=80=ED=84=B0=20=EA=B0=92=20=EB=84=98=EA=B2=A8=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 env에서 넘겨받는 방식에서 application.yml에서 넘겨 받도록 변경 --- .../backend/global/config/RedisConfig.java | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/global/config/RedisConfig.java b/backend/src/main/java/com/techeer/backend/global/config/RedisConfig.java index f2415421..181a434c 100644 --- a/backend/src/main/java/com/techeer/backend/global/config/RedisConfig.java +++ b/backend/src/main/java/com/techeer/backend/global/config/RedisConfig.java @@ -6,18 +6,17 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.core.StringRedisTemplate; @Configuration public class RedisConfig { - @Value("${REDIS_PORT}") + @Value("${spring.data.redis.port}") private int redisPort; - @Value("${REDIS_HOST}") + @Value("${spring.data.redis.host}") private String redisHost; - @Value("${REDIS_PASSWORD}") + @Value("${spring.data.redis.password}") private String redisPassword; @Bean @@ -30,17 +29,7 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - - // 키와 값의 직렬화 방식 설정 - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new StringRedisSerializer()); - template.setHashKeySerializer(new StringRedisSerializer()); - template.setHashValueSerializer(new StringRedisSerializer()); - - template.afterPropertiesSet(); - return template; + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); } } From cdaf6450c7e9151cbc191fb0b3bb249bd062e9f6 Mon Sep 17 00:00:00 2001 From: sanghun Date: Fri, 21 Feb 2025 01:08:26 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix=20:=20bean=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit String, String으로 key value를 사용하고 있는 상황이여서 StringRedisTemplate으로 변경 --- .../techeer/backend/global/redis/RedisService.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/global/redis/RedisService.java b/backend/src/main/java/com/techeer/backend/global/redis/RedisService.java index e80ca8c0..d9f620f3 100644 --- a/backend/src/main/java/com/techeer/backend/global/redis/RedisService.java +++ b/backend/src/main/java/com/techeer/backend/global/redis/RedisService.java @@ -6,7 +6,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @Service @@ -17,22 +17,21 @@ public class RedisService { @Value("${jwt.refresh.expiration}") private Long refreshTokenExpirationPeriod; - private final RedisTemplate redisTemplate; + private final StringRedisTemplate stringRedisTemplate; public String refreshTokenGet(String refreshToken) { - return redisTemplate.opsForValue().get("refreshToken:"+refreshToken); + return stringRedisTemplate.opsForValue().get("refreshToken:" + refreshToken); } public void cacheRefreshToken(String refreshToken) { String key = "refreshToken:" + refreshToken; log.info("refresh token cache key: {}", key); - // 리프레시 토큰을 Redis에 저장 (예: 7일 만료) - redisTemplate.opsForValue().set(key, refreshToken, Duration.ofMillis(refreshTokenExpirationPeriod)); + stringRedisTemplate.opsForValue().set(key, refreshToken, Duration.ofMillis(refreshTokenExpirationPeriod)); } public void deleteCacheRefreshToken(String refreshToken) { String key = "refreshToken:" + refreshToken; log.info("refresh token cache key: {}", key); - redisTemplate.delete(key); + stringRedisTemplate.delete(key); } } From 05df98489e330f11d78607b22445ff36ca60dffa Mon Sep 17 00:00:00 2001 From: sanghun Date: Fri, 21 Feb 2025 01:09:37 +0900 Subject: [PATCH 03/11] feat : redis health check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit redis 연결 성공 or 실패 여부 확인 가능하도록 connection check 작성 --- .../global/redis/RedisConnectionChecker.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/src/main/java/com/techeer/backend/global/redis/RedisConnectionChecker.java diff --git a/backend/src/main/java/com/techeer/backend/global/redis/RedisConnectionChecker.java b/backend/src/main/java/com/techeer/backend/global/redis/RedisConnectionChecker.java new file mode 100644 index 00000000..3979f4e7 --- /dev/null +++ b/backend/src/main/java/com/techeer/backend/global/redis/RedisConnectionChecker.java @@ -0,0 +1,25 @@ +package com.techeer.backend.global.redis; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisConnectionChecker { + + private final RedisConnectionFactory redisConnectionFactory; + + @PostConstruct + public void checkRedisConnection() { + try { + redisConnectionFactory.getConnection().ping(); + log.info("✅ Redis 연결 성공!"); + } catch (Exception e) { + log.error("❌ Redis 연결 실패!", e); + } + } +} From 7167a1aa053ea28156e70e30ee2eb9e60cd20737 Mon Sep 17 00:00:00 2001 From: sanghun Date: Fri, 21 Feb 2025 01:27:36 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor=20:=20log=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/techeer/backend/infra/aws/S3Uploader.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/java/com/techeer/backend/infra/aws/S3Uploader.java b/backend/src/main/java/com/techeer/backend/infra/aws/S3Uploader.java index 238fcd4b..ef471abc 100644 --- a/backend/src/main/java/com/techeer/backend/infra/aws/S3Uploader.java +++ b/backend/src/main/java/com/techeer/backend/infra/aws/S3Uploader.java @@ -33,6 +33,7 @@ public String uploadPdf(MultipartFile multipartFile) { String s3PdfName = UUID.randomUUID().toString().substring(0, 10) + "_" + originalFileName; String s3Key = "resume/" + s3PdfName; String fileUrl = uploadToS3(multipartFile, s3Key); + log.info("resume_pdf_url length: {}", fileUrl.length()); return fileUrl; } From 30314f340a4b24f07fc7f7aeb53db32d7ff05eb9 Mon Sep 17 00:00:00 2001 From: sanghun Date: Fri, 21 Feb 2025 01:29:16 +0900 Subject: [PATCH 05/11] =?UTF-8?q?chore=20:=20spring=20servlet=EC=9D=98=20m?= =?UTF-8?q?ultipart=20size=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upload file size 문제 해결을 위해 spring servlet multipart size 키움 --- .../com/techeer/backend/api/resume/domain/ResumePdf.java | 2 +- backend/src/main/resources/application.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/techeer/backend/api/resume/domain/ResumePdf.java b/backend/src/main/java/com/techeer/backend/api/resume/domain/ResumePdf.java index cb17740a..f885560a 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/domain/ResumePdf.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/domain/ResumePdf.java @@ -39,7 +39,7 @@ public class ResumePdf { @Embedded @AttributeOverrides({ - @AttributeOverride(name = "pdfUrl", column = @Column(name = "resume_pdf_url")), + @AttributeOverride(name = "pdfUrl", column = @Column(name = "resume_pdf_url", length = 1000)), @AttributeOverride(name = "pdfName", column = @Column(name = "resume_pdf_name")), @AttributeOverride(name = "pdfUUID", column = @Column(name = "resume_pdf_uuid")) }) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d438c775..7d44ee9f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,4 +1,9 @@ spring: + servlet: + multipart: + max-file-size: 100MB + max-request-size: 200MB + application: name: backend From 6b4b88d0b6c4026258f353bf335e975ee095a913 Mon Sep 17 00:00:00 2001 From: sanghun Date: Fri, 21 Feb 2025 18:20:06 +0900 Subject: [PATCH 06/11] =?UTF-8?q?test=20:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ResumeServiceIntegrationTest.java | 171 ++++++++++++++++++ .../service/ResumeServiceIntegrationTest.java | 101 ----------- backend/src/test/resources/application.yml | 13 ++ 3 files changed, 184 insertions(+), 101 deletions(-) create mode 100644 backend/src/test/java/com/techeer/backend/api/resume/controller/ResumeServiceIntegrationTest.java delete mode 100644 backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceIntegrationTest.java diff --git a/backend/src/test/java/com/techeer/backend/api/resume/controller/ResumeServiceIntegrationTest.java b/backend/src/test/java/com/techeer/backend/api/resume/controller/ResumeServiceIntegrationTest.java new file mode 100644 index 00000000..cd3bf4cb --- /dev/null +++ b/backend/src/test/java/com/techeer/backend/api/resume/controller/ResumeServiceIntegrationTest.java @@ -0,0 +1,171 @@ +package com.techeer.backend.api.resume.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.techeer.backend.api.resume.domain.Resume; +import com.techeer.backend.api.resume.dto.request.CreateResumeRequest; +import com.techeer.backend.api.resume.repository.ResumeRepository; +import com.techeer.backend.api.resume.service.ResumeService; +import com.techeer.backend.api.resume.service.facade.ResumeCreateFacade; +import com.techeer.backend.api.tag.position.Position; +import com.techeer.backend.api.user.domain.User; +import com.techeer.backend.api.user.repository.UserRepository; +import com.techeer.backend.api.user.service.UserService; +import com.techeer.backend.infra.aws.S3Uploader; +import jakarta.servlet.http.Cookie; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest +@Transactional +@AutoConfigureMockMvc +class ResumeServiceIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ResumeService resumeService; + + @Autowired + private ResumeCreateFacade resumeCreateFacade; + + @Autowired + private UserService userService; + + @MockBean + private S3Uploader s3Uploader; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ResumeRepository resumeRepository; + + @Autowired + private UserRepository userRepository; + + @BeforeEach + void setUp() { + // 필요 시 다른 리소스 초기화 + } + + @Test + @DisplayName("이력서 생성 통합 테스트 - 쿠키 기반 JWT & @RequestPart(JSON+파일)") + void createResume_ShouldCreateResume() throws Exception { + + // ----------------------------- + // Given + // ----------------------------- + // 1) /api/v1/mock/signup으로 회원 가입 + Access Token 획득 + String userId = "john_doe@gmail.com"; + MvcResult signupResult = mockMvc.perform( + MockMvcRequestBuilders.post("/api/v1/mock/signup") + .param("id", userId) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + + String signupResponse = signupResult.getResponse().getContentAsString(); + JsonNode signupJson = objectMapper.readTree(signupResponse); + + // 응답에서 JWT 문자열 추출(예: 필드명 "result") + String accessToken = signupJson.get("result").asText(); + + // 2) 이력서 생성 요청 DTO 준비 + CreateResumeRequest createResumeRequest = CreateResumeRequest.builder() + .position(Position.BACKEND) + .career(3) + .techStackNames(List.of("Java", "Spring")) + .companyNames(List.of("TechCompany")) + .build(); + + // 3) JSON 데이터를 멀티파트 파트 "resume"로 보낼 준비 + MockMultipartFile resumeJsonPart = new MockMultipartFile( + "resume", + "", // 파일 이름은 비어도 무방 + "application/json", // JSON 콘텐츠 타입 + objectMapper.writeValueAsBytes(createResumeRequest) // JSON 직렬화 + ); + + // 4) 업로드할 PDF 파일(파트 이름 "resume_file") + MockMultipartFile resumeFile = new MockMultipartFile( + "resume_file", + "resume.pdf", + "application/pdf", + new byte[1024] // 1KB짜리 PDF + ); + + // 5) S3 업로더 Mock 설정 + given(s3Uploader.uploadPdf(any(MultipartFile.class))) + .willReturn("https://s3.bucket.com/resume.pdf"); + + // 6) 쿠키 설정 (JwtAuthenticationFilter가 이 쿠키를 확인) + Cookie jwtCookie = new Cookie("accessToken", accessToken); + jwtCookie.setPath("/"); + jwtCookie.setHttpOnly(true); + + // ----------------------------- + // When + // ----------------------------- + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.multipart("/api/v1/resumes") + // "resume" JSON 파트 + .file(resumeJsonPart) + // "resume_file" PDF 파트 + .file(resumeFile) + .cookie(jwtCookie) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn(); + + // ----------------------------- + // Then + // ----------------------------- + // 1) 응답 메시지 검증 + String responseJson = result.getResponse().getContentAsString(); + assertThat(responseJson).contains("RESUME_201"); + + // 2) DB 상태 검증: + // 이메일 기준으로 User 찾기 (mockSignup 과정에서 저장됨) + User foundUser = userRepository.findByEmail(userId).orElse(null); + assertNotNull(foundUser); + + // resumeService가 최종적으로 저장한 이력서를 꺼내 확인 + Resume savedResume = resumeService.findLaterByUser(foundUser); + assertNotNull(savedResume); + assertEquals(Position.BACKEND, savedResume.getPosition()); + assertEquals(3, savedResume.getCareer()); + assertEquals(foundUser.getUsername(), savedResume.getUser().getUsername()); + assertEquals("https://s3.bucket.com/resume.pdf", savedResume.getResumePdf().getPdf().getPdfUrl()); + + // 추가 검증 (TechStack, Company 등) + // assertTrue(savedResume.getTechStackNames().contains("Java")); + // assertTrue(savedResume.getCompanyNames().contains("TechCompany")); + } +} diff --git a/backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceIntegrationTest.java b/backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceIntegrationTest.java deleted file mode 100644 index a79decd3..00000000 --- a/backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceIntegrationTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.techeer.backend.api.resume.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.techeer.backend.api.resume.domain.Resume; -import com.techeer.backend.api.resume.exception.ResumeNotFoundException; -import com.techeer.backend.api.resume.repository.ResumeRepository; -import com.techeer.backend.api.tag.position.Position; -import com.techeer.backend.api.user.domain.User; -import com.techeer.backend.api.user.repository.UserRepository; -import com.techeer.backend.api.user.service.UserService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class ResumeServiceIntegrationTest { - - @Autowired - private ResumeService resumeService; - - @Autowired - private ResumeRepository resumeRepository; - - @Autowired - private UserService userService; - - @Autowired - private UserRepository userRepository; - - private User user; - - @BeforeEach - void setUp() { - // 사용자 초기화 - user = User.builder() - .username("john_doe") - .build(); - userRepository.save(user); - } - - @Test - void getResume_WhenResumeExists_ShouldReturnResume() { - // Arrange - Resume resume = Resume.builder() - .user(user) - .name("John's Resume") - .career(5) - .position(Position.DEVOPS) - .build(); - resumeRepository.save(resume); - - // Act - Resume foundResume = resumeService.getResume(resume.getId()); - - // Assert - assertNotNull(foundResume); - assertEquals("John's Resume", foundResume.getName()); - } - - @Test - void getResume_WhenResumeDoesNotExist_ShouldThrowNotFoundException() { - // Arrange - Long invalidResumeId = 999L; - - // Act & Assert - assertThrows(ResumeNotFoundException.class, () -> resumeService.getResume(invalidResumeId)); - } - - @Test - void searchResumesByUserName_ShouldReturnResumes() { - // Arrange - Resume resume1 = Resume.builder() - .user(user) - .name("John's Resume 1") - .career(3) - .position(Position.BACKEND) - .build(); - - Resume resume2 = Resume.builder() - .user(user) - .name("John's Resume 2") - .career(5) - .position(Position.BACKEND) - .build(); - - resumeRepository.save(resume1); - resumeRepository.save(resume2); - - // Act - var resumes = resumeService.searchResumesByUserName("john_doe"); - - // Assert - assertEquals(2, resumes.size()); - } -} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index e204b824..a6402000 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -1,13 +1,20 @@ spring: + servlet: + multipart: + max-file-size: 100MB + max-request-size: 200MB + datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL username: sa password: driverClassName: org.h2.Driver + h2: console: enabled: true path: /h2-console + jpa: hibernate: ddl-auto: update @@ -22,6 +29,12 @@ spring: profiles: include: oauth + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD} + security: oauth2: client: From f0547d59561bdff125676ad4d005c35edb457ea6 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 23 Feb 2025 00:56:32 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix=20:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20v?= =?UTF-8?q?iew=20count=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이력서 조회순으로 조회 가능하도록 view count 추가, 이력서 개별 조회 로직에서 view count 증가 --- .../com/techeer/backend/api/resume/domain/Resume.java | 8 ++++++++ .../techeer/backend/api/resume/service/ResumeService.java | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/techeer/backend/api/resume/domain/Resume.java b/backend/src/main/java/com/techeer/backend/api/resume/domain/Resume.java index 14f8f14c..6507a004 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/domain/Resume.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/domain/Resume.java @@ -58,6 +58,10 @@ public class Resume extends BaseEntity { // 이후 버전(더 최신 버전) private Long laterResumeId; + // 조회수 + @Column(name = "view_count") + private Long viewCount = 0L; + @Builder.Default @OneToMany(mappedBy = "resume", cascade = CascadeType.ALL) private List resumeTechStacks = new ArrayList<>(); @@ -94,4 +98,8 @@ public void addResumePdf(ResumePdf resumePdf) { public void updateLaterResumeId(Long id) { this.laterResumeId = id; } + + public void increaseViewCount() { + this.viewCount++; + } } diff --git a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java index ee312d1c..5d01fd36 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java @@ -30,8 +30,12 @@ public Resume saveResume(Resume resume) { // 이력서 개별 조회 public Resume getResume(Long resumeId) { - return resumeRepository.findById(resumeId) + Resume resume = resumeRepository.findById(resumeId) .orElseThrow(ResumeNotFoundException::new); + + // 이력서 조회수 증가 + resume.increaseViewCount(); + return resume; } // 유저 이름으로 이력서 조회 From 0708f4abee16f22493c89ce6ff91e6941a9d2d70 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 23 Feb 2025 02:36:43 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix=20:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이력서가 없을 때 404 에러를 발생시키던 것에서 빈 값을 주는 방법으로 변경 --- .../backend/api/resume/service/ResumeService.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java index 5d01fd36..a0bdc066 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java @@ -5,8 +5,6 @@ import com.techeer.backend.api.resume.exception.ResumeNotFoundException; import com.techeer.backend.api.resume.repository.ResumeRepository; import com.techeer.backend.api.user.domain.User; -import com.techeer.backend.global.error.ErrorCode; -import com.techeer.backend.global.error.exception.BusinessException; import java.util.List; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -49,18 +47,9 @@ public Page searchByTages(ResumeSearchRequest req, Pageable pageable) { } - public Page getResumePage(Pageable pageable) { + public Slice getResumePage(Pageable pageable) { // 페이지네이션을 적용하여 레포지토리에서 데이터를 가져옴 - Page resumes = resumeRepository.findAll(pageable); - - // 첫 번째 Resume 객체를 가져옴 (예시로 첫 번째 요소를 변환) - Resume resume = resumes.getContent().isEmpty() ? null : resumes.getContent().get(0); - - // Resume가 없을 경우 빈 결과를 처리 - if (resume == null) { - throw new BusinessException(ErrorCode.RESUME_NOT_FOUND); - } - + Slice resumes = resumeRepository.findResumesByDeletedAtIsNull(pageable); return resumes; } From 943b06bcf553789c7181596c71bcaf4f3eb27ac4 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 23 Feb 2025 02:41:45 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix=20:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findAll 제거 및 page -> slice 로 변경 --- .../backend/api/resume/repository/ResumeRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java b/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java index 4d42ec5b..edc358ba 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java @@ -4,6 +4,7 @@ import com.techeer.backend.api.user.domain.User; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -24,4 +25,6 @@ public interface ResumeRepository extends JpaRepository, ResumeRep Resume findFirstByUserOrderByCreatedAtDesc(User user); Slice findResumeByUser(User user); + + Slice findResumesByDeletedAtIsNull(Pageable pageable); } From 68bcead5d6729d069aec0a75b3820372265406a5 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 23 Feb 2025 02:44:28 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix=20:=20view=5Fcount=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit view count 추가로 개별 조회 시 view가 증가, 조회 로직에서 output값으로 view_count 추가 --- .../api/resume/controller/ResumeController.java | 16 +++++++++++++++- .../api/resume/converter/ResumeConverter.java | 2 ++ .../backend/api/resume/domain/Resume.java | 11 ++++++++++- .../dto/response/PageableResumeResponse.java | 1 + .../api/resume/dto/response/ResumeResponse.java | 1 + 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java b/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java index 8d15cde1..c4b9bdfa 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java @@ -118,7 +118,7 @@ public CommonResponse> searchResumes(@RequestParam( @RequestParam(name = "size") int size) { //ResumeService를 통해 페이지네이션된 이력서 목록을 가져옵니다. final Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Order.desc("createdAt"))); - Page resumes = resumeService.getResumePage(pageable); + Slice resumes = resumeService.getResumePage(pageable); List resumeResponses = resumes.stream() .map(ResumeConverter::toPageableResumeResponse) @@ -151,4 +151,18 @@ public CommonResponse deleteResume(@PathVariable("resume_id") Long resumeId) resumeService.softDeleteResume(user, resumeId); return CommonResponse.of(SuccessCode.RESUME_SOFT_DELETED, null); } + + @Operation(summary = "이력서 조회순으로 조회") + @GetMapping("/resumes/view") + public CommonResponse> searchResumesByView(@RequestParam(name = "page") int page, + @RequestParam(name = "size") int size) { + final Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Order.desc("viewCount"))); + Slice resumeList = resumeService.getResumePage(pageable); + + List pageableResumeResponse = resumeList.stream() + .map(ResumeConverter::toPageableResumeResponse) + .collect(Collectors.toList()); + + return CommonResponse.of(SuccessCode.OK, pageableResumeResponse); + } } diff --git a/backend/src/main/java/com/techeer/backend/api/resume/converter/ResumeConverter.java b/backend/src/main/java/com/techeer/backend/api/resume/converter/ResumeConverter.java index fe11a5dd..5e64d55f 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/converter/ResumeConverter.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/converter/ResumeConverter.java @@ -17,6 +17,7 @@ public static PageableResumeResponse toPageableResumeResponse(Resume resume) { .resumeId(resume.getId()) .resumeName(resume.getName()) .userName(resume.getUser().getUsername()) + .viewCount(resume.getViewCount()) .resumeName(resume.getName()) .position(resume.getPosition().getValue()) .career(resume.getCareer()) @@ -54,6 +55,7 @@ public static ResumeDetailResponse toResumeDetailResponse(Resume resume, List companyNames; private int totalPage; private int currentPage; + private Long viewCount; } diff --git a/backend/src/main/java/com/techeer/backend/api/resume/dto/response/ResumeResponse.java b/backend/src/main/java/com/techeer/backend/api/resume/dto/response/ResumeResponse.java index 5f6181d8..5275a05e 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/dto/response/ResumeResponse.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/dto/response/ResumeResponse.java @@ -14,4 +14,5 @@ public class ResumeResponse { private final String position; private final List techStackNames; private List companyNames; + private Long viewCount; } From fc7bcc0b5a567e9a88bca595db3d55131ad2f974 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 23 Feb 2025 02:45:03 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix=20:=20update=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=20transactional=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techeer/backend/api/resume/service/ResumeService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java index a0bdc066..9d89fcf6 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java @@ -13,6 +13,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @@ -27,6 +28,7 @@ public Resume saveResume(Resume resume) { } // 이력서 개별 조회 + @Transactional public Resume getResume(Long resumeId) { Resume resume = resumeRepository.findById(resumeId) .orElseThrow(ResumeNotFoundException::new);