Skip to content

Conversation

@KiSeungMin
Copy link
Member

@KiSeungMin KiSeungMin commented Aug 7, 2025

✔️ 연관 이슈

📝 작업 내용

  • 현재까지의 진행 상황을 main 브랜치에 반영합니다.
    • 유저 관련 버그 수정
    • 대중교통 길찾기 API 구현
    • 도보 길찾기 API 구현
    • 관리자 페이지 개선
    • 파일 업로드 기능 개선

스크린샷 (선택)

Summary by CodeRabbit

  • 신규 기능

    • 지도 자동완성 및 장소 저장 기능이 추가되었습니다.
    • 관리자 대시보드에 오늘 가입자/방문자 통계가 표시됩니다.
    • 이미지 업로드 API 및 관리자 페이지 내 이미지 업로드 UI가 도입되었습니다.
    • 사용자 행동 로그(회원가입, 토큰 갱신 등)가 비동기로 기록되어 통계에 활용됩니다.
  • 버그 수정

    • 관리자 및 사용자 상세/목록 화면에서 로그인 유형, 사용자 유형 등 null 값 처리로 오류를 방지하였습니다.
  • 기능 개선

    • 웨이블존 이미지 변경 시 기존 이미지를 S3에서 삭제하도록 개선되었습니다.
    • 길찾기 API가 Elasticsearch 기반 자동완성 및 장소 인덱싱을 지원합니다.
    • 외부 API 연동이 WebClient 기반으로 개선되고, 거리 제한(30km)이 추가되었습니다.
  • 보안/접근성

    • 파일 업로드 API가 인증 없이 접근 가능하도록 보안 설정이 변경되었습니다.
  • 기타

    • 예외 발생 시 Discord 웹훅으로 알림이 전송됩니다.
    • Elasticsearch 인덱스 및 한글 분석 설정 파일이 추가되었습니다.

zyovn and others added 30 commits August 3, 2025 14:02
[feat] Discord Webhook 에러 알림 기능 구현
zyovn and others added 22 commits August 6, 2025 22:56
[feat] 관리자 페이지에 당일 가입자 수, 방문자 수 조회 로직 추가
[FEAT] 길찾기 검색 연관 키워드 API
[refactor] 대중교통 길찾기 api 관련 리팩토링
[feat] 파일 업로드 기능 개선 및 API 구현, 관리자 페이지 웨이블존 이미지 등록&수정 기능 구현
@KiSeungMin KiSeungMin self-assigned this Aug 7, 2025
@KiSeungMin KiSeungMin added the release 프로덕션 서버에 배포 label Aug 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 7, 2025

Walkthrough

이 변경 사항은 지도 길찾기(방향) 기능, 장소 관리, 이미지 업로드(S3 연동), 사용자 행동 로그(Elasticsearch), 관리자 대시보드 통계, 예외 디스코드 알림, 보안 및 HTTP 클라이언트 설정 등 다양한 영역에 걸친 대규모 기능 추가와 리팩토링을 포함합니다. 프론트엔드 템플릿과 백엔드 서비스, 저장소, DTO, 엔티티, 설정 파일 등이 모두 업데이트되었습니다.

Changes

