From bd8e3e8df86189261b64e1ee547e8efba6bf2135 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Mon, 29 Sep 2025 16:22:34 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=EC=97=AC=EB=9F=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=A7=88=EC=9D=B8?= =?UTF-8?q?=EB=93=9C=EB=A7=B5=EC=9D=98=20=EB=AA=A8=EB=93=A0=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=EA=B0=80=20=EB=8F=99=EC=9D=BC=ED=95=9C=20?= =?UTF-8?q?=EC=A0=91=EC=86=8D=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20Redis=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set 자료구조를 통한 중복 제거 - 간단한 Redis Key 사용(mindmap:{mapId}:users) --- .../mindmap/service/MindmapSseService.java | 74 +++++++++++++------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java index fdcad7a..a226046 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java @@ -1,19 +1,21 @@ package com.teamEWSN.gitdeun.mindmap.service; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails; import com.teamEWSN.gitdeun.mindmap.dto.MindmapDetailResponseDto; import com.teamEWSN.gitdeun.mindmap.dto.prompt.PromptPreviewResponseDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j @@ -22,7 +24,7 @@ public class MindmapSseService { private final ObjectMapper objectMapper; - + private final RedisTemplate redisTemplate; private record SseConnection(Long userId, String nickname, String profileImage, SseEmitter emitter) {} private final Map> connectionsByMapId = new ConcurrentHashMap<>(); @@ -45,19 +47,27 @@ public SseEmitter createConnection(Long mapId, CustomUserDetails userDetails) { connectionsByMapId.computeIfAbsent(mapId, k -> new CopyOnWriteArrayList<>()).add(connection); + try { + // Redis Set에 현재 접속자 정보 추가 + String redisKey = "mindmap:" + mapId + ":users"; + String userData = objectMapper.writeValueAsString( + new ConnectedUserDto(userDetails.getId(), userDetails.getNickname(), userDetails.getProfileImage()) + ); + redisTemplate.opsForSet().add(redisKey, userData); + + // Redis Set의 만료 시간을 설정하여 비정상 종료된 연결 처리 + redisTemplate.expire(redisKey, 1, TimeUnit.HOURS); + + } catch (JsonProcessingException e) { + log.error("사용자 정보 직렬화 실패", e); + } + // 연결 종료 시 정리 (기존 로직 개선) - emitter.onCompletion(() -> { - removeConnection(mapId, connection); - broadcastUserListUpdate(mapId); - }); - emitter.onTimeout(() -> { - removeConnection(mapId, connection); - broadcastUserListUpdate(mapId); - }); + emitter.onCompletion(() -> removeConnection(mapId, connection)); + emitter.onTimeout(() -> removeConnection(mapId, connection)); emitter.onError(throwable -> { log.error("SSE 연결 오류 - 마인드맵 ID: {}, 사용자 ID: {}", mapId, userDetails.getId(), throwable); removeConnection(mapId, connection); - broadcastUserListUpdate(mapId); }); // 연결 확인용 초기 메시지 @@ -158,14 +168,20 @@ private void sendToEmitter(SseEmitter emitter, Object data) { // 연결 종료 시 SseConnection 객체를 찾아 제거 private void removeConnection(Long mapId, SseConnection connection) { List connections = connectionsByMapId.get(mapId); - if (connections != null) { - connections.remove(connection); - log.info("SSE 연결 해제 - 마인드맵 ID: {}, 사용자 ID: {}", mapId, connection.userId()); - if (connections.isEmpty()) { - connectionsByMapId.remove(mapId); - log.info("마인드맵 ID {} 의 모든 구독자 연결 종료", mapId); + if (connections != null && connections.remove(connection)) { + try { + // Redis Set에서 접속 종료한 사용자 정보 제거 + String redisKey = "mindmap:" + mapId + ":users"; + String userData = objectMapper.writeValueAsString( + new ConnectedUserDto(connection.userId(), connection.nickname(), connection.profileImage()) + ); + redisTemplate.opsForSet().remove(redisKey, userData); + + } catch (JsonProcessingException e) { + log.error("사용자 정보 직렬화 실패 (연결 종료)", e); } broadcastUserListUpdate(mapId); + log.info("SSE 연결 해제 및 Redis 업데이트 - 마인드맵 ID: {}, 사용자 ID: {}", mapId, connection.userId()); } } @@ -188,13 +204,23 @@ public record ConnectedUserDto(Long userId, String nickname, String profileImage // 현재 접속 중인 사용자 목록을 반환하는 서비스 메서드 public List getConnectedUsers(Long mapId) { - List connections = connectionsByMapId.getOrDefault(mapId, new CopyOnWriteArrayList<>()); + String redisKey = "mindmap:" + mapId + ":users"; + Set usersJson = redisTemplate.opsForSet().members(redisKey); - // 중복된 userId를 제거하고 DTO로 변환하여 반환 (여러 탭 접속 시 중복 방지) - return connections.stream() - .map(conn -> new ConnectedUserDto(conn.userId(), conn.nickname(), conn.profileImage())) - .distinct() // userId 기준으로 중복 제거 + if (usersJson == null) { + return Collections.emptyList(); + } + + return usersJson.stream() + .map(json -> { + try { + return objectMapper.readValue(json, ConnectedUserDto.class); + } catch (JsonProcessingException e) { + log.error("사용자 정보 역직렬화 실패", e); + return null; + } + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); } - } \ No newline at end of file From d1c84afdb7db2e8b9c7bfebc6ff5a6cc50d1ddef Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Thu, 2 Oct 2025 23:14:39 +0900 Subject: [PATCH 02/12] =?UTF-8?q?setting:=20fastapi=20ec2=20=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=ED=84=B4=EC=8A=A4=20url=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index cf65f9a..507df30 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -31,4 +31,4 @@ app: same-site: None fastapi: - base-url: http://fastapi-server:8000 # Docker 네트워크 내부에서 사용할 주소 \ No newline at end of file + base-url: http://13.124.93.205:8000 # Docker 네트워크 내부에서 사용할 주소 \ No newline at end of file From 6e6b301cf061f2affb32cafa4ac0f015b76a0731 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Fri, 3 Oct 2025 17:35:17 +0900 Subject: [PATCH 03/12] =?UTF-8?q?setting:=20Actuator=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ docker-compose.prod.yml | 6 +++--- .../gitdeun/common/config/SecurityConfig.java | 1 + src/main/resources/application-prod.yml | 12 +++++++++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index d0cfbd2..e7e9fc7 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,9 @@ dependencies { // OAuth 2.0 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + // Actuator/Health + implementation("org.springframework.boot:spring-boot-starter-actuator") + // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f1a80d0..3b06946 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -16,11 +16,11 @@ services: volumes: - ./logs:/app/logs healthcheck: - test: ["CMD-SHELL", "wget -qO- http://localhost:8080/actuator/health | grep -q 'UP' || exit 1"] + test: [ "CMD", "curl", "-fsS", "http://127.0.0.1:8080/actuator/health" ] interval: 15s timeout: 5s - retries: 10 - start_period: 30s + retries: 20 + start_period: 60s redis: image: redis:7-alpine diff --git a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java index f7db26b..013bf39 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java @@ -69,6 +69,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests((auth) -> auth // 내부 webhook 통신 API .requestMatchers("/api/webhook/**").permitAll() + .requestMatchers("/actuator/health", "/actuator/health/**", "/actuator/info").permitAll() // 외부 공개 API(클라이언트 - JWT) .requestMatchers(SecurityPath.ADMIN_ENDPOINTS).hasRole("ADMIN") .requestMatchers(SecurityPath.USER_ENDPOINTS).hasAnyRole("USER", "ADMIN") diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 507df30..dd3f6a1 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -31,4 +31,14 @@ app: same-site: None fastapi: - base-url: http://13.124.93.205:8000 # Docker 네트워크 내부에서 사용할 주소 \ No newline at end of file + base-url: http://13.124.93.205:8000 # Docker 네트워크 내부에서 사용할 주소 + +management: + endpoints: + web: + exposure: + include: health,info + endpoint: + health: + probes: + enabled: true \ No newline at end of file From c71f9b8a2979a2d1dac99a834c023cf40cc7d803 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Fri, 3 Oct 2025 18:59:46 +0900 Subject: [PATCH 04/12] =?UTF-8?q?refector:=20SecurityPath=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EB=B3=84=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitdeun/common/config/SecurityConfig.java | 29 ++++++++++--------- .../gitdeun/common/config/SecurityPath.java | 9 +++++- src/main/resources/application-dev.yml | 3 +- src/main/resources/application-prod.yml | 2 ++ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java index 013bf39..a63c5a4 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java @@ -7,6 +7,7 @@ import com.teamEWSN.gitdeun.common.oauth.handler.CustomOAuth2SuccessHandler; import com.teamEWSN.gitdeun.common.oauth.service.CustomOAuth2UserService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -35,6 +36,9 @@ public class SecurityConfig { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; + @Value("${security.require-auth-for-unknown:true}") + private boolean requireAuthForUnknown; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -53,9 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .oauth2Login((oauth2) -> oauth2 .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig .userService(customOAuth2UserService)) -// .defaultSuccessUrl("/oauth/success") // 로그인 성공시 이동할 URL .successHandler(customOAuth2SuccessHandler) -// .failureUrl("/oauth/fail") // 로그인 실패시 이동할 URL .failureHandler(customOAuthFailureHandler)) .logout(logout -> logout .logoutUrl("/logout") @@ -66,17 +68,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 경로별 인가 작업 http - .authorizeHttpRequests((auth) -> auth - // 내부 webhook 통신 API - .requestMatchers("/api/webhook/**").permitAll() - .requestMatchers("/actuator/health", "/actuator/health/**", "/actuator/info").permitAll() - // 외부 공개 API(클라이언트 - JWT) - .requestMatchers(SecurityPath.ADMIN_ENDPOINTS).hasRole("ADMIN") - .requestMatchers(SecurityPath.USER_ENDPOINTS).hasAnyRole("USER", "ADMIN") - .requestMatchers(SecurityPath.PUBLIC_ENDPOINTS).permitAll() - .anyRequest().permitAll() - // .anyRequest().authenticated() - ); + .authorizeHttpRequests((auth) -> { + auth + .requestMatchers(SecurityPath.ADMIN_ENDPOINTS).hasRole("ADMIN") + .requestMatchers(SecurityPath.USER_ENDPOINTS).hasAnyRole("USER", "ADMIN") + .requestMatchers(SecurityPath.PUBLIC_ENDPOINTS).permitAll(); + + if (requireAuthForUnknown) { + auth.anyRequest().authenticated(); + } else { + auth.anyRequest().permitAll(); + } + }); // 예외 처리 http diff --git a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java index 24d0f9d..820ba05 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java @@ -7,8 +7,13 @@ public class SecurityPath { public static final String[] PUBLIC_ENDPOINTS = { "/api/auth/token/refresh", "/api/auth/oauth/refresh/*", + "/oauth/logout", "/", + "/api/webhook/**", + "/actuator/health", + "/actuator/health/**", + "/actuator/info" }; // hasRole("USER") @@ -27,8 +32,10 @@ public class SecurityPath { "/api/skills/**", "/api/recruitments/**", "/api/applications/**", + "/api/comments/**", + "/api/code-reviews/**", + "/api/references/**", "/api/s3/bucket/**", - "/api/webhook/**" }; // hasRole("ADMIN") diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 37d7e1b..44336ec 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -21,7 +21,8 @@ spring: # user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo # user-name-attribute: sub - +security: + require-auth-for-unknown: false jwt: access-expired: 28800 # 8시간 refresh-expired: 86400 # 1일 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index dd3f6a1..f21a298 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -33,6 +33,8 @@ app: fastapi: base-url: http://13.124.93.205:8000 # Docker 네트워크 내부에서 사용할 주소 +security: + require-auth-for-unknown: false management: endpoints: web: From 399546caf319007c43b75b202f77e138fef5f09d Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Fri, 3 Oct 2025 22:40:34 +0900 Subject: [PATCH 05/12] =?UTF-8?q?refector:=20SecurityPath=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EB=B3=84=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3b06946..74d9d3f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -16,7 +16,7 @@ services: volumes: - ./logs:/app/logs healthcheck: - test: [ "CMD", "curl", "-fsS", "http://127.0.0.1:8080/actuator/health" ] + test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:8080/actuator/health | grep -q '\"status\":\"UP\"'" ] interval: 15s timeout: 5s retries: 20 From 914efc6aa1a71b81516390be860fbcedf3966105 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Sat, 4 Oct 2025 00:33:13 +0900 Subject: [PATCH 06/12] =?UTF-8?q?setting:=20application-prod.yml=20fastapi?= =?UTF-8?q?=EC=9D=98=20baseUrl=EC=9D=84=20Private=20IP=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index f21a298..b9f0ae7 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -31,7 +31,7 @@ app: same-site: None fastapi: - base-url: http://13.124.93.205:8000 # Docker 네트워크 내부에서 사용할 주소 + base-url: http://172.31.47.81:8000 # Docker 네트워크 내부에서 사용할 주소 security: require-auth-for-unknown: false From 61aea5decf805745d68f5088f8986068f91489b5 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Mon, 6 Oct 2025 00:30:59 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EC=B6=94=EC=B6=9C=20=ED=95=A8=EC=88=98=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 --- .../gitdeun/common/fastapi/FastApiClient.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java index b9d9390..6e9b704 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java @@ -313,14 +313,13 @@ public String getFileRaw(String repoUrl, String filePath, Integer startLine, Int }*/ public String getCodeFromNode(String nodeKey, String filePath, String authHeader) { - String fileName = extractFileName(filePath); try { - log.debug("FastAPI 노드 기반 코드 조회 시작 - nodeKey: {}, filePath: {}", nodeKey, fileName); + log.debug("FastAPI 노드 기반 코드 조회 시작 - nodeKey: {}, filePath: {}", nodeKey, filePath); UriComponentsBuilder uriBuilder = UriComponentsBuilder .fromPath("/content/file/by-node") // FastAPI의 해당 엔드포인트 .queryParam("node_key", nodeKey) - .queryParam("file_path", fileName); + .queryParam("file_path", filePath); // 'prefer' 파라미터는 생략하여 FastAPI의 기본값(auto)을 따르도록 함 String uri = uriBuilder.build().toUriString(); @@ -336,8 +335,8 @@ public String getCodeFromNode(String nodeKey, String filePath, String authHeader .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> clientResponse.bodyToMono(String.class) .map(errorBody -> { - log.warn("FastAPI 노드 코드 조회 4xx 오류 - nodeKey: {}, fileName: {}, status: {}, body: {}", - nodeKey, fileName, clientResponse.statusCode(), errorBody); + log.warn("FastAPI 노드 코드 조회 4xx 오류 - nodeKey: {}, filePath: {}, status: {}, body: {}", + nodeKey, filePath, clientResponse.statusCode(), errorBody); return new RuntimeException("FastAPI 클라이언트 오류: " + errorBody); }) ) @@ -346,13 +345,13 @@ public String getCodeFromNode(String nodeKey, String filePath, String authHeader .block(); String codeContent = (response != null) ? response.getCode() : ""; - log.debug("FastAPI 노드 기반 코드 조회 성공 - nodeKey: {}, fileName: {}, 길이: {}", - nodeKey, fileName, codeContent != null ? codeContent.length() : 0); + log.debug("FastAPI 노드 기반 코드 조회 성공 - nodeKey: {}, filePath: {}, 길이: {}", + nodeKey, filePath, codeContent != null ? codeContent.length() : 0); return codeContent; } catch (Exception e) { - log.error("FastAPI 노드 기반 코드 조회 실패 - nodeKey: {}, fileName: {}", nodeKey, fileName, e); + log.error("FastAPI 노드 기반 코드 조회 실패 - nodeKey: {}, filePath: {}", nodeKey, filePath, e); return ""; // 예외 발생 시 빈 문자열 반환 } } @@ -365,11 +364,6 @@ private String extractMapId(String repoUrl) { return segments[segments.length - 1].replaceAll("\\.git$", ""); } - // 파일명 추출 - public String extractFileName(String filePath) { - return filePath.substring(filePath.lastIndexOf('/') + 1); - } - private AnalysisResultDto buildAnalysisResultDto( RepoInfoResponse repoInfo ) { From d9de32dd77f766dca367ed0d948b19ea833b8147 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Mon, 13 Oct 2025 15:00:05 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20languages=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=AA=A8=EC=A7=91=20=EA=B3=B5=EA=B3=A0=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RecruitmentController.java | 4 +++- .../repository/RecruitmentCustomRepository.java | 2 ++ .../repository/RecruitmentRepositoryImpl.java | 15 +++++++++++---- .../recruitment/service/RecruitmentService.java | 4 ++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/recruitment/controller/RecruitmentController.java b/src/main/java/com/teamEWSN/gitdeun/recruitment/controller/RecruitmentController.java index 6c4ad01..935d173 100644 --- a/src/main/java/com/teamEWSN/gitdeun/recruitment/controller/RecruitmentController.java +++ b/src/main/java/com/teamEWSN/gitdeun/recruitment/controller/RecruitmentController.java @@ -5,6 +5,7 @@ import com.teamEWSN.gitdeun.recruitment.entity.RecruitmentStatus; import com.teamEWSN.gitdeun.recruitment.service.RecruitmentService; import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails; +import com.teamEWSN.gitdeun.userskill.entity.DeveloperSkill; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -85,9 +86,10 @@ public ResponseEntity> searchRecruitments( @RequestParam(required = false) String keyword, @RequestParam(required = false) RecruitmentStatus status, @RequestParam(required = false) List field, + @RequestParam(required = false) List languages, @PageableDefault(size = 10) Pageable pageable ) { - Page response = recruitmentService.searchRecruitments(keyword, status, field, pageable); + Page response = recruitmentService.searchRecruitments(keyword, status, field, languages, pageable); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentCustomRepository.java b/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentCustomRepository.java index f17c3e4..27bf2de 100644 --- a/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentCustomRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentCustomRepository.java @@ -3,6 +3,7 @@ import com.teamEWSN.gitdeun.recruitment.entity.Recruitment; import com.teamEWSN.gitdeun.recruitment.entity.RecruitmentField; import com.teamEWSN.gitdeun.recruitment.entity.RecruitmentStatus; +import com.teamEWSN.gitdeun.userskill.entity.DeveloperSkill; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -13,6 +14,7 @@ Page searchRecruitments( String keyword, RecruitmentStatus status, List fields, + List languages, Pageable pageable ); } diff --git a/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java b/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java index 06bf211..7b24bb9 100644 --- a/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java +++ b/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java @@ -7,6 +7,7 @@ import com.teamEWSN.gitdeun.recruitment.entity.Recruitment; import com.teamEWSN.gitdeun.recruitment.entity.RecruitmentField; import com.teamEWSN.gitdeun.recruitment.entity.RecruitmentStatus; +import com.teamEWSN.gitdeun.userskill.entity.DeveloperSkill; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -33,12 +34,12 @@ public class RecruitmentRepositoryImpl implements RecruitmentCustomRepository { @Override public Page searchRecruitments( - String keyword, RecruitmentStatus status, List fields, Pageable pageable + String keyword, RecruitmentStatus status, List fields, List languages, Pageable pageable ) { // 키워드 전처리 후 Full-Text Search 활용 여부 확인 String processed = preprocessKeyword(keyword); - // 키워드 없으면: 키워드 조건 없이 status/fields만으로 페이지 조회 + // 키워드 없으면: 키워드 조건 없이 status/fields/languages 으로 페이지 조회 boolean hasKeyword = (processed != null); boolean useFullTextSearch = hasKeyword && isFullTextSearchAvailable(processed); @@ -52,7 +53,7 @@ public Page searchRecruitments( // id 페이지닝 List ids = queryFactory.select(recruitment.id).distinct() .from(recruitment) - .where(keywordExpr, statusEq(status), fieldOrFilter(fields)) + .where(keywordExpr, statusEq(status), fieldOrFilter(fields), languageOrFilter(languages)) .orderBy(useFullTextSearch ? scoreOrder(processed) : recruitment.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -71,7 +72,7 @@ public Page searchRecruitments( // 카운팅 Long total = queryFactory.select(recruitment.id.countDistinct()) .from(recruitment) - .where(keywordExpr, statusEq(status), fieldOrFilter(fields)) + .where(keywordExpr, statusEq(status), fieldOrFilter(fields), languageOrFilter(languages)) .fetchOne(); return new PageImpl<>(content, pageable, total != null ? total : 0L); @@ -190,4 +191,10 @@ private BooleanExpression fieldOrFilter(List fields) { ? null : recruitment.fieldTags.any().in(fields); } + + private BooleanExpression languageOrFilter(List languages) { + return CollectionUtils.isEmpty(languages) + ? null + : recruitment.languageTags.any().in(languages); + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/recruitment/service/RecruitmentService.java b/src/main/java/com/teamEWSN/gitdeun/recruitment/service/RecruitmentService.java index bae53ce..7edb626 100644 --- a/src/main/java/com/teamEWSN/gitdeun/recruitment/service/RecruitmentService.java +++ b/src/main/java/com/teamEWSN/gitdeun/recruitment/service/RecruitmentService.java @@ -116,8 +116,8 @@ public RecruitmentDetailResponseDto getRecruitment(Long recruitmentId) { * @return 페이징 처리된 검색 결과 목록 */ @Transactional(readOnly = true) - public Page searchRecruitments(String keyword, RecruitmentStatus status, List fields, Pageable pageable) { - return recruitmentRepository.searchRecruitments(keyword, status, fields, pageable) + public Page searchRecruitments(String keyword, RecruitmentStatus status, List fields, List languages, Pageable pageable) { + return recruitmentRepository.searchRecruitments(keyword, status, fields, languages, pageable) .map(recruitmentMapper::toListResponseDto); } From e1da565d22db04667aaacfbc3f1b97ac3b8ca11b Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Thu, 16 Oct 2025 00:16:49 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=EB=AA=A8=EC=A7=91=20=EA=B3=B5?= =?UTF-8?q?=EA=B3=A0=20=ED=95=9C=EA=B8=80=EC=9E=90=20=EC=99=84=EC=A0=84=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RecruitmentRepositoryImpl.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java b/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java index 7b24bb9..9b51bc4 100644 --- a/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java +++ b/src/main/java/com/teamEWSN/gitdeun/recruitment/repository/RecruitmentRepositoryImpl.java @@ -41,14 +41,20 @@ public Page searchRecruitments( // 키워드 없으면: 키워드 조건 없이 status/fields/languages 으로 페이지 조회 boolean hasKeyword = (processed != null); - boolean useFullTextSearch = hasKeyword && isFullTextSearchAvailable(processed); - BooleanExpression keywordExpr = null; + boolean useFullTextSearch = false; + if (hasKeyword) { - keywordExpr = useFullTextSearch ? titleFullTextSearch(processed) - : fallbackContains(processed); + if (processed.length() == 1) { + keywordExpr = titleExactMatch(processed); // 한 글자: 완전 일치 검색 + } else { + useFullTextSearch = isFullTextSearchAvailable(processed); + keywordExpr = useFullTextSearch ? titleFullTextSearch(processed) // 두 글자 이상: FTS 또는 Contains + : fallbackContains(processed); + } } + // 엔티티 자체 데이터 조회 // id 페이지닝 List ids = queryFactory.select(recruitment.id).distinct() @@ -61,7 +67,7 @@ public Page searchRecruitments( if (ids.isEmpty()) return Page.empty(pageable); - // 내용 로딩 + 순서 복원 + // ID 목록으로 전체 데이터 조회 String idCsv = ids.stream().map(String::valueOf).collect(Collectors.joining(",")); List content = queryFactory.selectFrom(recruitment) .where(recruitment.id.in(ids)) @@ -69,7 +75,7 @@ public Page searchRecruitments( "FIELD({0}, " + idCsv + ")", recruitment.id).asc()) .fetch(); - // 카운팅 + // 전체 카운트 조회 Long total = queryFactory.select(recruitment.id.countDistinct()) .from(recruitment) .where(keywordExpr, statusEq(status), fieldOrFilter(fields), languageOrFilter(languages)) @@ -103,12 +109,17 @@ private String preprocessKeyword(String keyword) { } // 길이 제한 - if (s.length() < 2) return null; + if (s.isEmpty()) return null; if (s.length() > 30) s = s.substring(0, 30); return s; } + // 제목 완전 일치 검색(한글자용) + private BooleanExpression titleExactMatch(String keyword) { + return recruitment.title.eq(keyword); + } + // Full Text Search 조건 (전처리 이후 동작) private boolean isFullTextSearchAvailable(String keyword) { if (!StringUtils.hasText(keyword)) { From 6467eb1b7c8f2cdc899f01dd140c455cc35333df Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Thu, 16 Oct 2025 22:55:26 +0900 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=82=AD=EC=A0=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/CodeReferenceRepository.java | 1 - .../codereference/service/CodeReferenceService.java | 12 +++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java b/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java index d0d978c..92febb4 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java @@ -14,5 +14,4 @@ public interface CodeReferenceRepository extends JpaRepository findByMindmapIdAndId(Long mindmapId, Long id); - boolean existsByMindmapIdAndId(Long mindmapId, Long id); } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java b/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java index f664f66..db02871 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java @@ -127,18 +127,16 @@ public ReferenceResponse updateReference(Long mapId, Long refId, Long userId, Cr @Transactional public void deleteReference(Long mapId, Long refId, Long userId) { - // 1. 권한 확인 + // 권한 확인 if (!mindmapAuthService.hasEdit(mapId, userId)) { throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); } - // 2. 해당 마인드맵에 코드 참조가 존재하는지 확인 - if (!codeReferenceRepository.existsByMindmapIdAndId(mapId, refId)) { - throw new GlobalException(ErrorCode.CODE_REFERENCE_NOT_FOUND); - } + CodeReference codeReference = codeReferenceRepository.findByMindmapIdAndId(mapId, refId) + .orElseThrow(() -> new GlobalException(ErrorCode.CODE_REFERENCE_NOT_FOUND)); - // 3. 코드 참조 삭제 - codeReferenceRepository.deleteById(refId); + // 코드 참조 삭제 + codeReferenceRepository.delete(codeReference); } private String extractLines(String fullContent, Integer startLine, Integer endLine) { From a0ffb39c2c57569459b63ce0f5c3207e6c429a32 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Fri, 17 Oct 2025 13:15:57 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=95=EB=B3=B4=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/oauth/service/CustomOAuth2UserService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java b/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java index d934856..9988f66 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java @@ -67,7 +67,12 @@ public User processUser(OAuth2User oAuth2User, OAuth2UserRequest userRequest) { .map(conn -> { // provider 별 refresh 정책 socialTokenRefreshService.refreshSocialToken(conn, accessToken, refreshToken); - return conn.getUser(); + + // 사용자 정보 갱신 + User user = conn.getUser(); + user.updateProfile(dto.getName(), dto.getProfileImageUrl()); + + return user; }) .orElseGet(() -> createOrConnect(dto, provider, providerId, accessToken, refreshToken)); } @@ -105,6 +110,7 @@ private User createOrConnect(OAuth2ResponseDto response, OauthProvider provider, return userRepository.findByEmailAndDeletedAtIsNull(response.getEmail()) .map(user -> { // 사용자가 존재하면, 새 소셜 계정을 연결 + user.updateProfile(response.getName(), response.getProfileImageUrl()); connectSocialAccount(user, provider, providerId, accessToken, refreshToken); return user; }) From 0f5047f98c11540c458f2642e7833d297c6245c4 Mon Sep 17 00:00:00 2001 From: kobumseouk Date: Sat, 18 Oct 2025 03:40:18 +0900 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=95=EB=B3=B4=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomOAuth2UserService.java | 19 +++++++++++++++- .../common/oauth/service/GoogleApiHelper.java | 22 +++++++++++++++++++ .../user/repository/UserRepository.java | 3 --- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java b/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java index 9988f66..3ebbd91 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/CustomOAuth2UserService.java @@ -34,6 +34,7 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService { private final GitHubApiHelper gitHubApiHelper; + private final GoogleApiHelper googleApiHelper; private final SocialTokenRefreshService socialTokenRefreshService; private final UserRepository userRepository; private final SocialConnectionRepository socialConnectionRepository; @@ -71,7 +72,6 @@ public User processUser(OAuth2User oAuth2User, OAuth2UserRequest userRequest) { // 사용자 정보 갱신 User user = conn.getUser(); user.updateProfile(dto.getName(), dto.getProfileImageUrl()); - return user; }) .orElseGet(() -> createOrConnect(dto, provider, providerId, accessToken, refreshToken)); @@ -84,8 +84,24 @@ private OAuth2ResponseDto getOAuth2ResponseDto(OAuth2User oAuth2User, OAuth2User Map attr = new HashMap<>(oAuth2User.getAttributes()); if (registrationId.equalsIgnoreCase("google")) { + String accessToken = userRequest.getAccessToken().getTokenValue(); + + // 만료 시 토큰 리프레시 후 최신 userinfo 재조회 + if (googleApiHelper.isExpired(accessToken)) { + String refreshToken = (String) userRequest.getAdditionalParameters().get("refresh_token"); + if (refreshToken != null) { + var tokenResp = googleApiHelper.refreshToken(refreshToken); + accessToken = tokenResp.accessToken(); + } + } + + // 항상 최신 userinfo로 덮어쓰기 (이름 변경 즉시 반영) + Map latestInfo = googleApiHelper.fetchLatestUserInfo(accessToken); + attr.putAll(latestInfo); + return new GoogleResponseDto(attr); } + if (registrationId.equalsIgnoreCase("github")) { /* ① 기본 프로필에 e-mail 없으면 /user/emails 호출 */ if (attr.get("email") == null) { @@ -156,6 +172,7 @@ private void connectSocialAccount(User user, OauthProvider provider, String prov .accessToken(accessToken) .refreshToken(refreshToken) .build(); + socialConnectionRepository.save(connection); } diff --git a/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/GoogleApiHelper.java b/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/GoogleApiHelper.java index 753a0cd..d7dcfd4 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/GoogleApiHelper.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/oauth/service/GoogleApiHelper.java @@ -15,6 +15,8 @@ import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; +import java.util.Map; + @Slf4j @Component @@ -96,6 +98,26 @@ protected GoogleTokenResponse refreshToken(String refreshToken) { } } + /** + * access token 으로 최신 프로필(userinfo) 조회 + * (name, picture, email 등) + */ + public Map fetchLatestUserInfo(String accessToken) { + try { + return webClient.get() + .uri("https://www.googleapis.com/oauth2/v2/userinfo") + .header("Authorization", "Bearer " + accessToken) + .retrieve() + .bodyToMono(Map.class) + .block(); + } catch (WebClientResponseException e) { + log.error("Google userinfo 조회 실패: {}", e.getResponseBodyAsString()); + throw new GlobalException(ErrorCode.OAUTH_COMMUNICATION_FAILED); + } catch (Exception e) { + log.error("Google userinfo 조회 중 오류: {}", e.getMessage()); + throw new GlobalException(ErrorCode.OAUTH_COMMUNICATION_FAILED); + } + } public Mono revokeToken(String accessToken) { String revokeUrl = "https://accounts.google.com/o/oauth2/revoke"; diff --git a/src/main/java/com/teamEWSN/gitdeun/user/repository/UserRepository.java b/src/main/java/com/teamEWSN/gitdeun/user/repository/UserRepository.java index 7ce92f3..6e5b186 100644 --- a/src/main/java/com/teamEWSN/gitdeun/user/repository/UserRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/user/repository/UserRepository.java @@ -18,7 +18,4 @@ public interface UserRepository extends JpaRepository { // user email로 검색 Optional findByEmailAndDeletedAtIsNull(String email); - - // 닉네임으로 사용자 찾기 - Optional findByNicknameAndDeletedAtIsNull(String nickname); } \ No newline at end of file