Cohort / File(s) Change Summary
지도 길찾기/장소 관리 및 검색
src/main/java/com/wayble/server/direction/controller/DirectionController.java, .../swagger/DirectionSwagger.java, .../dto/request/DirectionSearchRequest.java, .../dto/request/PlaceSaveRequest.java, .../dto/response/DirectionSearchResponse.java, .../entity/DirectionDocument.java, .../entity/Place.java, .../repository/DirectionElasticSearchCustomRepository.java, .../repository/DirectionElasticSearchCustomRepositoryImpl.java, .../repository/DirectionElasticsearchRepository.java, .../repository/PlaceRepository.java, .../service/DirectionService.java, .../dto/TransportationResponseDto.java, .../entity/transportation/Edge.java, .../entity/transportation/Node.java, .../exception/DirectionErrorCase.java, .../controller/TransportationController.java, .../service/TransportationService.java, .../service/FacilityService.java, .../repository/FacilityRepository.java, .../controller/WalkingController.java, .../controller/swagger/WalkingSwagger.java, .../service/WalkingService.java, .../external/kric/KricProperties.java, .../external/kric/dto/KricToiletRawBody.java, .../external/kric/dto/KricToiletRawItem.java, .../external/kric/dto/KricToiletRawResponse.java
길찾기(방향) 및 장소 저장/검색 기능 전체 구현, Elasticsearch 연동, DTO/엔티티/저장소/서비스/컨트롤러/Swagger 문서화 추가, TMap/KRIC 관련 패키지 이동 및 설정 추가, 도보/교통 길찾기 리팩토링 및 예외 처리 강화, 관련 엔티티 및 DTO 필드 수정, 엘라스틱서치 매핑/설정 파일 추가, EdgeService 삭제 등
이미지 업로드 및 S3 관리
src/main/java/com/wayble/server/aws/AmazonS3Manager.java, .../AwsErrorCase.java, .../controller/FileUploadController.java, .../admin/service/AdminWaybleZoneService.java, .../wayblezone/entity/WaybleZone.java, src/main/resources/templates/admin/wayblezone/wayble-zone-create.html, .../wayble-zone-edit.html, .../wayble-zone-detail.html
S3 이미지 업로드/삭제 기능 추가, 이미지 업로드 API 및 예외 처리, 관리자 페이지 이미지 업로드 UI 및 미리보기/진행률/드래그앤드롭/직접 URL입력 지원, 기존 이미지 삭제 로직, WaybleZone 이미지 필드 초기화 방식 변경 등
사용자 행동 로그(Elasticsearch) 및 통계
src/main/java/com/wayble/server/logging/entity/UserActionLog.java, .../repository/UserActionLogRepository.java, .../service/UserActionLogService.java, .../admin/service/AdminSystemService.java, .../admin/controller/AdminController.java, .../admin/dto/DailyStatsDto.java, src/main/resources/templates/admin/dashboard.html, .../user/service/UserService.java, .../auth/service/AuthService.java, .../auth/service/KakaoLoginService.java, .../user/service/UserInfoService.java, src/main/java/com/wayble/server/logging/config/LoggingAsyncConfig.java
사용자 회원가입/토큰갱신 행동 로그 비동기 저장, 일일 가입자/방문자/통계 API 및 관리자 대시보드 표시, Elasticsearch 저장소 및 엔티티/서비스/DTO 추가, 관련 서비스/컨트롤러/템플릿 수정, 비동기 로깅 설정 등
예외/오류 Discord Webhook 알림
src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java, .../common/dto/DiscordWebhookPayload.java, .github/workflows/cd-develop.yml
예외 발생 시 Discord Webhook으로 상세 알림 전송, Webhook 페이로드 구조 추가, Github Actions 워크플로우 메시지 단순화 등
보안 및 HTTP 클라이언트/설정
src/main/java/com/wayble/server/common/config/SecurityConfig.java, .../common/config/WebClientConfig.java, .../common/config/RestTemplateConfig.java, src/main/java/com/wayble/server/ServerApplication.java
파일 업로드 API 공개, directions API 비공개로 변경, WebClient 기반 외부 API 연동, RestTemplateConfig 삭제, Elasticsearch 리포지토리 스캔 범위 확장, TMap/Kric 설정 적용 등
템플릿(Null 안전성/표현 개선)
src/main/resources/templates/admin/user/users.html, .../user-detail.html
사용자 타입/로그인타입/성별 등 null 체크 추가, 미설정 시 기본값/스타일 적용, 잠재적 NPE 방지 등
기타/사소한 변경
src/main/java/com/wayble/server/auth/entity/RefreshToken.java, .../common/client/tmap/*, .../direction/service/EdgeService.java
DB 컬럼명 명시, 패키지 이동, 불필요/중복 클래스 삭제 등
Elasticsearch 설정/매핑 파일
src/main/resources/elasticsearch/settings/direction_mappings.json, .../direction_settings.json
한국어 분석기/edge-ngram 기반 커스텀 analyzer 및 매핑 파일 신규 추가

Sequence Diagram(s)

1. 장소 저장 및 Elasticsearch 인덱싱

sequenceDiagram
    participant Admin as AdminController
    participant DirSvc as DirectionService
    participant JPA as PlaceRepository
    participant ESRepo as DirectionElasticsearchRepository
    participant ES as Elasticsearch

    Admin->>DirSvc: savePlaceAndIndexDocument(requests)
    DirSvc->>JPA: saveAll(Place)
    JPA-->>DirSvc: List<Place>
    DirSvc->>ESRepo: saveAll(DirectionDocument)
    ESRepo->>ES: Bulk Index
    ES-->>ESRepo: Ack/Fail
    ESRepo-->>DirSvc: Result
    DirSvc-->>Admin: 완료/예외
Loading

2. 이미지 업로드 및 S3 저장

sequenceDiagram
    participant AdminPage as AdminWaybleZoneCreate/Edit.html
    participant FileCtrl as FileUploadController
    participant S3Mgr as AmazonS3Manager
    participant S3 as AWS S3

    AdminPage->>FileCtrl: POST /api/v1/files/images (MultipartFile)
    FileCtrl->>S3Mgr: uploadFile(file)
    S3Mgr->>S3: putObject(file)
    S3-->>S3Mgr: Success/Fail
    S3Mgr-->>FileCtrl: 이미지 URL/예외
    FileCtrl-->>AdminPage: CommonResponse<이미지 URL>
Loading

3. 사용자 행동 로그 비동기 저장

sequenceDiagram
    participant Service as UserService/AuthService/KakaoLoginService
    participant LogSvc as UserActionLogService
    participant ESRepo as UserActionLogRepository
    participant ES as Elasticsearch

    Service->>LogSvc: logUserRegister/logTokenRefresh(...)
    LogSvc->>ESRepo: save(UserActionLog) (비동기)
    ESRepo->>ES: Index
    ES-->>ESRepo: Ack
    ESRepo-->>LogSvc: 완료
    LogSvc-->>Service: CompletableFuture<Void>
Loading

4. 예외 발생 시 Discord Webhook 알림

sequenceDiagram
    participant App as Application
    participant Handler as GlobalExceptionHandler
    participant Discord as Discord Webhook

    App->>Handler: Exception 발생
    Handler->>Discord: POST Webhook Payload
    Discord-->>Handler: 2xx/4xx 응답
    Handler-->>App: 예외 응답 반환
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

Suggested labels

⚙️ chore, 💡 feature, 🛠️ fix

Suggested reviewers

  • seung-in-Yoo
  • zyovn
  • hyoinYang
  • wonjun-lee-fcwj245

Poem

🐇
새벽 풀밭에 토끼가 춤을 추네,
길찾기, 이미지, 로그까지 모두 새로워졌네!
S3에 사진 척척,
엘라스틱엔 기록 쏙쏙,
대시보드엔 숫자 반짝,
예외 땐 디스코드로 깡총!
오늘도 개발밭엔 변화의 바람이 솔솔~
🗺️📈🖼️

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 24

🔭 Outside diff range comments (1)
src/main/java/com/wayble/server/common/client/tmap/TMapClient.java (1)

17-26: WebClient .block() 사용으로 인한 쓰레드 블로킹

현재 response 메서드가 즉시 .block() 으로 응답을 동기화하여 Netty 이벤트루프 쓰레드를 불필요하게 막아둘 수 있습니다.
리액티브 스택을 택했다면 호출부에서 Mono<TMapResponse> 를 반환-구독하도록 설계하거나, 반드시 블로킹이 필요하다면 전용 Scheduler로 이동하는 것이 안전합니다.

-    public TMapResponse response(TMapRequest request) {
-        return tMapWebClient.post()
+    public Mono<TMapResponse> response(TMapRequest request) {
+        return tMapWebClient.post()
                 ...
-                .bodyToMono(TMapResponse.class)
-                .block();
+                .bodyToMono(TMapResponse.class);
 }

또한 타임아웃·재시도 정책을 명시하지 않아 외부 API 지연 시 무한 대기가 발생할 수 있습니다.
WebClient 빌더 단계에서 responseTimeout, retryBackoff 등을 설정해 주세요.

🧹 Nitpick comments (24)
src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java (1)

64-100: sendToDiscord 메소드 구현 우수, 보안 고려사항 제안

메소드가 전반적으로 잘 구현되었습니다. 프로파일 체크, 예외 처리, 로깅이 모두 적절합니다.

보안 측면에서 고려해볼 점:

 DiscordWebhookPayload.Embed embed = new DiscordWebhookPayload.Embed(
         "🚨 서버 에러 발생",
-        "```" + ex.getMessage() + "```",
+        "```" + sanitizeErrorMessage(ex.getMessage()) + "```",
         timestamp,

에러 메시지에 민감한 정보(DB 연결 정보, API 키 등)가 포함될 수 있으므로 sanitization을 고려해보세요.

src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawItem.java (1)

3-3: 불필요한 Lombok import 제거 필요

record 는 기본적으로 모든 필드의 접근자 메서드를 생성하므로 lombok.Getter 가 필요 없습니다. 사용되지 않는 import 는 컴파일 경고나 정적 분석 도구에서 지적될 수 있으므로 삭제하는 편이 좋습니다.

- import lombok.Getter;
src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawResponse.java (1)

3-3: 사용되지 않는 java.util.List import 삭제 권장

이 파일에서는 List 타입을 사용하지 않으므로 import 문이 불필요합니다. 정적 분석 경고를 방지하기 위해 삭제해 주세요.

- import java.util.List;
src/main/java/com/wayble/server/direction/repository/PlaceRepository.java (1)

6-7: @repository 애노테이션 명시 여부 검토 필요

Spring Data JPA가 인터페이스를 자동으로 빈으로 등록해 주긴 하지만, 팀 내 다른 JPA 레포지토리들처럼 @Repository 애노테이션을 명시해 일관성을 맞추는 편이 가독성과 IDE-메타데이터 지원에 유리합니다.
필요 시 다음과 같이 추가를 고려해 주세요.

+import org.springframework.stereotype.Repository;
+
-public interface PlaceRepository extends JpaRepository<Place, Long> {
+@Repository
+public interface PlaceRepository extends JpaRepository<Place, Long> {
src/main/java/com/wayble/server/direction/controller/swagger/WalkingSwagger.java (1)

12-31: Swagger 파라미터 메타데이터 누락

@RequestParam만 사용하면 Swagger UI에 각 파라미터의 설명·예시가 노출되지 않아 API 문서화 수준이 떨어집니다.
@Parameter(or @Schema) 애노테이션을 활용해 의미, 단위, 예시 값을 추가하면 프론트엔드/외부 소비자가 훨씬 이해하기 쉽습니다.

예시:

@Parameter(description = "출발지 경도", example = "126.9784")
@RequestParam double startX,
src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (1)

5-5: 사용되지 않는 import 정리

List import가 추가되었지만 실제로 사용되지 않고 있습니다.

-import java.util.List;
src/main/java/com/wayble/server/direction/dto/request/DirectionSearchRequest.java (1)

7-10: 입력값 검증 애노테이션 추가를 고려해보세요.

좌표값의 유효성 검증을 위해 validation 애노테이션을 추가하는 것이 좋겠습니다.

+import jakarta.validation.constraints.DecimalMax;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotBlank;
+
 @Builder
 public record DirectionSearchRequest(
+        @NotBlank(message = "장소명은 필수입니다")
         String name,
+        @DecimalMin(value = "-90.0", message = "위도는 -90도 이상이어야 합니다")
+        @DecimalMax(value = "90.0", message = "위도는 90도 이하여야 합니다")
         Double latitude,
+        @DecimalMin(value = "-180.0", message = "경도는 -180도 이상이어야 합니다")
+        @DecimalMax(value = "180.0", message = "경도는 180도 이하여야 합니다")
         Double longitude
 ) {
src/main/java/com/wayble/server/auth/service/AuthService.java (1)

47-51: 로그인 시 활성 사용자 로깅 추가

로그인 성공 후 토큰 갱신 로그를 저장하는 기능이 추가되었습니다. user.getUserType() null 체크도 적절히 처리되어 있습니다.

주석에서 "첫 로그인시에도"라고 명시되어 있는데, 실제로는 모든 로그인에서 호출되는 것으로 보입니다. 주석을 더 정확하게 수정하는 것이 좋겠습니다.

-        // 첫 로그인시에도 활성 유저 로그 저장 (비동기, 하루 1회만)
+        // 로그인 시 활성 유저 로그 저장 (비동기, 하루 1회만)
src/main/resources/elasticsearch/settings/direction_settings.json (1)

1-36: 한국어 텍스트 분석을 위한 훌륭한 Elasticsearch 설정입니다.

nori_tokenizer 사용과 mixed decompound 모드 설정이 한국어 복합어 처리에 적절합니다. 인덱싱용과 검색용 analyzer를 분리한 것도 좋은 패턴입니다.

하지만 edge n-gram의 파라미터를 검증해보시기 바랍니다:

  • min_gram: 2가 한국어 검색에 적절한지
  • max_gram: 20이 너무 크지 않은지 (인덱스 크기와 성능 고려)
src/main/java/com/wayble/server/logging/config/LoggingAsyncConfig.java (1)

10-26: 로깅을 위한 비동기 설정이 잘 구성되었습니다.

ThreadPoolTaskExecutor 설정이 적절합니다:

  • 로깅 작업에 맞는 적절한 스레드 풀 크기
  • 버스트 처리를 위한 충분한 큐 용량
  • 디버깅을 위한 명확한 스레드 명명
  • 우아한 종료 설정

비동기 로깅 작업의 오류 처리 방안도 고려해보시기 바랍니다:

@Bean("loggingTaskExecutor")
public Executor loggingTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(4);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("logging-");
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.setAwaitTerminationSeconds(30);
    // 오류 처리를 위한 RejectedExecutionHandler 고려
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
}
src/main/java/com/wayble/server/direction/entity/DirectionDocument.java (1)

32-37: 생성자 접근 제한자 일관성 개선

클래스 레벨에서는 PROTECTED 접근 제한자를 사용했는데, Builder 생성자는 private로 설정되어 일관성이 없습니다.

Builder 생성자도 protected로 통일하거나, 기본 생성자를 private으로 변경하여 일관성을 맞추세요:

-@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)

또는

 @Builder
-private DirectionDocument(Long id, String name, EsAddress esAddress) {
+protected DirectionDocument(Long id, String name, EsAddress esAddress) {
src/main/java/com/wayble/server/direction/dto/response/DirectionSearchResponse.java (1)

7-24: record 클래스에서 @builder 사용을 재검토하세요.

record 클래스는 이미 모든 필드를 받는 생성자를 자동 생성하므로 @builder 어노테이션이 불필요할 수 있습니다. record의 간결성을 유지하기 위해 @builder 제거를 고려해보세요.

-@Builder
-@Schema(description = "지도 연관어 자동완성 API")
-public record DirectionSearchResponse(
+@Schema(description = "지도 연관어 자동완성 API")
+public record DirectionSearchResponse(
src/main/java/com/wayble/server/direction/entity/Place.java (1)

26-37: 불필요한 중복을 제거하고 일관성을 유지하세요.

Builder 생성자와 static factory method of()가 동일한 기능을 제공합니다. 코드의 일관성을 위해 하나의 방식으로 통일하는 것을 권장합니다.

-    public static Place of(String name, Address address) {
-        return Place.builder()
-                .name(name)
-                .address(address)
-                .build();
-    }

또는 Builder 패턴을 제거하고 static factory method만 유지하는 것도 고려해보세요.

src/main/java/com/wayble/server/direction/repository/DirectionElasticSearchCustomRepositoryImpl.java (2)

22-36: 검색 기능을 개선하고 로깅을 추가하세요.

현재 구현은 기본적인 기능은 충족하지만, 더 나은 사용자 경험을 위해 개선할 수 있습니다.

 @Override
 public List<DirectionSearchResponse> searchDirection(DirectionSearchRequest request) {
-    if (request.name() == null || request.name().trim().isEmpty()) return List.of();
+    if (request.name() == null || request.name().trim().isEmpty()) {
+        return List.of();
+    }
     
-    Criteria criteria = new Criteria("name").matches(request.name());
+    String searchTerm = request.name().trim().toLowerCase();
+    Criteria criteria = new Criteria("name").matches(searchTerm);

     CriteriaQuery query = new CriteriaQuery(criteria);
     query.setMaxResults(5);

-    return elasticsearchOperations
+    List<DirectionSearchResponse> results = elasticsearchOperations
             .search(query, DirectionDocument.class)
             .map(SearchHit::getContent)
             .map(DirectionSearchResponse::from)
             .stream()
             .toList();
+    
+    log.debug("방향 검색 완료 - 검색어: '{}', 결과 수: {}", searchTerm, results.size());
+    return results;
 }

또한 클래스 상단에 로깅을 위한 어노테이션을 추가하세요:

+import lombok.extern.slf4j.Slf4j;

+@Slf4j
 @Repository
 @RequiredArgsConstructor
 public class DirectionElasticSearchCustomRepositoryImpl implements DirectionElasticSearchCustomRepository{

25-25: 더 나은 검색 정확도를 위한 Elasticsearch 쿼리 개선을 고려하세요.

현재 단순한 matches() 쿼리를 사용하고 있지만, 더 정확한 검색을 위해 fuzzy search나 multi-field 검색을 고려해보세요.

검색 요구사항에 따라 다음과 같은 개선을 고려할 수 있습니다:

// 퍼지 검색을 위한 예시
Criteria criteria = new Criteria("name")
    .matches(searchTerm)
    .fuzzy(Fuzziness.AUTO);

또는 부분 매칭을 위해:

// 부분 매칭을 위한 예시  
Criteria criteria = new Criteria("name").contains(searchTerm);
src/main/java/com/wayble/server/aws/AwsErrorCase.java (1)

11-12: HTTP 상태 코드 재검토 권장

이미지 업로드/삭제 오류에 대해 모두 400 (Bad Request)을 사용하고 있습니다. 실제 오류 상황에 따라 더 구체적인 상태 코드를 고려해보세요:

  • 서버 내부 오류: 500 (Internal Server Error)
  • 권한 관련 오류: 403 (Forbidden)
  • 파일을 찾을 수 없는 경우: 404 (Not Found)
-    IMAGE_UPLOAD_ERROR(400, 8001, "이미지 업로드 과정에서 문제가 발생했습니다."),
-    IMAGE_DELETE_ERROR(400, 8002, "이미지 삭제 과정에서 문제가 발생했습니다.");
+    IMAGE_UPLOAD_ERROR(500, 8001, "이미지 업로드 과정에서 문제가 발생했습니다."),
+    IMAGE_DELETE_ERROR(500, 8002, "이미지 삭제 과정에서 문제가 발생했습니다.");
src/main/java/com/wayble/server/direction/controller/DirectionController.java (1)

23-26: 더 구체적인 응답 메시지 고려

장소 저장 결과에 대해 더 구체적인 정보를 제공하는 것을 고려해보세요. 예를 들어, 저장된 장소의 개수나 처리 결과 요약 등을 포함할 수 있습니다.

src/main/java/com/wayble/server/direction/controller/swagger/DirectionSwagger.java (1)

32-44: API 문서화 개선 제안

키워드 파라미터에 대한 설명과 추가적인 응답 코드 문서화를 고려해보세요.

     CommonResponse<List<DirectionSearchResponse>> getDirectionKeywords(
-            @RequestParam String keyword
+            @RequestParam(value = "keyword", required = true) 
+            @Parameter(description = "검색할 키워드 (최소 1글자 이상)", example = "강남역")
+            String keyword
     );

추가로 오류 응답 코드도 문서화할 수 있습니다:

     @ApiResponses(value = {
             @ApiResponse(
                     responseCode = "200",
                     description = "길찾기 검색 연관어 자동완성이 성공적으로 실행되었습니다."
-            )
+            ),
+            @ApiResponse(
+                    responseCode = "400",
+                    description = "잘못된 키워드 형식입니다."
+            )
     })

@Parameter import도 추가해주세요:

+import io.swagger.v3.oas.annotations.Parameter;
src/main/java/com/wayble/server/logging/repository/UserActionLogRepository.java (1)

21-21: 성능 최적화 권장

countDistinctUserIdByActionAndTimestampBetween 메서드는 대용량 데이터셋에서 성능 문제를 야기할 수 있습니다. Elasticsearch의 cardinality aggregation을 사용하는 커스텀 구현을 고려해보세요.

필요시 다음과 같은 커스텀 리포지토리 인터페이스를 추가할 수 있습니다:

public interface UserActionLogCustomRepository {
    long countDistinctUsersByActionAndTimestampRange(String action, LocalDateTime start, LocalDateTime end);
}
src/main/java/com/wayble/server/direction/service/DirectionService.java (1)

61-67: 검색어 유효성 검증 추가 권장

검색어에 대한 기본적인 유효성 검증을 추가하면 더 안정적인 서비스가 됩니다.

 public List<DirectionSearchResponse> searchDirection(String keyword) {
+    if (keyword == null || keyword.trim().isEmpty()) {
+        return List.of();
+    }
+    
     DirectionSearchRequest request = DirectionSearchRequest.builder()
-            .name(keyword)
+            .name(keyword.trim())
             .build();
 
     return directionElasticsearchRepository.searchDirection(request);
 }
src/main/java/com/wayble/server/aws/AmazonS3Manager.java (1)

34-50: 파일 업로드 검증 및 로깅 개선

파일 크기와 타입 검증을 추가하고, 로깅 메시지를 개선하는 것이 좋습니다.

 public String uploadFile(MultipartFile file){
+    // 파일 검증
+    if (file.isEmpty()) {
+        throw new ApplicationException(AwsErrorCase.EMPTY_FILE);
+    }
+    
     String fileName = createFileName(file);
     String fileUrl = getFileUrl(fileName);
 
     ObjectMetadata objectMetadata = new ObjectMetadata();
     objectMetadata.setContentType(file.getContentType());
     objectMetadata.setContentLength(file.getSize());
 
     try {
         amazonS3.putObject(bucket, fileName, file.getInputStream(), objectMetadata);
     } catch (IOException e) {
+        log.error("S3 파일 업로드 실패 - 파일명: {}, 오류: {}", fileName, e.getMessage(), e);
         throw new ApplicationException(AwsErrorCase.IMAGE_UPLOAD_ERROR);
     }
 
-    log.info("file url : " + fileUrl + " has upload to S3");
+    log.info("S3 파일 업로드 성공 - URL: {}, 크기: {} bytes", fileUrl, file.getSize());
     return fileUrl;
 }
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)

79-92: WebClient 블로킹 호출 개선 권장

WebClient를 사용하면서 block()을 호출하는 것은 반응형 프로그래밍의 이점을 없앱니다. 또한 외부 API 호출 시 타임아웃 설정이 필요합니다.

 try{
     KricToiletRawResponse response = kricWebClient
             .get()
             .uri(uri)
             .retrieve()
             .bodyToMono(KricToiletRawResponse.class)
-            .block();
+            .block(Duration.ofSeconds(5)); // 5초 타임아웃 설정

     items = response.body().item();

 } catch(Exception e){
-    log.info("역사 화장실 api 호출 중 에러 발생: {}: {}", uri, e.getCause());
+    log.warn("역사 화장실 API 호출 실패 - URI: {}, 오류: {}", uri, e.getMessage());
     return new HashMap<>();
 }

필요한 import 추가:

import java.time.Duration;
src/main/resources/templates/admin/wayblezone/wayble-zone-edit.html (1)

573-581: 드래그 앤 드롭 시 다중 파일 처리

사용자가 여러 파일을 드롭하는 경우를 처리해야 합니다.

 function handleDrop(e) {
     const dt = e.dataTransfer;
     const files = dt.files;
     
     if (files.length > 0) {
+        if (files.length > 1) {
+            alert('한 번에 하나의 이미지만 업로드할 수 있습니다.');
+            return;
+        }
         document.getElementById('imageUpload').files = files;
         handleImageUpload({ target: { files } });
     }
 }
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (1)

52-58: 생성자 시그니처 업데이트 승인, 파라미터 순서 개선 제안

DirectionType nodeType 파라미터 추가는 적절하나, 파라미터 순서를 필드 선언 순서에 맞게 조정하는 것을 고려해보세요.

필드 선언 순서에 맞춘 생성자 순서로 개선:

-public Node(Long id, String stationName, DirectionType nodeType, double latitude, double longitude) {
+public Node(Long id, String stationName, double latitude, double longitude, DirectionType nodeType) {
     this.id = id;
     this.stationName = stationName;
-    this.nodeType = nodeType;
     this.latitude = latitude;
     this.longitude = longitude;
+    this.nodeType = nodeType;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 381434e and 9f7c1e4.

📒 Files selected for processing (67)
  • .github/workflows/cd-develop.yml (2 hunks)
  • src/main/java/com/wayble/server/ServerApplication.java (2 hunks)
  • src/main/java/com/wayble/server/admin/controller/AdminController.java (4 hunks)
  • src/main/java/com/wayble/server/admin/dto/DailyStatsDto.java (1 hunks)
  • src/main/java/com/wayble/server/admin/service/AdminSystemService.java (3 hunks)
  • src/main/java/com/wayble/server/admin/service/AdminUserService.java (3 hunks)
  • src/main/java/com/wayble/server/admin/service/AdminWaybleZoneService.java (4 hunks)
  • src/main/java/com/wayble/server/auth/entity/RefreshToken.java (1 hunks)
  • src/main/java/com/wayble/server/auth/service/AuthService.java (4 hunks)
  • src/main/java/com/wayble/server/auth/service/KakaoLoginService.java (3 hunks)
  • src/main/java/com/wayble/server/aws/AmazonS3Manager.java (2 hunks)
  • src/main/java/com/wayble/server/aws/AwsErrorCase.java (1 hunks)
  • src/main/java/com/wayble/server/aws/controller/FileUploadController.java (1 hunks)
  • src/main/java/com/wayble/server/common/client/tmap/TMapClient.java (1 hunks)
  • src/main/java/com/wayble/server/common/client/tmap/TMapProperties.java (1 hunks)
  • src/main/java/com/wayble/server/common/client/tmap/dto/request/TMapRequest.java (1 hunks)
  • src/main/java/com/wayble/server/common/client/tmap/dto/response/TMapParsingResponse.java (1 hunks)
  • src/main/java/com/wayble/server/common/client/tmap/dto/response/TMapResponse.java (1 hunks)
  • src/main/java/com/wayble/server/common/client/tmap/mapper/TMapMapper.java (1 hunks)
  • src/main/java/com/wayble/server/common/config/RestTemplateConfig.java (0 hunks)
  • src/main/java/com/wayble/server/common/config/SecurityConfig.java (1 hunks)
  • src/main/java/com/wayble/server/common/config/WebClientConfig.java (3 hunks)
  • src/main/java/com/wayble/server/common/dto/DiscordWebhookPayload.java (1 hunks)
  • src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java (3 hunks)
  • src/main/java/com/wayble/server/direction/controller/DirectionController.java (1 hunks)
  • src/main/java/com/wayble/server/direction/controller/TransportationController.java (2 hunks)
  • src/main/java/com/wayble/server/direction/controller/WalkingController.java (1 hunks)
  • src/main/java/com/wayble/server/direction/controller/swagger/DirectionSwagger.java (1 hunks)
  • src/main/java/com/wayble/server/direction/controller/swagger/WalkingSwagger.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java (3 hunks)
  • src/main/java/com/wayble/server/direction/dto/request/DirectionSearchRequest.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/request/PlaceSaveRequest.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/response/DirectionSearchResponse.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/DirectionDocument.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/Place.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (2 hunks)
  • src/main/java/com/wayble/server/direction/entity/transportation/Node.java (1 hunks)
  • src/main/java/com/wayble/server/direction/exception/DirectionErrorCase.java (1 hunks)
  • src/main/java/com/wayble/server/direction/external/kric/KricProperties.java (1 hunks)
  • src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawBody.java (1 hunks)
  • src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawItem.java (1 hunks)
  • src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawResponse.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/DirectionElasticSearchCustomRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/DirectionElasticSearchCustomRepositoryImpl.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/DirectionElasticsearchRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/PlaceRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/DirectionService.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/EdgeService.java (0 hunks)
  • src/main/java/com/wayble/server/direction/service/FacilityService.java (3 hunks)
  • src/main/java/com/wayble/server/direction/service/TransportationService.java (9 hunks)
  • src/main/java/com/wayble/server/direction/service/WalkingService.java (1 hunks)
  • src/main/java/com/wayble/server/logging/config/LoggingAsyncConfig.java (1 hunks)
  • src/main/java/com/wayble/server/logging/entity/UserActionLog.java (1 hunks)
  • src/main/java/com/wayble/server/logging/repository/UserActionLogRepository.java (1 hunks)
  • src/main/java/com/wayble/server/logging/service/UserActionLogService.java (1 hunks)
  • src/main/java/com/wayble/server/user/service/UserInfoService.java (1 hunks)
  • src/main/java/com/wayble/server/user/service/UserService.java (3 hunks)
  • src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java (1 hunks)
  • src/main/resources/elasticsearch/settings/direction_mappings.json (1 hunks)
  • src/main/resources/elasticsearch/settings/direction_settings.json (1 hunks)
  • src/main/resources/templates/admin/dashboard.html (3 hunks)
  • src/main/resources/templates/admin/user/user-detail.html (2 hunks)
  • src/main/resources/templates/admin/user/users.html (2 hunks)
  • src/main/resources/templates/admin/wayblezone/wayble-zone-create.html (3 hunks)
  • src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html (1 hunks)
  • src/main/resources/templates/admin/wayblezone/wayble-zone-edit.html (3 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/com/wayble/server/common/config/RestTemplateConfig.java
  • src/main/java/com/wayble/server/direction/service/EdgeService.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: java에서 같은 패키지 내의 클래스들은 import 구문 없이 서로를 참조할 수 있다. com.wayble.server.user.entity 패키지 내의 클래스들은 명시적인 im...
Learnt from: seung-in-Yoo
PR: Wayble-Project/wayble-spring#37
File: src/main/java/com/wayble/server/user/entity/User.java:61-62
Timestamp: 2025-07-13T15:08:59.318Z
Learning: Java에서 같은 패키지 내의 클래스들은 import 구문 없이 서로를 참조할 수 있다. com.wayble.server.user.entity 패키지 내의 클래스들은 명시적인 import 없이 사용 가능하다.

Applied to files:

  • src/main/java/com/wayble/server/logging/entity/UserActionLog.java
  • src/main/java/com/wayble/server/direction/entity/Place.java
  • src/main/java/com/wayble/server/ServerApplication.java
🧬 Code Graph Analysis (4)
src/main/java/com/wayble/server/direction/dto/request/DirectionSearchRequest.java (1)
src/main/java/com/wayble/server/common/response/CommonResponse.java (1)
  • Builder (8-42)
src/main/java/com/wayble/server/direction/entity/Place.java (1)
src/main/java/com/wayble/server/direction/entity/DirectionDocument.java (1)
  • Getter (11-46)
src/main/java/com/wayble/server/direction/dto/response/DirectionSearchResponse.java (1)
src/main/java/com/wayble/server/common/response/CommonResponse.java (1)
  • Builder (8-42)
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)
  • Service (23-109)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-docker-image-on-ec2
🔇 Additional comments (80)
.github/workflows/cd-develop.yml (2)

5-8: 변경사항 승인

브랜치명 변경은 문제없습니다. 주석처리되어 있어 실제 워크플로우 동작에는 영향을 주지 않습니다.


192-192: Discord 알림 메시지 간소화 승인

브랜치 정보를 제거하여 메시지가 더 간결해졌습니다. 좋은 개선사항입니다.

src/main/java/com/wayble/server/common/dto/DiscordWebhookPayload.java (1)

5-17: Discord 웹훅 페이로드 구조 설계 우수

Discord 웹훅 API의 표준 구조를 정확히 모델링했습니다. record 사용도 불변 데이터 전송 객체로 적절합니다. 중첩 구조가 깔끔하게 정의되어 있습니다.

src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java (3)

4-4: 새로운 import들 승인

Discord 웹훅 기능을 위한 필요한 import들이 모두 적절하게 추가되었습니다.

Also applies to: 7-11, 18-18, 21-22


44-44: 예외 핸들러에 Discord 알림 추가 승인

예외 발생 시 Discord로 알림을 보내는 기능이 잘 추가되었습니다. 개발 단계에서 에러 모니터링에 유용할 것입니다.

Also applies to: 58-58


33-34: Discord 웹훅 URL 보안 설정 검증 요청

코드에서 @Value("${discord.webhook-url}")로 민감한 웹훅 URL을 주입하고 있으나, 레포지토리 내 application.yml 또는 application.properties에서 해당 설정이 발견되지 않습니다. 운영 환경에서 아래 내용을 반드시 확인해주세요.

  • 레포지토리 내에 하드코딩된 웹훅 URL이 존재하지 않는지 재확인
  • 프로덕션에서는 환경 변수 또는 시크릿 매니저(e.g. Spring Cloud Config, HashiCorp Vault)로만 관리되고 있는지 검증
  • CI/CD 설정 혹은 클라우드 시크릿 스토어에 올바르게 등록·조회되고 있는지 확인

위 사항들을 검토하고, 안전하게 비밀 값을 관리하고 있는지 회신 부탁드립니다.

src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawBody.java (1)

5-7: 패키지 이동 및 제네릭 선언 정상 확인

List<KricToiletRawItem> 제네릭 선언이 올바르게 유지되었으며, 패키지 이동도 문제없이 반영되었습니다.

src/main/java/com/wayble/server/common/client/tmap/dto/request/TMapRequest.java (1)

1-1: 패키지 구조 리팩터링 확인

common.client.tmap 패키지로 이동이 일관되게 적용되었습니다. 추가 수정 사항은 없어 보입니다.

src/main/java/com/wayble/server/common/client/tmap/dto/response/TMapResponse.java (1)

1-1: 패키지 변경사항 이상 없음

구조적 변경만 이루어졌으며, 중첩 record 정의들도 정상적으로 유지되었습니다.

src/main/java/com/wayble/server/direction/controller/WalkingController.java (1)

6-7: 패키지 리팩토링이 적절하게 적용되었습니다.

TMap 관련 클래스들을 common.client.tmap 패키지로 이동시킨 것은 외부 클라이언트 통합을 중앙화하는 좋은 리팩토링입니다. 코드 재사용성과 모듈화가 개선되었습니다.

src/main/java/com/wayble/server/common/client/tmap/TMapProperties.java (1)

1-1: 패키지 구조 개선이 일관되게 적용되었습니다.

설정 클래스를 공통 클라이언트 패키지로 이동한 것은 외부 API 클라이언트 관리를 중앙화하는 적절한 설계입니다.

src/main/java/com/wayble/server/common/client/tmap/dto/response/TMapParsingResponse.java (1)

1-1: DTO 클래스의 패키지 이동이 일관성 있게 처리되었습니다.

응답 DTO를 공통 클라이언트 패키지로 이동한 것은 여러 서비스에서 재사용할 수 있도록 하는 좋은 구조 개선입니다.

src/main/java/com/wayble/server/direction/service/WalkingService.java (1)

7-11: 서비스 레이어의 패키지 리팩토링이 완료되었습니다.

TMap 클라이언트 관련 import들이 공통 패키지로 일관되게 이동되어 코드 구조가 개선되었습니다. 외부 API 클라이언트의 중앙 관리가 가능해졌습니다.

src/main/java/com/wayble/server/common/client/tmap/mapper/TMapMapper.java (1)

21-84: Null 가능 필드 접근 시 NPE 위험

properties.name() 등이 null 일 경우 그대로 Step 생성자에 전달되어 이후 서비스-레이어에서 NPE가 발생할 수 있습니다.
DTO 특성상 외부 API 포맷이 바뀌면 null 이 올 확률이 높으므로, Objects.requireNonNullElse 또는 기본값을 지정해 방어 코드를 추가하는 편이 좋습니다.

-STEP_TYPE_POINT,
-properties.name(),
+STEP_TYPE_POINT,
+Objects.requireNonNullElse(properties.name(), ""),

유사 필드 모두 검토 바랍니다.

src/main/java/com/wayble/server/auth/entity/RefreshToken.java (1)

18-19: 명시적 컬럼명 지정 👍

user_id 컬럼명을 명시해 스키마 가독성이 개선되었습니다. unique = true 속성으로 인덱스도 자동 생성되므로 추가 조치 없이 충분합니다.

src/main/java/com/wayble/server/direction/external/kric/KricProperties.java (1)

1-10: 설정 파일에 KRIC API 프로퍼티 누락 여부 확인 필요

현재 src/main/resources 하위의 application.yml 또는 application.properties 파일에서
kric.api.keykric.api.baseUrl 항목이 검색되지 않습니다.
설정 파일에 아래 키들이 올바르게 정의되어 있는지 직접 확인하거나 추가해주세요.

  • kric.api.key
  • kric.api.baseUrl
src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html (1)

97-107: LGTM! 훌륭한 UI 구현

대표 이미지 섹션이 잘 구현되었습니다:

  • 조건부 렌더링으로 이미지가 없는 경우 적절히 처리
  • 접근성을 위한 동적 alt 속성 설정
  • 일관된 스타일링 적용 (max-height, object-cover 등)
  • 기존 UI와 조화로운 디자인
src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java (1)

178-178: LGTM! 올바른 데이터 바인딩

DTO에서 mainImageUrl 값을 올바르게 설정하도록 수정되었습니다. 이는 관리자 페이지에서 설정한 대표 이미지가 엔티티에 제대로 반영되도록 보장합니다.

src/main/java/com/wayble/server/common/config/SecurityConfig.java (1)

53-54: /api/v1/directions/** 제거, /api/v1/files/` 추가된 보안 설정 점검 필요**

SecurityConfig.java (53–54줄):

-               "/api/v1/directions/**",
+               "/api/v1/files/**"
  • FileUploadController에서 /api/v1/files 엔드포인트가
    @RestController + @RequestMapping("/api/v1/files")로 구현되어 있어 public 접근이 정상입니다.
  • 기존에 public이던 directions API(@GetMapping("/api/v1/directions/**"))가 더 이상 permitAll 대상이 아닙니다.
    해당 컨트롤러가 여전히 존재하는지, 인증 요구 변경이 의도된 것인지 확인해주세요.
  • 이로 인해 인증 없이 호출하던 기존 클라이언트에 영향이 있는지,
    API 문서·테스트·프론트엔드 코드에 반영됐는지 검토가 필요합니다.
src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (1)

11-11: LGTM! 적절한 쿼리 메서드 추가

nodeId로 Facility를 조회하는 메서드가 올바르게 추가되었습니다. JPA 메서드 네이밍 컨벤션을 잘 따르고 있고, Optional 반환 타입도 적절합니다.

src/main/java/com/wayble/server/direction/dto/request/DirectionSearchRequest.java (1)

1-11: 레코드 구조가 간결하고 적절합니다.

길찾기 검색 요청을 위한 DTO로서 필요한 필드들을 잘 정의했습니다. 빌더 패턴 지원도 좋습니다.

src/main/java/com/wayble/server/direction/repository/DirectionElasticSearchCustomRepository.java (1)

8-10: 깔끔한 인터페이스 설계입니다.

단일 메서드로 명확한 책임을 가지며, 메서드명과 파라미터가 직관적입니다.

src/main/resources/templates/admin/user/user-detail.html (4)

70-71: null 안전성 개선이 잘 되었습니다.

userType에 대한 null 체크를 추가하여 NPE를 방지하고, "미설정" 기본값을 제공하는 것이 좋습니다.


74-75: 로그인 타입 null 처리가 적절합니다.

loginType에 대해서도 일관성 있게 null 체크를 적용했습니다.


116-116: 상세 정보 영역의 null 처리가 일관됩니다.

표시 영역에서도 동일하게 null 체크를 적용하여 일관성을 유지했습니다.


120-120: 사용자 타입 표시의 null 안전성이 확보되었습니다.

모든 userType 참조에 null 체크가 적용되어 안전합니다.

src/main/resources/templates/admin/user/users.html (3)

102-103: 사용자 타입 null 처리가 올바릅니다.

목록에서도 userType에 대한 null 체크를 추가하여 안전성을 확보했습니다. 상세 페이지와 동일한 패턴을 사용하여 일관성도 좋습니다.


106-107: 로그인 타입 null 안전성이 잘 구현되었습니다.

loginType에 대해서도 일관된 null 체크와 기본값 처리를 적용했습니다.


131-131: 성별 정보의 null 처리가 적절합니다.

gender 필드에도 null 체크를 추가하여 완전한 null 안전성을 제공합니다.

src/main/java/com/wayble/server/admin/dto/DailyStatsDto.java (1)

7-15: 일일 통계를 위한 깔끔한 DTO 설계입니다.

필드명이 명확하고 적절한 타입을 사용했습니다. 불변 레코드로서 통계 데이터를 안전하게 전달할 수 있습니다.

src/main/java/com/wayble/server/admin/service/AdminUserService.java (4)

168-169: null safety 개선이 잘 적용되었습니다.

LoginType과 UserType enum 변환 시 null 체크를 추가하여 NullPointerException을 방지했습니다. 데이터베이스에서 null 값이 올 수 있는 상황을 적절히 처리하고 있습니다.


184-185: 일관된 null safety 처리

convertToDetailDto 메소드에서도 동일한 null safety 패턴이 적용되어 일관성을 유지하고 있습니다.


206-207: 전체 메소드에 일관된 null safety 적용

convertToDetailDtoWithStats 메소드에서도 같은 패턴으로 null safety가 적용되어 세 개의 변환 메소드 모두에서 일관된 처리가 이루어지고 있습니다.


168-169: LGTM! Null 안전성 개선

LoginType과 UserType enum 변환 시 null 체크를 추가하여 NPE를 방지하는 좋은 개선사항입니다. 세 개의 변환 메소드 모두에 일관되게 적용되어 있어 안정성이 향상되었습니다.

Also applies to: 184-185, 206-207

src/main/java/com/wayble/server/direction/entity/transportation/Node.java (1)

52-58: Node 생성자 호출부 모두 5-파라미터 시그니처로 업데이트됨

  • src/main/java/com/wayble/server/direction/service/TransportationService.java:49
  • src/main/java/com/wayble/server/direction/service/TransportationService.java:50

4-파라미터 호출부는 탐지되지 않았으며, 모든 코드가 새로운 시그니처(id, stationName, nodeType, latitude, longitude)를 사용하고 있습니다. Breaking change 대응이 완료되었습니다.

src/main/java/com/wayble/server/auth/service/AuthService.java (5)

5-5: 로깅 서비스 의존성 추가

UserActionLogService를 통한 사용자 액션 로깅이 인증 서비스에도 추가되었습니다.


85-89: 토큰 갱신 로깅 하루 1회 제한 로직 확인

UserActionLogService.logTokenRefresh 메서드 내부에서 startOfDay/endOfDay 기준으로 기존 로그 개수(existingCount)를 조회해 하루에 한 번만 저장하도록 구현되어 있습니다.

확인 위치:

  • src/main/java/com/wayble/server/logging/service/UserActionLogService.java:59–65

이대로 승인합니다.


5-5: LGTM! UserActionLogService 의존성 추가

인증 관련 사용자 액션 로깅을 위한 서비스 의존성 추가가 적절합니다.

Also applies to: 26-26


47-51: LGTM! 로그인 시 활성 유저 로깅

첫 로그인 시에도 활성 유저 로그를 저장하는 것은 사용자 활동 분석 및 보안 감사에 유용합니다. 비동기 처리로 메인 로그인 플로우에 영향을 주지 않습니다.


85-89: LGTM! 토큰 갱신 로깅

토큰 갱신 시 로깅은 보안 감사와 사용자 활동 추적에 중요한 기능입니다. 하루 1회 제한으로 로그 중복을 방지하는 것도 적절합니다.

src/main/java/com/wayble/server/direction/dto/request/PlaceSaveRequest.java (3)

8-11: Java record를 활용한 깔끔한 DTO 구현

Java record를 사용하여 불변 데이터 구조를 잘 구현했습니다. @builder 어노테이션을 통해 빌더 패턴도 지원하고 있어 좋습니다.


13-18: 중첩된 record 구조가 적절합니다.

PlaceDetailRequest를 중첩 record로 구현하여 응집도를 높이고, name과 address 필드로 장소 정보를 명확하게 표현하고 있습니다.


8-18: LGTM! 새로운 장소 저장 요청 DTO

record 기반의 immutable DTO 구조가 잘 설계되었습니다. nested record 사용으로 관련 데이터를 논리적으로 그룹화하고, @builder 어노테이션으로 빌더 패턴을 지원하는 것이 적절합니다.

src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (2)

10-10: 캡슐화 개선이 잘 되었습니다.

Builder를 private으로 변경하여 객체 생성을 더 잘 제어할 수 있게 되었습니다. 이는 좋은 설계 패턴입니다.


38-46: route 필드의 명시적 null 설정 의도를 확인해주세요

저희 검색 결과, 리포지토리 전반에서 route 필드를 설정하거나 참조하는 코드가 발견되지 않았습니다. 해당 필드가 이후에 설정되는지, 아니면 기본적으로 null로 유지할 의도인지 직접 검토가 필요합니다.

  • Edge.createEdge 정적 팩토리 메서드에서 .route(null) 호출
  • .route( 또는 setRoute 검색 시 해당 패턴 외에는 사용 흔적 없음

의도대로라면 주석이나 문서로 명확히 남겨주시고, 그렇지 않다면 불필요한 null 설정 제거 또는 기본값 관리 방안을 반영해 주세요.

src/main/java/com/wayble/server/direction/repository/DirectionElasticsearchRepository.java (1)

6-7: 좋은 리포지토리 인터페이스 설계입니다.

표준 ElasticsearchRepository와 커스텀 리포지토리 인터페이스를 결합하는 것은 CRUD 작업과 커스텀 검색 메서드를 분리하는 좋은 패턴입니다.

src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java (3)

4-4: Spring의 @nullable 사용이 적절합니다.

io.micrometer.common.lang.Nullable에서 org.springframework.lang.Nullable로 변경한 것은 Spring 프로젝트에서 일관성을 위해 좋은 선택입니다.


16-17: @nullable 어노테이션 추가가 좋습니다.

routeName과 information 필드에 @nullable 어노테이션을 추가하여 null 안전성을 명확히 문서화했습니다.


35-35: 오타 수정이 잘 되었습니다.

"Longtitude"를 "Longitude"로 수정한 것은 API 일관성과 정확성을 위해 중요한 수정입니다.

src/main/java/com/wayble/server/common/config/WebClientConfig.java (3)

3-4: 패키지 구조 개선이 잘 반영되었습니다.

TMap 관련 클래스들을 common.client 패키지로 이동하고 새로운 KRIC API 지원을 위한 import가 적절히 추가되었습니다.


15-15: KRIC API 지원을 위한 Properties 주입이 올바르게 구현되었습니다.

기존 TMapProperties와 동일한 패턴으로 KricProperties가 추가되어 일관성이 유지됩니다.


30-35: KRIC WebClient Bean 구현이 적절합니다.

기존 TMap WebClient와 동일한 구조로 구현되어 코드 일관성이 유지되며, baseUrl 설정도 올바르게 적용되었습니다.

src/main/resources/templates/admin/dashboard.html (4)

61-61: 반응형 그리드 레이아웃 개선이 잘 적용되었습니다.

medium 스크린에서 2컬럼, large 스크린에서 4컬럼으로 설정하여 다양한 화면 크기에 더 적합한 레이아웃을 제공합니다.


109-109: 아이콘 색상 변경으로 시각적 구분이 개선되었습니다.

방문자 카드의 아이콘을 purple로 변경하여 다른 통계 카드들과의 시각적 구분이 명확해졌습니다.


118-118: 동적 데이터 바인딩으로 실시간 통계 표시가 가능해졌습니다.

정적 텍스트를 ${dailyStats.dailyActiveUserCount}로 변경하여 실제 일일 활성 사용자 수를 표시할 수 있게 되었습니다.


125-144: 새로운 가입자 통계 카드가 적절히 구현되었습니다.

기존 통계 카드들과 동일한 구조와 스타일을 유지하면서, 오렌지 색상의 user-plus 아이콘과 ${dailyStats.dailyRegistrationCount} 바인딩이 올바르게 적용되었습니다.

src/main/java/com/wayble/server/logging/entity/UserActionLog.java (3)

18-19: Elasticsearch Document 설정이 적절합니다.

user_action_logs 인덱스명이 명확하고, 사용자 행동 로그를 저장하기에 적합한 구조입니다.


21-40: 필드 타입 매핑이 올바르게 구성되었습니다.

각 필드의 Elasticsearch 타입이 데이터 특성에 맞게 설정되었습니다:

  • userId: Long 타입으로 숫자 검색 최적화
  • action, loginType, userType: Keyword 타입으로 정확한 매칭 지원
  • userAgent: Text 타입으로 전문 검색 지원
  • timestamp: Date 타입으로 시간 범위 쿼리 지원

42-45: ActionType enum이 명확하게 정의되었습니다.

USER_REGISTERUSER_TOKEN_REFRESH 액션 타입이 사용자 행동 추적에 적합하며, 향후 확장 가능한 구조입니다.

src/main/java/com/wayble/server/aws/controller/FileUploadController.java (1)

18-27: 파일 업로드 엔드포인트 구현이 간결하고 효율적입니다.

Stream API를 활용하여 다중 파일을 처리하고 일관된 응답 구조를 제공합니다.

src/main/java/com/wayble/server/direction/controller/TransportationController.java (2)

13-13: HTTP 메소드 변경에 맞는 import 수정이 적절합니다.

GetMapping에서 PostMapping으로 변경되어 새로운 HTTP 메소드와 일치합니다.


54-55: GET에서 POST로의 메소드 변경이 적절합니다.

복잡한 요청 본문(TransportationRequestDto)을 가진 대중교통 경로 조회 API는 POST 메소드가 더 적합하며, 메소드명도 findDirections로 변경되어 HTTP 메소드와 일치합니다.

src/main/resources/elasticsearch/settings/direction_mappings.json (1)

1-23: Elasticsearch 매핑 및 analyzer 설정 검증 완료

매핑에서 사용하는 korean_edge_ngram_analyzerkorean_search_analyzer
src/main/resources/elasticsearch/settings/direction_settings.json에 올바르게 정의되어 있습니다.
해당 설정으로 코드 변경을 승인합니다.

• 확인된 파일

  • src/main/resources/elasticsearch/settings/direction_mappings.json
  • src/main/resources/elasticsearch/settings/direction_settings.json
src/main/java/com/wayble/server/admin/controller/AdminController.java (1)

80-82: 일일 통계 기능 추가 잘 구현됨

대시보드에 동적 통계 데이터를 추가하는 기능이 깔끔하게 구현되었습니다.

src/main/java/com/wayble/server/ServerApplication.java (2)

19-20: 설정 변경사항 확인

새로운 기능에 맞게 설정이 적절히 업데이트되었습니다. KricProperties 추가와 Elasticsearch 리포지토리 패키지 확장이 올바르게 적용되었습니다.


3-4: TMapProperties 패키지 경로 일관성 확인 완료

코드베이스 전체에서 TMapProperties의 패키지 경로가 com.wayble.server.common.client.tmap으로 일관되게 적용되었으며, 이전 경로(direction.external.tmap)는 더 이상 존재하지 않습니다.

src/main/java/com/wayble/server/logging/repository/UserActionLogRepository.java (1)

11-22: 인덱스 매핑 확인 완료: 추가 수정 불필요

  • UserActionLog 엔티티의 timestamp 필드는 @Field(type = FieldType.Date)로 올바르게 매핑되어 있습니다.
  • 별도의 Elasticsearch 매핑 설정 파일이 존재하지 않아, Spring Data Elasticsearch의 기본 매핑을 사용합니다.

현재 코드상에서는 추가적인 인덱스 설정이나 매핑 변경이 필요하지 않습니다.

src/main/java/com/wayble/server/logging/service/UserActionLogService.java (2)

46-52: 적절한 일별 통계 조회 구현

오늘의 사용자 등록 수를 조회하는 로직이 올바르게 구현되었습니다.


87-93: 효율적인 활성 사용자 수 조회

토큰 갱신 로그를 기반으로 중복 제거된 활성 사용자 수를 조회하는 로직이 잘 구현되었습니다.

src/main/java/com/wayble/server/direction/service/FacilityService.java (1)

31-67: 적절한 시설 정보 조회 구현

노드 정보 조회 및 DTO 변환 로직이 깔끔하게 구현되었습니다. null 처리도 적절합니다.

src/main/resources/templates/admin/wayblezone/wayble-zone-edit.html (1)

535-541: 이미지 제거 기능 적절히 구현됨

UI 요소들을 올바르게 초기화하는 로직이 잘 구현되었습니다.

src/main/resources/templates/admin/wayblezone/wayble-zone-create.html (2)

335-392: UI 구조가 적절하게 구현되었습니다!

이미지 업로드와 URL 직접 입력 옵션을 모두 제공하는 UI 디자인이 사용자 친화적입니다. 파일 업로드 시 URL 필드가 자동으로 채워진다는 안내 문구도 명확합니다.


515-553: 드래그 앤 드롭 기능이 잘 구현되었습니다!

이벤트 처리와 시각적 피드백이 적절히 구현되어 있습니다.

src/main/java/com/wayble/server/direction/service/TransportationService.java (5)

42-47: 거리 제한 검증이 적절히 구현되었습니다!

30km 제한으로 서비스 범위를 합리적으로 제한하고 있습니다.


32-35: 매직 넘버를 상수로 대체한 것이 좋습니다!

가독성과 유지보수성이 향상되었습니다.


52-52: 메서드 이름 오타가 수정되었습니다!

Dijkstra 알고리즘의 올바른 철자로 수정되어 코드 품질이 향상되었습니다.

Also applies to: 71-71, 128-128, 133-133


260-263: Null 체크가 적절히 추가되었습니다!

NullPointerException을 방지하는 방어적 프로그래밍이 잘 적용되었습니다.


21-21: 로깅 개선이 잘 되었습니다!

System.out.println을 적절한 로그 레벨(warn)로 대체하여 프로덕션 환경에 적합하게 개선되었습니다.

Also applies to: 216-216, 319-319

src/main/java/com/wayble/server/user/service/UserService.java (2)

4-4: LGTM! UserActionLogService 의존성 추가

사용자 액션 로깅을 위한 서비스 의존성 추가가 적절합니다.

Also applies to: 19-19


31-38: LGTM! 회원가입 로깅 추가

회원가입 성공 후 비동기적으로 사용자 등록 이벤트를 로깅하는 것은 감사(audit) 목적으로 유용합니다. save 결과를 savedUser 변수에 저장하여 ID를 사용하는 것도 적절합니다.

Comment on lines +104 to +114
@GetMapping("/api/daily-stats")
@ResponseBody
public CommonResponse<DailyStatsDto> getDailyStats(HttpSession session) {
// 로그인 확인 (API용)
if (session.getAttribute("adminLoggedIn") == null) {
return CommonResponse.error(401, "관리자 인증이 필요합니다.");
}

DailyStatsDto dailyStats = adminSystemService.getDailyStats();
return CommonResponse.success(dailyStats);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

API 엔드포인트 개선사항 제안

새로운 일일 통계 API가 잘 구현되었으나 몇 가지 개선할 점이 있습니다:

  1. HTTP 상태 코드 하드코딩
  2. 중복된 인증 확인 로직
  3. 예외 처리 부족

다음과 같이 개선하세요:

+private static final int UNAUTHORIZED_STATUS = 401;
+private static final String UNAUTHORIZED_MESSAGE = "관리자 인증이 필요합니다.";
+
+private boolean isAdminAuthenticated(HttpSession session) {
+    return session.getAttribute("adminLoggedIn") != null;
+}

 @GetMapping("/api/daily-stats")
 @ResponseBody
 public CommonResponse<DailyStatsDto> getDailyStats(HttpSession session) {
-    // 로그인 확인 (API용)
-    if (session.getAttribute("adminLoggedIn") == null) {
-        return CommonResponse.error(401, "관리자 인증이 필요합니다.");
+    if (!isAdminAuthenticated(session)) {
+        return CommonResponse.error(UNAUTHORIZED_STATUS, UNAUTHORIZED_MESSAGE);
     }
     
-    DailyStatsDto dailyStats = adminSystemService.getDailyStats();
-    return CommonResponse.success(dailyStats);
+    try {
+        DailyStatsDto dailyStats = adminSystemService.getDailyStats();
+        return CommonResponse.success(dailyStats);
+    } catch (Exception e) {
+        log.error("일일 통계 조회 중 오류 발생", e);
+        return CommonResponse.error(500, "통계 데이터 조회에 실패했습니다.");
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@GetMapping("/api/daily-stats")
@ResponseBody
public CommonResponse<DailyStatsDto> getDailyStats(HttpSession session) {
// 로그인 확인 (API용)
if (session.getAttribute("adminLoggedIn") == null) {
return CommonResponse.error(401, "관리자 인증이 필요합니다.");
}
DailyStatsDto dailyStats = adminSystemService.getDailyStats();
return CommonResponse.success(dailyStats);
}
private static final int UNAUTHORIZED_STATUS = 401;
private static final String UNAUTHORIZED_MESSAGE = "관리자 인증이 필요합니다.";
private boolean isAdminAuthenticated(HttpSession session) {
return session.getAttribute("adminLoggedIn") != null;
}
@GetMapping("/api/daily-stats")
@ResponseBody
public CommonResponse<DailyStatsDto> getDailyStats(HttpSession session) {
if (!isAdminAuthenticated(session)) {
return CommonResponse.error(UNAUTHORIZED_STATUS, UNAUTHORIZED_MESSAGE);
}
try {
DailyStatsDto dailyStats = adminSystemService.getDailyStats();
return CommonResponse.success(dailyStats);
} catch (Exception e) {
log.error("일일 통계 조회 중 오류 발생", e);
return CommonResponse.error(500, "통계 데이터 조회에 실패했습니다.");
}
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/admin/controller/AdminController.java lines
104 to 114, the getDailyStats API endpoint has hardcoded HTTP status codes,
duplicated authentication checks, and lacks exception handling. Refactor to use
standardized HTTP status constants instead of hardcoded values, extract the
authentication check into a reusable method or interceptor to avoid duplication,
and add try-catch blocks around service calls to handle exceptions gracefully,
returning appropriate error responses.

Comment on lines +56 to +92
public DailyStatsDto getDailyStats() {
try {
LocalDate today = LocalDate.now();

// 일일 가입자 수
long dailyRegistrationCount = userActionLogService.getTodayUserRegistrationCount();

// 일일 활성 유저 수 (방문자)
long dailyActiveUserCount = userActionLogService.getTodayActiveUserCount();

// 전체 유저 수
long totalUserCount = adminUserService.getTotalUserCount();

// 전체 웨이블존 수
long totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts();

return DailyStatsDto.builder()
.date(today)
.dailyRegistrationCount(dailyRegistrationCount)
.dailyActiveUserCount(dailyActiveUserCount)
.totalUserCount(totalUserCount)
.totalWaybleZoneCount(totalWaybleZoneCount)
.build();

} catch (Exception e) {
log.error("Failed to get daily stats", e);

// 에러 발생시 기본값 반환
return DailyStatsDto.builder()
.date(LocalDate.now())
.dailyRegistrationCount(0)
.dailyActiveUserCount(0)
.totalUserCount(0)
.totalWaybleZoneCount(0)
.build();
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

일일 통계 메서드 개선사항 제안

기본적인 구현은 잘되어 있으나, 개별 통계 수집 실패 시에도 다른 통계는 정상 수집할 수 있도록 개선하면 좋겠습니다.

다음과 같이 개별 예외 처리를 추가하여 부분적인 실패에도 대응하도록 개선하세요:

 public DailyStatsDto getDailyStats() {
-    try {
         LocalDate today = LocalDate.now();
         
-        // 일일 가입자 수
-        long dailyRegistrationCount = userActionLogService.getTodayUserRegistrationCount();
+        // 개별 통계 수집 (실패해도 다른 통계는 계속 수집)
+        long dailyRegistrationCount = 0;
+        long dailyActiveUserCount = 0;
+        long totalUserCount = 0;
+        long totalWaybleZoneCount = 0;
         
-        // 일일 활성 유저 수 (방문자)
-        long dailyActiveUserCount = userActionLogService.getTodayActiveUserCount();
+        try {
+            dailyRegistrationCount = userActionLogService.getTodayUserRegistrationCount();
+        } catch (Exception e) {
+            log.warn("일일 가입자 수 조회 실패", e);
+        }
         
-        // 전체 유저 수
-        long totalUserCount = adminUserService.getTotalUserCount();
+        try {
+            dailyActiveUserCount = userActionLogService.getTodayActiveUserCount();
+        } catch (Exception e) {
+            log.warn("일일 활성 유저 수 조회 실패", e);
+        }
         
-        // 전체 웨이블존 수
-        long totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts();
+        try {
+            totalUserCount = adminUserService.getTotalUserCount();
+        } catch (Exception e) {
+            log.warn("전체 유저 수 조회 실패", e);
+        }
+        
+        try {
+            totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts();
+        } catch (Exception e) {
+            log.warn("전체 웨이블존 수 조회 실패", e);
+        }
         
         return DailyStatsDto.builder()
                 .date(today)
                 .dailyRegistrationCount(dailyRegistrationCount)
                 .dailyActiveUserCount(dailyActiveUserCount)
                 .totalUserCount(totalUserCount)
                 .totalWaybleZoneCount(totalWaybleZoneCount)
                 .build();
-                
-    } catch (Exception e) {
-        log.error("Failed to get daily stats", e);
-        
-        // 에러 발생시 기본값 반환
-        return DailyStatsDto.builder()
-                .date(LocalDate.now())
-                .dailyRegistrationCount(0)
-                .dailyActiveUserCount(0)
-                .totalUserCount(0)
-                .totalWaybleZoneCount(0)
-                .build();
-    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public DailyStatsDto getDailyStats() {
try {
LocalDate today = LocalDate.now();
// 일일 가입자 수
long dailyRegistrationCount = userActionLogService.getTodayUserRegistrationCount();
// 일일 활성 유저 수 (방문자)
long dailyActiveUserCount = userActionLogService.getTodayActiveUserCount();
// 전체 유저 수
long totalUserCount = adminUserService.getTotalUserCount();
// 전체 웨이블존 수
long totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts();
return DailyStatsDto.builder()
.date(today)
.dailyRegistrationCount(dailyRegistrationCount)
.dailyActiveUserCount(dailyActiveUserCount)
.totalUserCount(totalUserCount)
.totalWaybleZoneCount(totalWaybleZoneCount)
.build();
} catch (Exception e) {
log.error("Failed to get daily stats", e);
// 에러 발생시 기본값 반환
return DailyStatsDto.builder()
.date(LocalDate.now())
.dailyRegistrationCount(0)
.dailyActiveUserCount(0)
.totalUserCount(0)
.totalWaybleZoneCount(0)
.build();
}
}
public DailyStatsDto getDailyStats() {
LocalDate today = LocalDate.now();
// 개별 통계 수집 (실패해도 다른 통계는 계속 수집)
long dailyRegistrationCount = 0;
long dailyActiveUserCount = 0;
long totalUserCount = 0;
long totalWaybleZoneCount = 0;
try {
dailyRegistrationCount = userActionLogService.getTodayUserRegistrationCount();
} catch (Exception e) {
log.warn("일일 가입자 수 조회 실패", e);
}
try {
dailyActiveUserCount = userActionLogService.getTodayActiveUserCount();
} catch (Exception e) {
log.warn("일일 활성 유저 수 조회 실패", e);
}
try {
totalUserCount = adminUserService.getTotalUserCount();
} catch (Exception e) {
log.warn("전체 유저 수 조회 실패", e);
}
try {
totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts();
} catch (Exception e) {
log.warn("전체 웨이블존 수 조회 실패", e);
}
return DailyStatsDto.builder()
.date(today)
.dailyRegistrationCount(dailyRegistrationCount)
.dailyActiveUserCount(dailyActiveUserCount)
.totalUserCount(totalUserCount)
.totalWaybleZoneCount(totalWaybleZoneCount)
.build();
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/admin/service/AdminSystemService.java between
lines 56 and 92, the getDailyStats method currently catches all exceptions in a
single try-catch block, causing all stats to fail if one fails. Refactor by
wrapping each individual stats retrieval call in its own try-catch block to
handle exceptions separately, allowing successful retrieval of other stats even
if one fails. Initialize each stat variable with a default value and update it
only if the corresponding service call succeeds.

Comment on lines +92 to +102
// 메인 이미지 변경 시 기존 이미지 S3에서 삭제
String oldImageUrl = waybleZone.getMainImageUrl();
String newImageUrl = updateDto.mainImageUrl();
if (oldImageUrl != null && !oldImageUrl.equals(newImageUrl)) {
try {
amazonS3Manager.deleteImageFileFromS3(oldImageUrl);
log.info("기존 메인 이미지 삭제 완료 - URL: {}", oldImageUrl);
} catch (Exception e) {
log.warn("기존 메인 이미지 삭제 실패 - URL: {}, 오류: {}", oldImageUrl, e.getMessage());
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

S3 이미지 삭제 로직을 별도 메서드로 분리하세요.

updateWaybleZone과 deleteWaybleZone 메서드에서 S3 이미지 삭제 로직이 중복됩니다. 코드 중복을 제거하고 가독성을 향상시키기 위해 별도 메서드로 분리하는 것을 권장합니다.

+    private void deleteImageFromS3(String imageUrl, String context) {
+        if (imageUrl != null) {
+            try {
+                amazonS3Manager.deleteImageFileFromS3(imageUrl);
+                log.info("{} 이미지 삭제 완료 - URL: {}", context, imageUrl);
+            } catch (Exception e) {
+                log.warn("{} 이미지 삭제 실패 - URL: {}, 오류: {}", context, imageUrl, e.getMessage());
+            }
+        }
+    }

그런 다음 중복된 코드를 이 메서드 호출로 대체하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 메인 이미지 변경 시 기존 이미지 S3에서 삭제
String oldImageUrl = waybleZone.getMainImageUrl();
String newImageUrl = updateDto.mainImageUrl();
if (oldImageUrl != null && !oldImageUrl.equals(newImageUrl)) {
try {
amazonS3Manager.deleteImageFileFromS3(oldImageUrl);
log.info("기존 메인 이미지 삭제 완료 - URL: {}", oldImageUrl);
} catch (Exception e) {
log.warn("기존 메인 이미지 삭제 실패 - URL: {}, 오류: {}", oldImageUrl, e.getMessage());
}
}
// Add this helper to AdminWaybleZoneService to centralize S3 image deletion
private void deleteImageFromS3(String imageUrl, String context) {
if (imageUrl != null) {
try {
amazonS3Manager.deleteImageFileFromS3(imageUrl);
log.info("{} 이미지 삭제 완료 - URL: {}", context, imageUrl);
} catch (Exception e) {
log.warn("{} 이미지 삭제 실패 - URL: {}, 오류: {}", context, imageUrl, e.getMessage());
}
}
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/admin/service/AdminWaybleZoneService.java
around lines 92 to 102, the S3 image deletion logic is duplicated in
updateWaybleZone and deleteWaybleZone methods. To fix this, extract the S3 image
deletion code into a separate private method that accepts the image URL as a
parameter. Then replace the duplicated deletion code in both methods with calls
to this new method to improve code reuse and readability.

Comment on lines +152 to +160
// 메인 이미지 S3에서 삭제
if (waybleZone.getMainImageUrl() != null) {
try {
amazonS3Manager.deleteImageFileFromS3(waybleZone.getMainImageUrl());
log.info("메인 이미지 삭제 완료 - URL: {}", waybleZone.getMainImageUrl());
} catch (Exception e) {
log.warn("메인 이미지 삭제 실패 - URL: {}, 오류: {}", waybleZone.getMainImageUrl(), e.getMessage());
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

중복된 S3 삭제 로직을 통합된 메서드로 교체하세요.

위에서 제안한 deleteImageFromS3 메서드를 사용하여 코드 중복을 제거하세요.

-            // 메인 이미지 S3에서 삭제
-            if (waybleZone.getMainImageUrl() != null) {
-                try {
-                    amazonS3Manager.deleteImageFileFromS3(waybleZone.getMainImageUrl());
-                    log.info("메인 이미지 삭제 완료 - URL: {}", waybleZone.getMainImageUrl());
-                } catch (Exception e) {
-                    log.warn("메인 이미지 삭제 실패 - URL: {}, 오류: {}", waybleZone.getMainImageUrl(), e.getMessage());
-                }
-            }
+            // 메인 이미지 S3에서 삭제
+            deleteImageFromS3(waybleZone.getMainImageUrl(), "메인");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 메인 이미지 S3에서 삭제
if (waybleZone.getMainImageUrl() != null) {
try {
amazonS3Manager.deleteImageFileFromS3(waybleZone.getMainImageUrl());
log.info("메인 이미지 삭제 완료 - URL: {}", waybleZone.getMainImageUrl());
} catch (Exception e) {
log.warn("메인 이미지 삭제 실패 - URL: {}, 오류: {}", waybleZone.getMainImageUrl(), e.getMessage());
}
}
// 메인 이미지 S3에서 삭제
deleteImageFromS3(waybleZone.getMainImageUrl(), "메인");
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/admin/service/AdminWaybleZoneService.java
around lines 152 to 160, replace the existing S3 main image deletion logic with
a call to the unified deleteImageFromS3 method to remove code duplication. This
involves removing the try-catch block and direct calls to
amazonS3Manager.deleteImageFileFromS3 and instead invoking deleteImageFromS3
with the main image URL, ensuring consistent error handling and logging.

Comment on lines +71 to +85
// 로그 저장 (비동기)
if (isNewUser) {
// 신규 가입 로그
userActionLogService.logUserRegister(
user.getId(),
LoginType.KAKAO.name(),
user.getUserType() != null ? user.getUserType().name() : null
);
}

// 모든 카카오 로그인시 활성 유저 로그 저장 (하루 1회만)
userActionLogService.logTokenRefresh(
user.getId(),
user.getUserType() != null ? user.getUserType().name() : null
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

로그 기록 로직을 별도 메서드로 분리하여 가독성을 개선하세요.

메인 로그인 로직과 로그 기록 로직이 섞여 있어 코드 가독성이 떨어집니다. 로그 기록을 별도 메서드로 분리하는 것을 권장합니다.

         // JWT 토큰 발급
         String accessToken = jwtProvider.generateToken(
                 user.getId(),
                 user.getUserType() != null ? user.getUserType().name() : null
         );
         String refreshToken = jwtProvider.generateRefreshToken(user.getId());
 
-        // 로그 저장 (비동기)
-        if (isNewUser) {
-            // 신규 가입 로그
-            userActionLogService.logUserRegister(
-                    user.getId(), 
-                    LoginType.KAKAO.name(), 
-                    user.getUserType() != null ? user.getUserType().name() : null
-            );
-        }
-        
-        // 모든 카카오 로그인시 활성 유저 로그 저장 (하루 1회만)
-        userActionLogService.logTokenRefresh(
-                user.getId(), 
-                user.getUserType() != null ? user.getUserType().name() : null
-        );
+        // 사용자 행동 로그 기록
+        logUserActions(user, isNewUser);

+    private void logUserActions(User user, boolean isNewUser) {
+        try {
+            if (isNewUser) {
+                userActionLogService.logUserRegister(
+                        user.getId(), 
+                        LoginType.KAKAO.name(), 
+                        user.getUserType() != null ? user.getUserType().name() : null
+                );
+            }
+            
+            userActionLogService.logTokenRefresh(
+                    user.getId(), 
+                    user.getUserType() != null ? user.getUserType().name() : null
+            );
+        } catch (Exception e) {
+            log.warn("사용자 행동 로그 기록 실패 - 사용자 ID: {}", user.getId(), e);
+        }
+    }
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/auth/service/KakaoLoginService.java around
lines 71 to 85, the logging logic is mixed with the main login process, reducing
readability. Refactor by extracting the user action logging code into a separate
private method that handles both new user registration logs and daily token
refresh logs. Then call this new method from the main login flow to keep the
code cleaner and more maintainable.

Comment on lines +95 to +103
private HttpServletRequest getCurrentRequest() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes != null ? attributes.getRequest() : null;
} catch (Exception e) {
log.debug("Failed to get current request", e);
return null;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

비동기 실행 후 사용되지 않을 메서드

위의 리팩토링을 적용하면 이 메서드는 더 이상 필요하지 않습니다. User-Agent를 파라미터로 전달받도록 수정 후 이 메서드를 제거하세요.

🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/logging/service/UserActionLogService.java
around lines 95 to 103, the getCurrentRequest() method is no longer needed after
refactoring to pass the User-Agent as a parameter. Remove this method entirely
and update all calls to it to instead accept the User-Agent string as a method
parameter.

Comment on lines +112 to +114
user.getDisabilityType().size();
user.getMobilityAid().size();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

컬렉션 초기화 방식을 개선해야 합니다.

.size() 호출로 지연 로딩된 컬렉션을 강제 초기화하고 있지만, 다음 문제점들이 있습니다:

  1. 반환값을 사용하지 않는 불필요한 호출
  2. 컬렉션이 null인 경우 NullPointerException 발생 위험
  3. 명시적이지 않은 초기화 방식

더 안전하고 명확한 방법으로 수정하는 것을 권장합니다:

-        user.getDisabilityType().size();
-        user.getMobilityAid().size();
+        // Eager loading for collections
+        if (user.getDisabilityType() != null) {
+            user.getDisabilityType().size();
+        }
+        if (user.getMobilityAid() != null) {
+            user.getMobilityAid().size();
+        }

또는 Entity에서 @Fetch(FetchMode.EAGER) 사용하거나 JPQL에서 JOIN FETCH를 사용하는 것이 더 명확한 해결방안입니다.

🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/user/service/UserInfoService.java around
lines 112 to 114, the code uses .size() calls on potentially lazy-loaded
collections to force initialization, which is unsafe and unclear. To fix this,
replace these .size() calls with a safer approach such as checking for null
before accessing the collections or explicitly initializing them using a utility
method designed for this purpose. Alternatively, consider modifying the entity
mappings to use @Fetch(FetchMode.EAGER) or adjust the JPQL queries to include
JOIN FETCH to ensure collections are loaded eagerly and safely.

Comment on lines +440 to +443
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드 가능합니다.');
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

파일 타입 검증을 강화해야 합니다.

MIME 타입만으로 검증하는 것은 충분하지 않습니다. 파일 확장자도 함께 검증하는 것이 좋습니다.

 // 파일 타입 체크
 if (!file.type.startsWith('image/')) {
     alert('이미지 파일만 업로드 가능합니다.');
     return;
 }
+
+// 파일 확장자 체크
+const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+const fileName = file.name.toLowerCase();
+const fileExtension = fileName.split('.').pop();
+if (!allowedExtensions.includes(fileExtension)) {
+    alert('지원되는 이미지 형식: JPG, JPEG, PNG, GIF, WEBP');
+    return;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드 가능합니다.');
return;
}
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드 가능합니다.');
return;
}
// 파일 확장자 체크
const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const fileName = file.name.toLowerCase();
const fileExtension = fileName.split('.').pop();
if (!allowedExtensions.includes(fileExtension)) {
alert('지원되는 이미지 형식: JPG, JPEG, PNG, GIF, WEBP');
return;
}
🤖 Prompt for AI Agents
In src/main/resources/templates/admin/wayblezone/wayble-zone-create.html around
lines 440 to 443, the file type validation currently only checks the MIME type,
which is insufficient. Enhance the validation by also checking the file
extension against a list of allowed image extensions (e.g., .jpg, .png, .gif).
Implement this by extracting the file extension from the file name and verifying
it matches accepted image formats before allowing the upload.

Comment on lines +472 to +475
const errorText = await response.text();
console.error('응답 오류:', errorText);
throw new Error(`업로드 실패 (${response.status}): ${errorText}`);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

에러 메시지에서 민감한 정보 노출을 방지해야 합니다.

콘솔과 알림에 상세한 에러 정보를 노출하면 보안 위험이 있습니다.

-const errorText = await response.text();
-console.error('응답 오류:', errorText);
-throw new Error(`업로드 실패 (${response.status}): ${errorText}`);
+const errorText = await response.text();
+console.error('업로드 실패:', response.status);
+throw new Error(`업로드 실패 (오류 코드: ${response.status})`);
 } catch (error) {
     console.error('업로드 오류:', error);
-    alert('이미지 업로드에 실패했습니다: ' + error.message);
+    alert('이미지 업로드에 실패했습니다. 다시 시도해 주세요.');

Also applies to: 499-500

🤖 Prompt for AI Agents
In src/main/resources/templates/admin/wayblezone/wayble-zone-create.html around
lines 472 to 475 and also lines 499 to 500, the error handling code logs and
throws errors with detailed response text that may contain sensitive
information. To fix this, remove or sanitize the detailed error text from both
the console.error and the thrown Error message, replacing it with a generic
error message that does not expose sensitive data while still indicating failure
and status code.

Comment on lines +489 to +507
console.log('업로드 시작:', file.name, file.size, file.type);

// S3에 업로드
const response = await fetch('/api/v1/files/images', {
method: 'POST',
body: formData
});

console.log('응답 상태:', response.status, response.statusText);

if (!response.ok) {
const errorText = await response.text();
console.error('응답 오류:', errorText);
throw new Error(`업로드 실패 (${response.status}): ${errorText}`);
}

const result = await response.json();
console.log('응답 결과:', result);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

프로덕션 환경에서 console.log 제거 필요

디버깅용 console.log 문은 프로덕션 환경에서 제거되어야 합니다.

-                console.log('업로드 시작:', file.name, file.size, file.type);
                 
                 // S3에 업로드
                 const response = await fetch('/api/v1/files/images', {
                     method: 'POST',
                     body: formData
                 });
                 
-                console.log('응답 상태:', response.status, response.statusText);
                 
                 if (!response.ok) {
                     const errorText = await response.text();
-                    console.error('응답 오류:', errorText);
                     throw new Error(`업로드 실패 (${response.status}): ${errorText}`);
                 }
                 
                 const result = await response.json();
-                console.log('응답 결과:', result);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log('업로드 시작:', file.name, file.size, file.type);
// S3에 업로드
const response = await fetch('/api/v1/files/images', {
method: 'POST',
body: formData
});
console.log('응답 상태:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
console.error('응답 오류:', errorText);
throw new Error(`업로드 실패 (${response.status}): ${errorText}`);
}
const result = await response.json();
console.log('응답 결과:', result);
// S3에 업로드
const response = await fetch('/api/v1/files/images', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`업로드 실패 (${response.status}): ${errorText}`);
}
const result = await response.json();
🤖 Prompt for AI Agents
In src/main/resources/templates/admin/wayblezone/wayble-zone-edit.html around
lines 489 to 507, remove all console.log statements used for debugging to
prevent them from appearing in the production environment. Replace them with
proper logging mechanisms if needed or remove them entirely to keep the
production code clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release 프로덕션 서버에 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants