-
Notifications
You must be signed in to change notification settings - Fork 1
[release] 진행상황 Main 브랜치에 반영 #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
[feat] 실제 무장애 가게 데이터로 현재까지 만든 API 통합 테스트 및 오류 해결
[feat] 에러 로그 조회 기능 구현, 웨이블존 csv data 삽입 및 각종 버그 수정
Walkthrough이번 변경에서는 에러 로그 관리 및 조회 기능이 관리자 페이지에 추가되고, 서초구 무장애 가게 데이터를 CSV로 일괄 등록하는 기능이 도입되었습니다. 로그 관리와 관련된 환경설정(로그백, 도커, 볼륨/타임존)도 함께 적용되었으며, 웨이블존, 리뷰, 시설 정보 등 여러 도메인에서 버그 수정과 트랜잭션 관리, UI 개선이 이루어졌습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin User
participant Browser as Web Browser
participant Controller as AdminLogController
participant Service as LogService
participant FileSys as Log Files
Admin->>Browser: 관리자 페이지 접속
Browser->>Controller: GET /admin/logs/error
Controller->>Service: getRecentErrorLogs(limit)
Service->>FileSys: 최근 7일 로그 파일 읽기
FileSys-->>Service: 로그 데이터 반환
Service-->>Controller: 파싱된 에러 로그 리스트
Controller-->>Browser: 에러 로그 페이지 렌더링
Browser->>Controller: GET /admin/logs/error/data?limit=100 (AJAX)
Controller->>Service: getRecentErrorLogs(100)
Service->>FileSys: 로그 파일 읽기
FileSys-->>Service: 로그 데이터 반환
Service-->>Controller: 에러 로그 리스트
Controller-->>Browser: JSON 응답
Browser->>Controller: GET /admin/logs/error/stats (AJAX)
Controller->>Service: getErrorLogStats()
Service->>FileSys: 로그 파일 읽기
FileSys-->>Service: 로그 데이터 반환
Service-->>Controller: 에러 통계
Controller-->>Browser: JSON 응답
sequenceDiagram
participant Importer as SeochoCsvImporter
participant CSV as CSV File
participant DB as Database
Importer->>CSV: CSV 파일 오픈
loop 각 행
Importer->>Importer: 파싱/유효성 검사
Importer->>DB: WaybleZone 저장
Importer->>DB: WaybleZoneFacility 저장
Importer->>DB: WaybleZoneOperatingHour 저장
end
Importer-->>DB: 커밋
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes해당 PR의 모든 주요 변경은 연결된 이슈(#120, #122)의 요구사항 범위 내에 있습니다. Possibly related PRs
Suggested reviewers
Poem
Note 🔌 MCP (Model Context Protocol) integration is now available in Early Access!Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 (4)
build.gradle (1)
55-57: 중복 의존성 제거 필요 (AWS SDK가 두 번 선언됨)
com.amazonaws:aws-java-sdk-core및com.amazonaws:aws-java-sdk-s3가 위쪽(라인 39-41)과 여기(라인 55-57)에 중복 선언되어 있습니다. Gradle이 버전을 하나로 해석하더라도, 유지보수성/혼동 측면에서 중복은 제거하는 것이 좋습니다.다음과 같이 하단 선언을 제거해 주세요:
- //AWS S3 - implementation 'com.amazonaws:aws-java-sdk-core:1.12.698' - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.698'src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java (3)
58-78: 크리티컬: @ExceptionHandler 시그니처가 잘못되었습니다 (BindingResult 주입 불가)@ExceptionHandler에서 BindingResult는 직접 주입되지 않습니다. ex.getBindingResult()로 사용하세요. 현 상태에선 파라미터 리졸브 실패로 예외 처리 중 또 예외가 발생할 수 있습니다.
-@ExceptionHandler(value = MethodArgumentNotValidException.class) -public ResponseEntity<CommonResponse> handleValidException(BindingResult bindingResult, - - MethodArgumentNotValidException ex, - WebRequest request) { - String message = bindingResult.getAllErrors().get(0).getDefaultMessage(); +@ExceptionHandler(MethodArgumentNotValidException.class) +public ResponseEntity<CommonResponse> handleValidException(MethodArgumentNotValidException ex, + WebRequest request) { + String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
74-78: 4xx(검증 실패)에 대한 Discord 알림은 노이즈입니다운영 알림은 5xx만 전송하도록 제한하는 게 일반적입니다. 검증 예외는 로그만 남기고 알림 제외를 권장합니다.
- sendToDiscord(ex, request, HttpStatus.BAD_REQUEST); + // 4xx는 운영 알림 제외 + // sendToDiscord(ex, request, HttpStatus.BAD_REQUEST);
127-136: webhook URL 미설정/오류 대비 가드 추가 권장discord.webhook-url이 누락되면 전송 시 예외가 발생할 수 있습니다. 빈 값 체크로 조기 종료하세요.
try{ + if (discordWebhookUrl == null || discordWebhookUrl.isBlank()) { + log.warn("Discord webhook URL이 설정되지 않아 알림을 건너뜁니다."); + return; + } restTemplate.postForEntity( discordWebhookUrl, new HttpEntity<>(payload, headers), String.class );
🧹 Nitpick comments (30)
src/main/java/com/wayble/server/user/repository/UserPlaceWaybleZoneMappingRepository.java (1)
12-13: 추가 로딩 범위 점검 제안(사용처에 따라 userPlace.user 등 2차 참조 필요 가능)서비스/컨트롤러에서
mapping.getUserPlace().getUser()같은 2차 참조를 즉시 접근한다면 여전히 LazyInitializationException/N+1이 발생할 수 있습니다. 사용 패턴을 확인해attributePaths에userPlace.user를 추가하거나, DTO 프로젝션/전용 조회 메서드로 분리하는 방안을 검토해 주세요.src/main/resources/logback-spring.xml (2)
24-42: 파일 쓰기 성능 및 지연을 줄이기 위해 AsyncAppender 사용을 권장합니다고빈도 INFO/DEBUG 로깅 시 동기식 파일 I/O가 요청 지연을 유발할 수 있습니다. AsyncAppender로 완충하세요.
아래처럼 AsyncAppender를 추가하고 root/logger에서 해당 AsyncAppender를 참조하도록 변경을 고려해 주세요.
+ <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> + <queueSize>8192</queueSize> + <discardingThreshold>0</discardingThreshold> + <neverBlock>true</neverBlock> + <appender-ref ref="FILE"/> + </appender> + + <appender name="ASYNC_ERROR_FILE" class="ch.qos.logback.classic.AsyncAppender"> + <queueSize>4096</queueSize> + <discardingThreshold>0</discardingThreshold> + <neverBlock>true</neverBlock> + <appender-ref ref="ERROR_FILE"/> + </appender> ... - <root level="INFO"> - <appender-ref ref="CONSOLE"/> - <appender-ref ref="FILE"/> - <appender-ref ref="ERROR_FILE"/> - </root> + <root level="INFO"> + <appender-ref ref="CONSOLE"/> + <appender-ref ref="ASYNC_FILE"/> + <appender-ref ref="ASYNC_ERROR_FILE"/> + </root> ... - <logger name="com.wayble.server" level="DEBUG" additivity="false"> - <appender-ref ref="CONSOLE"/> - <appender-ref ref="FILE"/> - <appender-ref ref="ERROR_FILE"/> - </logger> + <logger name="com.wayble.server" level="DEBUG" additivity="false"> + <appender-ref ref="CONSOLE"/> + <appender-ref ref="ASYNC_FILE"/> + <appender-ref ref="ASYNC_ERROR_FILE"/> + </logger>또한
cleanHistoryOnStart는 보존기간을 초과한 아카이브를 재시작 시 삭제합니다. 장애 분석 시 구간 손실을 피하려면 off 혹은 보존기간을 충분히 길게 설정하세요.Also applies to: 44-49, 51-56
51-56: 애플리케이션 로거 설정(L2): 적절하게 분리되어 있습니다
additivity="false"로 루트 중복 로그를 방지했고, 패키지 레벨을 DEBUG로 설정해 트러블슈팅에 유용합니다. 하위 환경(prod)에서는 DEBUG를 INFO로 상향 조정할 수 있도록 프로파일별 설정 분리만 추가로 고려해 주세요.src/main/java/com/wayble/server/wayblezone/importer/BusinessHourParser.java (3)
28-35: “휴무/점포별 상이”는 빈 맵 반환으로 끝납니다 — 휴무일 표기가 필요한 경우가 있습니다지금은
Map<DayOfWeek, TimeRange>만 반환되어 휴무일 정보를 전달할 수 없습니다. CSV에 “월요일 휴무”처럼 명시된 경우를 반영할 수 있어야 합니다.
- 파서 반환을
ParsedBusinessHours { Map<DayOfWeek, TimeRange> open, Set<DayOfWeek> closed }형태로 확장하거나,- 최소한 “휴무”를 전체 요일로 표기하는 옵션을 importer에서 처리할 수 있도록 신호값을 제공합니다(예: 특수 키/상수).
원하시면 호환성(기존 호출부 영향 최소화)을 유지하는 인터페이스 확장안을 제안드리겠습니다.
36-49: matchAndFill는 첫 매치만 반영합니다 — 동일 패턴 다중 구간 지원 고려문자열에 동일 패턴이 2회 이상 등장하는 경우(예: “주말 10
18, 공휴일 1217”) 첫 구간만 반영됩니다.
if (m.find())→while (m.find())로 변경해 누적 반영하거나, 현재 요구사항상 단일 구간만 허용한다면 주석으로 제약을 명시해 주세요.Also applies to: 62-69
21-60: 단위 테스트 추가 제안다음 케이스 커버리지가 중요합니다.
- “9시
18시”, “09:0018:00”, “평일 …, 주말 …”, “토/일 개별”, “24시간/24시”- “월 10
18, 화 1119 …” 개별 요일 혼합- “휴무”, “점포별 상이/상이”
원하시면 JUnit 파라미터라이즈드 테스트 템플릿을 제공하겠습니다.
src/main/java/com/wayble/server/wayblezone/importer/CsvSupport.java (3)
15-20: YN 파싱 범위 확장 제안(운영 데이터 내 다양한 표기 호환)실데이터에서
"Y" / "N"외"예","O","1","true"등이 혼재할 수 있습니다.간단 확장 예:
- public static boolean ynToBool(String v) { - if (v == null) return false; - v = v.trim(); - if (v.isEmpty()) return false; - return "Y".equalsIgnoreCase(v); - } + public static boolean ynToBool(String v) { + if (v == null) return false; + String s = v.trim(); + if (s.isEmpty()) return false; + String u = s.toUpperCase(Locale.ROOT); + return u.equals("Y") || u.equals("YES") || u.equals("TRUE") || u.equals("1") || u.equals("O") || u.equals("T") || s.equals("예"); + }
22-28: toDouble 내고장성: 다중 점/이상치 문자 제거 후 NFE 캐치 OK — 로그 남김 고려파싱 실패 시 null 반환만 하므로, 원문과 함께 한 줄 로그(TRACE/DEBUG)라도 남기면 데이터 정제 이슈 역추적에 유리합니다.
52-62: Address detailAddress null 고정 — 원데이터에 보조 주소가 있으면 반영 고려CSV에 건물명/호수 등 세부 주소가 있으면
detailAddress에 반영하는 편이 사용자 경험에 유리합니다. 현재 스키마가 허용하므로 향후 확장을 고려해 주세요.src/main/resources/templates/admin/dashboard.html (1)
226-247: 에러 로그 관리 메뉴 추가 — 좋습니다. 접근성 미세 개선 제안카드 링크에 aria-label을 추가하면 스크린리더 접근성이 좋아집니다.
-<a href="/admin/logs/error" class="group block h-full"> +<a href="/admin/logs/error" class="group block h-full" aria-label="에러 로그 관리">src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java (1)
74-82: 예외 상세 정보 처리에서 길이 제한 로직을 개선할 수 있습니다.현재 1000자로 하드코딩된 길이 제한을 상수로 추출하고, 보다 안전한 문자열 자르기 방식을 사용하는 것이 좋겠습니다.
+ private static final int MAX_EXCEPTION_DETAIL_LENGTH = 1000; + private static final String TRUNCATION_SUFFIX = "..."; + // 예외 세부 정보 라인 처리 if (line.contains("Exception:") && exceptionDetail.isEmpty()) { - if (line.length() > 1000) { - exceptionDetail = line.substring(0, 1000) + "..."; + if (line.length() > MAX_EXCEPTION_DETAIL_LENGTH) { + exceptionDetail = line.substring(0, MAX_EXCEPTION_DETAIL_LENGTH) + TRUNCATION_SUFFIX; } else { exceptionDetail = line; }src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java (1)
19-21: LIKE '%도시%' 검색의 성능 고려 필요contains 검색은 인덱스를 타기 어려워 풀스캔이 발생할 수 있습니다. 데이터가 커지면 응답 지연이 생깁니다.
대안:
- prefix 검색으로 모델링(= 'seoul%' 형태) 및 적절한 인덱스 활용.
- 별도 정규화 컬럼(예: address_city_normalized) 도입 후 인덱스 적용.
- Elasticsearch 등 전문 검색 도입(중장기).
src/main/resources/templates/admin/log/error-logs.html (2)
181-186: 선택된 limit 값이 모델의 limit와 불일치할 수 있습니다에 th:value만 주면 초기 선택 반영이 되지 않습니다. 옵션별 th:selected로 모델 값과 동기화하세요. -<select id="limitSelect" class="limit-select ml-2" th:value="${limit}"> - <option value="50">50개</option> - <option value="100" selected>100개</option> - <option value="200">200개</option> - <option value="500">500개</option> -</select> +<select id="limitSelect" class="limit-select ml-2"> + <option value="50" th:selected="${limit == 50}">50개</option> + <option value="100" th:selected="${limit == 100}">100개</option> + <option value="200" th:selected="${limit == 200}">200개</option> + <option value="500" th:selected="${limit == 500}">500개</option> +</select> 7-7: CDN 스크립트는 버전 고정 및 SRI 적용을 고려하세요 운영/관리자 화면에서도 정적 리소스 무결성 보장을 위해 Tailwind CDN을 버전 고정하고 integrity/crossorigin을 사용하는 편이 안전합니다. 또는 자체 호스팅을 검토하세요. src/main/java/com/wayble/server/wayblezone/service/WaybleZoneService.java (2) 39-49: 목록 썸네일 일관성 확보(선택) 첫 이미지를 선택하는 로직은 리스트 정렬 기준이 없으면 비결정적일 수 있습니다. 업로드 시각/ID 오름차순 등 명시적 정렬 혹은 대표 이미지 플래그를 고려하세요. 108-125: resolveType 케이스 확장 및 테스트 권장 한글 매핑은 훌륭합니다. 추가로 공백/영문 소문자/대문자/혼용 케이스를 커버하는 단위 테스트를 권장합니다. (예: " cafe ", "RESTAURANT", "편의점") 원하시면 JUnit 테스트 스켈레톤을 생성해드리겠습니다. src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java (2) 51-52: 비즈니스 예외 처리부의 미사용 변수 정리 status 변수가 더 이상 사용되지 않습니다. 정리해 가독성을 높이세요. -HttpStatus status = HttpStatus.valueOf(e.getErrorCase().getHttpStatusCode()); -//sendToDiscord(e, request, status); +// sendToDiscord(e, request, HttpStatus.valueOf(e.getErrorCase().getHttpStatusCode())); 36-38: 주입 방식 일관화 권장 (선택) 필드 주입(@Autowired)과 생성자 주입(@requiredargsconstructor)이 혼재되어 있습니다. Environment도 생성자 주입으로 통일하면 테스트와 유지보수가 용이합니다. -@Autowired -private Environment env; +private final Environment env; src/main/java/com/wayble/server/wayblezone/importer/SeochoCsvImporter.java (4) 31-35: 로컬 프로파일 하드 제한 — 운영 배포/일회성 배치 실행 전략 재검토 필요 @Profile("local")로 한정되어 있어 운영 서버에서 실제 데이터 이관 시 재사용이 어렵습니다. property 기반의 on/off 게이트(wayble.import.enabled)만으로도 충분히 안전장치를 둘 수 있으니, 프로파일 제한을 완화하거나 별도 배치/관리자용 프로파일로 분리하는 것을 권장합니다. 운영 데이터 이관에도 이 Importer를 사용할 계획이라면, 프로파일 전략/실행 방법을 명확히 해주세요(예: prod에서 enabled=true로 일회성 실행). 원하시면 안전한 실행 절차 문서/스크립트를 제안하겠습니다. 51-52: 대용량 트랜잭션 범위 조정 권장 메서드 전체 @transactional은 레코드 수가 많을 때 트랜잭션/메모리 부담이 큽니다. 현재는 레코드별 예외를 삼켜(commit 경로) 전체 롤백 위험은 낮지만, flush/clear 배치(예: N건 단위) 또는 청크 단위 트랜잭션으로 조정하면 성능/잠금 측면에서 더 안전합니다. 원하시면 청크 커밋/배치 저장 패턴으로 리팩터 제안 드립니다. 113-127: 문턱 여부의 2진 표현 한계(불명확/미제공 케이스 구분 필요) hasNoDoorStep = !(entranceStep || entranceDoorStep)로 계산되어 “불명/미제공”까지 “문턱 있음”처럼 보일 수 있습니다. 불명 값을 구분해야 한다면 Boolean(래퍼) 또는 별도 “unknown” 플래그를 권장합니다. 데이터가 ‘Y/N/빈값(미상)’을 구분한다면 3상 로직으로 전환하는 게 UI/분석에 유리합니다. 143-146: 실패 로그에 스택트레이스 포함 필요 e.toString()만 남기면 원인 분석이 어렵습니다. 예외 인자를 함께 전달해 스택트레이스를 기록하세요. - log.warn("[Wayble Import] row {} failed: {}", idx, e.toString()); + log.warn("[Wayble Import] row {} failed", idx, e); src/main/java/com/wayble/server/wayblezone/entity/WaybleZoneFacility.java (3) 27-41: columnDefinition의 이식성 및 마이그레이션 일관성 확인 columnDefinition = "boolean default false"는 DB 벤더에 종속적입니다. DDL을 Flyway/Liquibase로 관리한다면 기본값 선언은 마이그레이션 스크립트에 위임하고, 엔티티에서는 nullable=false + 자바 기본값(false)만 두는 편이 이식성/일관성면에서 안전합니다. 현재 운영 DB 벤더/DDL 관리 정책에 맞는지 확인 부탁드립니다. 필요 시 정리용 패치 제안 가능합니다. 30-31: 주석 의미 명확화 제안 hasNoDoorStep는 “문턱 없음”을 의미하나 주석은 “문턱”으로만 표기되어 혼동 여지 있습니다. “문턱 없음”으로 명확히 해주세요. - private boolean hasNoDoorStep = false; // 문턱 + private boolean hasNoDoorStep = false; // 문턱 없음 45-56: toString의 프레젠테이션 로직 분리 고려 엔티티의 toString에 현지화 문자열을 넣으면 i18n/표현 제어가 어려워집니다. 표현은 DTO/뷰 레이어로 이동하고, 엔티티는 데이터만 유지하는 것을 권장합니다. 현 구조 유지 시에도 MessageSource 기반의 변환 레이어 도입을 고려하세요. src/main/java/com/wayble/server/admin/service/LogService.java (5) 38-50: limit 조기 충족 시 불필요한 파일/라인 처리 최소화 현재 파일 단위 루프 초기에만 크기 검사합니다. 엔트리 추가 중에도 limit*2 도달 시 즉시 중단하도록 보완하세요. - for (String logEntry : logEntries) { - ErrorLogDto errorLog = ErrorLogDto.from(logEntry); - if (errorLog != null) { - errorLogs.add(errorLog); - } - } + for (String logEntry : logEntries) { + ErrorLogDto errorLog = ErrorLogDto.from(logEntry); + if (errorLog != null) { + errorLogs.add(errorLog); + if (errorLogs.size() >= limit * 2) break; + } + } + if (errorLogs.size() >= limit * 2) break; // 다음 파일 처리 중단 66-105: 롤링 로그 파일 정렬/필터의 견고성 파일명 파싱은 잘 되어 있으나, 파일명 기반 정렬은 경우에 따라(인덱스/압축 유무) 실제 최신순과 어긋날 수 있습니다. Files.getLastModifiedTime 또는 파싱한 날짜(및 인덱스)를 기준으로 정렬하는 것이 더 안전합니다. 로그 경로/파일명 패턴이 변경될 수 있으므로, 패턴을 설정값으로 분리하는 것도 고려해 주세요. 원하시면 날짜+인덱스 기준 정렬/필터 구현을 제안하겠습니다. 110-137: 멀티라인 파싱 패턴의 하드코딩 — 로거 패턴 변경 대응성 낮음 "[yyyy-MM-dd HH:mm:ss]" 고정 패턴 의존은 logback 패턴 변경 시 파싱 실패를 유발합니다. 패턴(정규식)을 설정값으로 분리하거나, 최소한 사전 컴파일된 Pattern 상수로 올려 성능/가독성을 개선하세요. 외부 적용 코드 예(클래스 상단에 추가): private static final java.util.regex.Pattern ENTRY_START = java.util.regex.Pattern.compile("^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\].*"); 사용부 교체: - if (line.matches("^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\].*")) { + if (ENTRY_START.matcher(line).matches()) { 167-205: 통계 계산: 예외 시 lastErrorTime ‘지금’ 반환은 오해 소지 파일 읽기 실패 시 lastErrorTime을 현재 시각으로 반환하면 대시보드에서 최근 에러가 있는 것처럼 보일 수 있습니다. null로 돌려보내거나 별도 플래그로 실패를 구분하세요. - } catch (IOException e) { - log.error("에러 로그 통계 조회 실패", e); - return new ErrorLogStats(0, 0, 0, LocalDateTime.now()); - } + } catch (IOException e) { + log.error("에러 로그 통계 조회 실패", e); + return new ErrorLogStats(0, 0, 0, null); + } 25-26: 로그 경로 상수화 대신 설정값 주입 권장 경로가 환경에 따라 달라질 수 있으므로 @value로 외부화하세요(컨테이너 볼륨/베이스 경로 변경 대응). 예: @org.springframework.beans.factory.annotation.Value("${wayble.error.log.path:logs/wayble-error.log}") private String errorLogPath; 그리고 Paths.get(errorLogPath)로 사용. 📜 Review details Configuration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📥 Commits Reviewing files that changed from the base of the PR and between 94d0336 and 15c1461. ⛔ Files ignored due to path filters (1) src/main/resources/data/seocho_wayblezone.csv is excluded by !**/*.csv 📒 Files selected for processing (28) .github/workflows/cd-develop.yml (1 hunks) .gitignore (1 hunks) build.gradle (1 hunks) docker-compose.yml (1 hunks) src/main/java/com/wayble/server/admin/controller/AdminLogController.java (1 hunks) src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java (1 hunks) src/main/java/com/wayble/server/admin/service/LogService.java (1 hunks) src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java (4 hunks) src/main/java/com/wayble/server/review/entity/Review.java (3 hunks) src/main/java/com/wayble/server/review/repository/ReviewRepository.java (1 hunks) src/main/java/com/wayble/server/review/service/ReviewService.java (3 hunks) src/main/java/com/wayble/server/user/repository/UserPlaceWaybleZoneMappingRepository.java (1 hunks) src/main/java/com/wayble/server/user/service/UserPlaceService.java (2 hunks) src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java (1 hunks) src/main/java/com/wayble/server/wayblezone/entity/WaybleZoneFacility.java (2 hunks) src/main/java/com/wayble/server/wayblezone/importer/BusinessHourParser.java (1 hunks) src/main/java/com/wayble/server/wayblezone/importer/CsvSupport.java (1 hunks) src/main/java/com/wayble/server/wayblezone/importer/SeochoCsvImporter.java (1 hunks) src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneFacilityRepository.java (1 hunks) src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneOperatingHourRepository.java (1 hunks) src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java (1 hunks) src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepositoryImpl.java (1 hunks) src/main/java/com/wayble/server/wayblezone/service/WaybleZoneService.java (4 hunks) src/main/resources/logback-spring.xml (1 hunks) src/main/resources/templates/admin/dashboard.html (1 hunks) src/main/resources/templates/admin/log/error-logs.html (1 hunks) src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html (1 hunks) src/main/resources/templates/admin/wayblezone/wayble-zones.html (1 hunks) 🧰 Additional context used 🧬 Code Graph Analysis (4) src/main/java/com/wayble/server/user/repository/UserPlaceWaybleZoneMappingRepository.java (2) src/main/java/com/wayble/server/user/entity/UserPlaceWaybleZoneMapping.java (1) Entity (10-29) src/main/java/com/wayble/server/user/entity/UserPlace.java (1) Entity (11-29) src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html (1) src/main/java/com/wayble/server/direction/service/FacilityService.java (1) Service (23-109) src/main/resources/logback-spring.xml (1) src/main/java/com/wayble/server/common/aop/LoggingAspect.java (1) Aspect (13-54) src/main/java/com/wayble/server/wayblezone/importer/BusinessHourParser.java (3) src/main/java/com/wayble/server/wayblezone/entity/WaybleZoneOperatingHour.java (1) Entity (14-64) src/main/java/com/wayble/server/wayblezone/dto/WaybleZoneDetailResponseDto.java (2) Builder (23-27) Builder (9-28) src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneDetailDto.java (1) OperatingHourInfo (45-50) ⏰ 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 (21) .gitignore (1) 43-44: 로그 디렉터리 ignore 추가 적절 컨테이너 볼륨 마운트 경로(/app/logs)와 로컬 ./logs를 맞춰 관리하려는 목적에 부합합니다. 운영/배포 파이프라인 변경과 일관성 있습니다. docker-compose.yml (1) 17-21: docker-compose.yml: 타임존·로그 볼륨 설정 검증 요청 환경변수(TZ, JAVA_TOOL_OPTIONS)와 로그 디렉터리 볼륨 마운트 방향성은 적절하나, 아래 부분을 수동으로 반드시 확인해 주세요. Dockerfile에 tzdata 설치 여부 확인 (예: Alpine 기반이라면 apk add --no-cache tzdata 등으로 OS 레벨 타임존 데이터가 포함되어야 합니다.) logback 설정 파일(src/main/resources/**)에서
${LOG_DIR:/app/logs} 형식의 fallback 및 디렉터리 생성 로직 확인 (현재 rg -n "LOG_DIR" -g "src/main/resources/**" 결과가 없어 환경변수 반영 여부를 수동으로 점검해야 합니다.) 호스트 ./logs 디렉터리 권한 상태 확인: drwxr-xr-x (jailuser:jailuser) 로 소유자만 쓰기 가능 → 컨테이너 내 애플리케이션 사용자(비루트)로 실제 쓰기 테스트 필요 컨테이너 기동 후 /app/logs 디렉터리에 비루트 사용자로 로그 생성이 정상 동작하는지 직접 검증 부탁드립니다. src/main/java/com/wayble/server/user/repository/UserPlaceWaybleZoneMappingRepository.java (1) 12-13: N+1 완화에 유효한 @entitygraph 추가 userPlace, waybleZone를 즉시 로딩하도록 지정한 것은 사용자 저장 장소 목록 조회의 N+1을 줄이는 데 도움이 됩니다. 현재 매핑이 모두 @manytoone이라 MultipleBagFetchException 우려도 없습니다. src/main/resources/logback-spring.xml (1) 12-22: 로그 디렉토리 외부화 및 컨테이너 마운트 경로 확인 필요 현재 logs/... 경로가 하드코딩되어 있어, 운영 환경에서 /app/logs 같은 볼륨으로 마운트할 경우 로그가 컨테이너 내부에만 쌓일 수 있습니다. LOG_DIR 프로퍼티를 도입하고, 모든 <file> 및 <fileNamePattern>에 $ {LOG_DIR}를 사용하도록 변경하세요. 수정 파일: src/main/resources/logback-spring.xml 적용 위치: <configuration> 바로 아래에 기본값을 갖는 프로퍼티 추가 각 <appender>의 <file>과 <fileNamePattern> 경로를${LOG_DIR}/… 형식으로 변경 <configuration> + <property name="LOG_DIR" value="$ {LOG_DIR:-logs}" /> … - <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> - <file>logs/wayble-app.log</file> + <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>${LOG_DIR}/wayble-app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> - <fileNamePattern>logs/wayble-app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> + <fileNamePattern>${LOG_DIR}/wayble-app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> … - <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> - <file>logs/wayble-error.log</file> + <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>${LOG_DIR}/wayble-error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> - <fileNamePattern>logs/wayble-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> + <fileNamePattern>${LOG_DIR}/wayble-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> ※ 레포지토리 내 docker-compose.yml·Dockerfile·.github/workflows에서 관련 설정이 검색되지 않아, 실제 볼륨 마운트 경로와 일치 여부를 직접 점검해 주세요. src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepositoryImpl.java (1) 71-77: left join으로 방문 로그 0건 Zone도 TOP3에 포함 — 의도 확인 및 정렬 기준 보강 권장 현재 leftJoin+groupBy 결과, 방문 로그가 없는(0건) Zone도 3위 내에 포함됩니다. “Top3 검색” 의도상 방문 수 0건을 제외해야 하는지 확인해주세요. 동일한 방문 수(특히 0) 간 결과 정렬이 비결정적이므로, 좋아요 수나 ID 등을 추가한 tie-breaker로 안정적인 순서를 보장하세요. 아래와 같이 수정할 수 있습니다: .from(waybleZone) .leftJoin(waybleZoneVisitLog).on(waybleZoneVisitLog.zoneId.eq(waybleZone.id)) .where(waybleZone.address.district.eq(district)) .groupBy(waybleZone.id) - .orderBy(waybleZoneVisitLog.count().desc()) + .having(waybleZoneVisitLog.id.count().gt(0)) + .orderBy( + waybleZoneVisitLog.id.count().desc(), + waybleZone.likes.desc(), + waybleZone.id.desc() + ) .limit(3) .fetch(); 참고: Querydsl에서 waybleZoneVisitLog.id.count()를 쓰면 집계 대상이 명확해집니다. src/main/java/com/wayble/server/user/service/UserPlaceService.java (2) 18-18: 스프링 @transactional로 전환 — 적절한 선택입니다 Spring 트랜잭션 AOP와의 호환 및 readOnly 플래그 활용 측면에서 올바른 전환입니다. 65-95: @entitygraph 검증 완료: N+1 쿼리 및 LazyInitializationException 방지 @transactional(readOnly = true) 지정으로 불필요한 flush를 방지하는 설정이 적절합니다. UserPlaceWaybleZoneMappingRepository의 findAllByUserPlace_User_Id 메서드에 @entitygraph(attributePaths = {"userPlace", "waybleZone"})가 적용되어 있어 연관 엔티티를 한 번의 쿼리로 페치(Fetch)합니다. → N+1 쿼리 및 LazyInitializationException 발생을 회피합니다. 추가 성능 최적화가 필요하다면, 서비스 레이어 매핑 대신 리포지토리에서 인터페이스/클래스 기반 DTO 프로젝션을 활용하는 방안을 고려해 보세요. src/main/java/com/wayble/server/review/repository/ReviewRepository.java (1) 11-15: @entitygraph로 N+1 완화 — 적절합니다. 중복 row/페이징 제약 주의 user(단건)와 reviewImageList(컬렉션)를 함께 페치해 N+1을 완화할 수 있어 적절합니다. 다만 컬렉션 페치 시 SQL row가 크게 늘 수 있으며(Pageable 미사용이라면 문제 없음), 향후 페이징이 필요하면 별도 전략(컬렉션 분리 로딩, @batchsize, 서브쿼리 + 배치 패턴 등)을 고려해 주세요. 향후 페이징 요구가 생길 가능성이 있으면, 리뷰 목록은 리뷰만 페치하고 이미지 목록은 별도 배치 로딩(@batchsize)로 전환하는 대안을 검토해 주세요. src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneFacilityRepository.java (1) 8-11: 리포지토리 메서드 시그니처 적절 — 삭제 트랜잭션 경계만 확인 조회/삭제 메서드 네이밍과 시그니처 모두 적합합니다. deleteByWaybleZone_Id는 서비스 계층 메서드(@transactional) 내에서 호출되어야 원자성 보장이 됩니다(연관 엔티티 갱신/정리와 함께 실행되는 경우 특히 중요). 현재 서비스 레벨에서 트랜잭션 경계가 잡혀있는지 확인해 주세요. src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html (1) 192-260: 시설 정보 UI 개선이 잘 구현되었습니다. 기존의 단순한 텍스트 표시에서 배지 기반의 상세 시설 정보 표시로 개선되었습니다. 각 시설 특성별로 적절한 색상과 아이콘을 사용하여 시각적 구분이 명확하고, 시설 정보가 없을 때의 대체 메시지도 적절히 구현되어 있습니다. src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneOperatingHourRepository.java (1) 9-13: 표준적인 JPA Repository 인터페이스 구현이 적절합니다. Spring Data JPA의 쿼리 메서드 명명 규칙을 올바르게 따르고 있으며, 운영시간 관리에 필요한 기본적인 CRUD 작업들이 적절히 정의되어 있습니다. src/main/java/com/wayble/server/review/entity/Review.java (4) 15-15: JPA 접근 방식을 명시적으로 지정한 것이 좋습니다. 필드 기반 접근 방식을 명시적으로 설정하여 JPA의 동작을 더 명확하게 정의했습니다. 36-36: 데이터베이스 컬럼 명명 규칙을 개선했습니다. Snake_case 네이밍 컨벤션을 사용하여 데이터베이스 표준에 맞게 수정되었습니다. 50-55: 데이터 무결성을 위한 @PrePersist 메서드가 잘 구현되었습니다. null 값과 NaN 값을 적절한 기본값으로 초기화하여 데이터 일관성을 보장합니다. 이는 데이터베이스 제약 조건 위반을 방지하는 좋은 접근입니다. 56-64: 팩토리 메서드의 null 처리 개선이 적절합니다. nullable Double 파라미터를 받아서 null인 경우 기본값으로 처리하는 방식이 방어적 프로그래밍 관점에서 좋습니다. src/main/java/com/wayble/server/review/service/ReviewService.java (2) 32-32: 리뷰 등록 메서드에 트랜잭션 처리가 적절히 적용되었습니다. 리뷰 저장, 평점 업데이트, 이미지 저장 등의 여러 작업이 하나의 트랜잭션으로 묶여서 데이터 일관성이 보장됩니다. 57-57: 읽기 전용 트랜잭션 설정이 성능 최적화에 도움이 됩니다. 조회 전용 메서드에 readOnly = true 설정을 통해 데이터베이스 성능을 최적화했습니다. src/main/resources/templates/admin/log/error-logs.html (1) 139-145: CSRF 보호 비활성화 확인 SecurityConfig.java(37행)에서 .csrf(csrf -> csrf.disable())로 전체 CSRF 보호가 비활성화되어 있습니다. 따라서 해당 로그아웃 폼에 CSRF 토큰 숨은 필드를 추가하더라도 실제 보호가 적용되지 않습니다. CSRF 보호가 필요하다면 SecurityConfig에서 CSRF 비활성화 설정을 제거하세요. 이후 모든 POST 폼(로그아웃 포함)에 아래처럼 토큰을 포함하도록 구현을 검토해주시기 바랍니다. <form th:action="@{/admin/logout}" method="post" class="inline"> + <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <button type="submit">로그아웃</button> </form> Likely an incorrect or invalid review comment. src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java (1) 89-91: 로그 포맷 OK, 스택트레이스 첨부도 적절 마지막 파라미터로 Throwable 전달해 스택트레이스를 기록하는 방식이 올바르게 적용되어 있습니다. src/main/java/com/wayble/server/wayblezone/importer/SeochoCsvImporter.java (2) 50-56: 구성 플래그로 안전하게 게이트 처리됨 wayble.import.enabled 플래그로 실행 여부를 제어하는 구조는 안전하고 명확합니다. LGTM. 153-171: 파일 인코딩/경로 처리 양호 — 클래스패스/파일시스템 모두 지원 UTF-8 고정, classpath/절대경로 분기 모두 적절합니다. LGTM.
| -v /var/log/wayble:/app/logs \ | ||
| -e "SPRING_PROFILES_ACTIVE=develop" \ | ||
| -e "TZ=Asia/Seoul" \ | ||
| ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
CD 런 단계에 LOG_DIR 누락—logback 경로 불일치 가능성
docker-compose에는 LOG_DIR=/app/logs를 설정했지만, GitHub Actions의 컨테이너 실행 단계에는 LOG_DIR가 없습니다. logback이 ${LOG_DIR}를 참조한다면, 배포 경로에서 로그가 예상 위치로 기록되지 않을 수 있습니다.
다음과 같이 -e "LOG_DIR=/app/logs"를 추가해 주세요:
sudo docker run -d \
--name github-actions-demo \
--network wayble-network \
-p 8080:8080 \
- -v /var/log/wayble:/app/logs \
+ -v /var/log/wayble:/app/logs \
-e "SPRING_PROFILES_ACTIVE=develop" \
+ -e "LOG_DIR=/app/logs" \
-e "TZ=Asia/Seoul" \
${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest또한 호스트 디렉터리 생성/권한 보장을 위해 컨테이너 실행 전 단계에 다음을 추가하는 것을 권장합니다:
+ - name: Ensure host log directory exists
+ run: |
+ sudo mkdir -p /var/log/wayble
+ # 필요 시 권한 조정(비루트 컨테이너 사용자 사용 시)
+ sudo chown -R $USER:$USER /var/log/wayble || true다음과 같이 docker-compose 설정에 LOG_DIR가 정의되어 있는지 확인하겠습니다:
🏁 Script executed:
#!/bin/bash
# docker-compose 파일 목록 확인
fd 'docker-compose.*\.yml'
# 리포지토리 전반에서 LOG_DIR 사용 여부 검색
rg -R "LOG_DIR" .Length of output: 470
docker-compose.yml의 environment 섹션 및 LOG_DIR 설정 여부를 확인하기 위해 다음 스크립트를 실행하겠습니다:
🏁 Script executed:
#!/bin/bash
# environment 섹션이 있는 위치 찾기
grep -n "environment" docker-compose.yml
# 해당 위치부터 10줄 출력하여 상세 확인
grep -n "environment" docker-compose.yml | cut -d: -f1 | xargs -I{} sed -n '{},+10p' docker-compose.yml
# LOG_DIR 설정 여부 확인
rg "LOG_DIR" -n docker-compose.yml || echo "LOG_DIR 설정 없음"Length of output: 851
CD 런 단계에 LOG_DIR 환경 변수 및 호스트 디렉터리 생성 추가 필요
docker-compose에서는 LOG_DIR=/app/logs를 설정하고 있지만, GitHub Actions의 docker run 단계에는 해당 환경 변수가 빠져 있어 logback이 ${LOG_DIR}를 참조할 경우 로그가 예상 위치에 쌓이지 않을 수 있습니다. 또한 호스트의 /var/log/wayble 디렉터리가 없거나 권한이 부적절할 경우 컨테이너가 로그를 쓸 수 없습니다. 아래와 같이 수정해 주세요.
- 파일:
.github/workflows/cd-develop.yml - 대상 위치: 147–151행 (
docker run스텝)
sudo docker run -d \
--name github-actions-demo \
--network wayble-network \
-p 8080:8080 \
- -v /var/log/wayble:/app/logs \
- -e "SPRING_PROFILES_ACTIVE=develop" \
- -e "TZ=Asia/Seoul" \
+ -v /var/log/wayble:/app/logs \
+ -e "SPRING_PROFILES_ACTIVE=develop" \
+ -e "LOG_DIR=/app/logs" \
+ -e "TZ=Asia/Seoul" \
${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest- 호스트 로그 디렉터리 생성/권한 보장 스텝 추가
+ - name: Ensure host log directory exists
+ run: |
+ sudo mkdir -p /var/log/wayble
+ # 비루트 사용자로 컨테이너가 로그를 쓸 수 있도록 권한 조정
+ sudo chown -R $USER:$USER /var/log/wayble || true📝 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.
| -v /var/log/wayble:/app/logs \ | |
| -e "SPRING_PROFILES_ACTIVE=develop" \ | |
| -e "TZ=Asia/Seoul" \ | |
| ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest | |
| # Ensure host log directory exists before starting container | |
| - name: Ensure host log directory exists | |
| run: | | |
| sudo mkdir -p /var/log/wayble | |
| # 비루트 사용자로 컨테이너가 로그를 쓸 수 있도록 권한 조정 | |
| sudo chown -R $USER:$USER /var/log/wayble || true | |
| # Deploy container | |
| - name: Run Docker container | |
| run: | | |
| sudo docker run -d \ | |
| --name github-actions-demo \ | |
| --network wayble-network \ | |
| -p 8080:8080 \ | |
| -v /var/log/wayble:/app/logs \ | |
| -e "SPRING_PROFILES_ACTIVE=develop" \ | |
| -e "LOG_DIR=/app/logs" \ | |
| -e "TZ=Asia/Seoul" \ | |
| ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest |
🤖 Prompt for AI Agents
In .github/workflows/cd-develop.yml around lines 147 to 151, add the environment
variable LOG_DIR=/app/logs to the docker run command to match the docker-compose
setup and ensure logback writes logs to the correct directory. Additionally,
before running the container, insert a step to create the host directory
/var/log/wayble if it does not exist and set appropriate permissions so the
container can write logs without permission issues.
| // csv 파서 | ||
| implementation 'org.apache.commons:commons-csv:1.10.0' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
CSV 파서 의존성 추가 적절함. 버전 최신/보안 상태 확인 권장
Apache Commons CSV 도입은 본 PR의 CSV 일괄 등록 기능과 일치합니다. 다만 1.10.0이 조직 표준 및 최신 보안 패치와 부합하는지 확인해 주세요. 필요 시 패치 버전으로 상향을 고려하세요.
다음 웹 질의를 통해 최신 버전 및 1.10.x 관련 CVE 여부를 확인해 주세요:
🌐 Web query:
What is the latest version of Apache Commons CSV, and are there any known CVEs for 1.10.x?
💡 Result:
The latest version of Apache Commons CSV is 1.14.0, released on March 15, 2025. (commons.apache.org)
Regarding known security vulnerabilities (CVEs) for the 1.10.x series, there are no reported issues. The Apache Commons CSV project maintains a security page listing all known vulnerabilities, and as of now, there are no reported vulnerabilities for version 1.10.x. (commons.apache.org)
For the most up-to-date information on security vulnerabilities, you can refer to the Apache Commons CSV security page. (commons.apache.org)
Citations:
- 1: https://commons.apache.org/csv/changes.html?utm_source=openai
- 2: https://commons.apache.org/csv/security.html?utm_source=openai
- 3: https://commons.apache.org/csv/security.html?utm_source=openai
CSV 파서 의존성 버전 검토 및 업그레이드 권장
현재 org.apache.commons:commons-csv:1.10.0에는 알려진 CVE가 없으나, 최신 버전인 1.14.0(2025-03-15 릴리스)가 배포되었습니다. 보안 패치 및 기능 개선을 위해 의존성을 1.14.0으로 상향 검토를 권장드립니다.
- 파일:
build.gradle - 라인: 67–68
🤖 Prompt for AI Agents
In build.gradle around lines 67 to 68, the dependency version for
'org.apache.commons:commons-csv' is set to 1.10.0. Update this version to the
latest stable release 1.14.0 to incorporate recent security patches and
improvements. Modify the implementation line to reflect the new version number.
| @Controller | ||
| @RequestMapping("/admin/logs") | ||
| @RequiredArgsConstructor | ||
| public class AdminLogController { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
엔드포인트 보호 확인: 관리자 전용 접근 제어
컨트롤러가 /admin 하위이긴 하나, 보안 설정에서 실제로 관리자 권한만 접근 가능하도록 설정되어 있는지 확인 필요합니다. 명시적으로 @PreAuthorize를 권장합니다.
@Controller
@RequestMapping("/admin/logs")
@RequiredArgsConstructor
+@org.springframework.security.access.prepost.PreAuthorize("hasRole('ADMIN')")
public class AdminLogController {보안 설정 점검 스크립트:
🏁 Script executed:
#!/bin/bash
# Admin 접근 제어 및 보안 설정 확인
rg -n --hidden -S "@EnableWebSecurity|SecurityFilterChain|WebSecurityConfigurerAdapter|@PreAuthorize|hasRole|ROLE_ADMIN" -A 3 -B 3Length of output: 1832
🏁 Script executed:
#!/bin/bash
# SecurityConfig 내 인증 설정 확인: 관리자 경로 보호 여부
rg -nE "authorize(HttpSecurity|authorizeHttpRequests|authorizeRequests|antMatchers|requestMatchers)" -A 10 -B 3 src/main/java/com/wayble/server/common/config/SecurityConfig.javaLength of output: 308
🏁 Script executed:
#!/bin/bash
# 관리자 엔드포인트 접근 제어 설정 재확인
rg -nE 'antMatchers|requestMatchers|authorizeHttpRequests|authorizeRequests' -A 5 -B 2 src/main/java/com/wayble/server/common/config/SecurityConfig.java
rg -nE 'hasRole|hasAuthority|ROLE_ADMIN' -A 3 -B 3 src/main/java/com/wayble/server/common/config/SecurityConfig.javaLength of output: 447
🏁 Script executed:
#!/bin/bash
# SecurityConfig 파일에서 securityFilterChain 설정 부분 전체 확인
sed -n '1,200p' src/main/java/com/wayble/server/common/config/SecurityConfig.javaLength of output: 3543
관리자 엔드포인트 보안 설정 필요
현재 SecurityConfig에서 “/admin/**” 경로가 permitAll 목록에 포함되어 있어, 관리자 전용 접근 제어가 전혀 적용되지 않고 있습니다. 아래와 같이 수정해주세요.
• SecurityConfig.java
- “/admin/**”를 permitAll 목록에서 제거
- 관리자 전용 엔드포인트를
.requestMatchers("/admin/**").hasRole("ADMIN")등으로 명시적 권한 설정 - 권한 설정 순서상, 관리자 경로가 기타 공개 경로보다 먼저 선언되도록 배치
Diff 예시:
--- a/src/main/java/com/wayble/server/common/config/SecurityConfig.java
+++ b/src/main/java/com/wayble/server/common/config/SecurityConfig.java
@@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- .authorizeHttpRequests(auth -> auth
- .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
- .requestMatchers(
- "/api/v1/users/signup",
- "/api/v1/users/login",
- "/api/v1/users/reissue",
- "/api/v1/users/logout",
- "/api/v1/auth/**",
- "/swagger-ui/**",
- "/v3/api-docs/**",
- "/",
- "/index",
- "/index.html",
- "/admin/**", // ← 제거
- "/api/v1/files/**"
- ).permitAll()
- .anyRequest().authenticated()
- )
+ .authorizeHttpRequests(auth -> auth
+ // CORS preflight
+ .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+ // 관리자 전용 경로 (인증·권한 필수)
+ .requestMatchers("/admin/**").hasRole("ADMIN")
+ // 공개 API
+ .requestMatchers(
+ "/api/v1/users/signup",
+ "/api/v1/users/login",
+ "/api/v1/users/reissue",
+ "/api/v1/users/logout",
+ "/api/v1/auth/**",
+ "/swagger-ui/**",
+ "/v3/api-docs/**",
+ "/",
+ "/index",
+ "/index.html",
+ "/api/v1/files/**"
+ ).permitAll()
+ .anyRequest().authenticated()
+ )• Controller 레벨 방어선 (선택 사항)
@Controller
@RequestMapping("/admin/logs")
@RequiredArgsConstructor
+@org.springframework.security.access.prepost.PreAuthorize("hasRole('ADMIN')")
public class AdminLogController {위 수정 후, 관리자 기능 및 기타 엔드포인트에 대한 권한 정책이 의도대로 동작하는지 반드시 테스트 및 검증을 부탁드립니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/admin/controller/AdminLogController.java
around lines 13 to 16, the admin endpoint security is not properly configured
because "/admin/**" is currently included in permitAll in SecurityConfig. To fix
this, remove "/admin/**" from the permitAll list in SecurityConfig.java, add
explicit role-based access control by configuring
.requestMatchers("/admin/**").hasRole("ADMIN"), and ensure this admin path
matcher is declared before other public path matchers to enforce correct
authorization order. After these changes, test to confirm admin and other
endpoints enforce the intended access controls.
| @RequestParam(defaultValue = "100") int limit, | ||
| Model model | ||
| ) { | ||
| List<ErrorLogDto> errorLogs = logService.getRecentErrorLogs(limit); | ||
| ErrorLogStats stats = logService.getErrorLogStats(); | ||
|
|
||
| model.addAttribute("errorLogs", errorLogs); | ||
| model.addAttribute("stats", stats); | ||
| model.addAttribute("limit", limit); | ||
|
|
||
| return "admin/log/error-logs"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
limit 파라미터 검증/클램핑 추가 권장
UI가 50/100/200/500만 제공하지만 API는 임의의 큰 값이 들어올 수 있습니다. 서비스/파일 I/O 부담을 줄이기 위해 상한/하한을 강제하세요.
@GetMapping("/error")
public String errorLogPage(
- @RequestParam(defaultValue = "100") int limit,
+ @RequestParam(defaultValue = "100") int limit,
Model model
) {
- List<ErrorLogDto> errorLogs = logService.getRecentErrorLogs(limit);
+ int sanitized = sanitizeLimit(limit);
+ List<ErrorLogDto> errorLogs = logService.getRecentErrorLogs(sanitized);
- ErrorLogStats stats = logService.getErrorLogStats();
+ ErrorLogStats stats = logService.getErrorLogStats();
model.addAttribute("errorLogs", errorLogs);
model.addAttribute("stats", stats);
- model.addAttribute("limit", limit);
+ model.addAttribute("limit", sanitized);
return "admin/log/error-logs";
}
+
+private int sanitizeLimit(int limit) {
+ if (limit <= 0) return 100;
+ return Math.min(limit, 500);
+}🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/admin/controller/AdminLogController.java
around lines 25 to 35, the limit parameter from the request is not validated or
clamped, allowing potentially large values that could overload the service or
file I/O. Add validation logic to enforce a minimum and maximum limit value
(e.g., clamp limit to between 50 and 500) before passing it to the service
methods. This ensures the API respects the UI constraints and prevents excessive
resource usage.
| @GetMapping("/error/data") | ||
| @ResponseBody | ||
| public List<ErrorLogDto> getErrorLogs(@RequestParam(defaultValue = "100") int limit) { | ||
| return logService.getRecentErrorLogs(limit); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
데이터 API에도 동일한 limit 검증 적용
프론트에서 limit를 바꿔 호출하므로 동일한 방어 로직이 필요합니다.
@GetMapping("/error/data")
@ResponseBody
public List<ErrorLogDto> getErrorLogs(@RequestParam(defaultValue = "100") int limit) {
- return logService.getRecentErrorLogs(limit);
+ return logService.getRecentErrorLogs(sanitizeLimit(limit));
}📝 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.
| @GetMapping("/error/data") | |
| @ResponseBody | |
| public List<ErrorLogDto> getErrorLogs(@RequestParam(defaultValue = "100") int limit) { | |
| return logService.getRecentErrorLogs(limit); | |
| } | |
| @GetMapping("/error/data") | |
| @ResponseBody | |
| public List<ErrorLogDto> getErrorLogs(@RequestParam(defaultValue = "100") int limit) { | |
| return logService.getRecentErrorLogs(sanitizeLimit(limit)); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/admin/controller/AdminLogController.java
around lines 41 to 45, the getErrorLogs method accepts a limit parameter from
the request without validation. Add validation logic to ensure the limit
parameter is within acceptable bounds (e.g., a maximum allowed value) to prevent
misuse or excessive data requests, similar to other APIs. Implement defensive
checks before passing the limit to logService.getRecentErrorLogs.
| // 상세 조회용: 시설 + 이미지 + 운영시간 | ||
| @EntityGraph(attributePaths = {"facility", "waybleZoneImageList"}) | ||
| @Query(""" | ||
| select distinct z | ||
| from WaybleZone z | ||
| where z.id = :id | ||
| """) | ||
| Optional<WaybleZone> findDetailById(Long id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
주석과 실제 페치 전략이 불일치합니다 (운영시간 미포함).
주석은 "시설 + 이미지 + 운영시간"을 예고하지만, EntityGraph에는 operatingHours가 포함되어 있지 않습니다. 현재 설계(트랜잭션 내 지연로딩)는 정상 동작하나, N+1을 줄이려는 의도였다면 다음 중 하나를 고려해주세요.
- 운영시간까지 즉시 로딩: EntityGraph에 "operatingHours" 추가
- 단, 이미지(컬렉션) + 운영시간(컬렉션) 동시 fetch join은 Hibernate에서 MultipleBagFetchException 위험이 큽니다.
- 권장: 운영시간은 지연로딩 유지 + @batchsize로 N+1 완화
- WaybleZone.operatingHours에 @batchsize(size = 50) 등 적용 권장.
- 또는 운영시간을 별도 리포지토리로 by zoneId 일괄 조회.
주석을 실제 전략과 일치하도록 수정하거나, 위 중 하나로 전략을 명확히 해주세요.
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java
around lines 24 to 31, the comment states that the fetch includes facility,
images, and operating hours, but the EntityGraph only fetches facility and
images, causing a mismatch. To fix this, either update the comment to reflect
only facility and images are fetched, or modify the fetch strategy by adding
"operatingHours" to the EntityGraph if immediate loading is desired (noting the
risk of MultipleBagFetchException), or keep operatingHours lazy-loaded and apply
@BatchSize on the operatingHours collection to reduce N+1 queries, or
alternatively fetch operatingHours separately via another repository method by
zoneId. Choose one approach and update the code and comment accordingly to
ensure consistency.
🛠️ Refactor suggestion
@param 누락 (findDetailById)도 함께 보완 필요
위와 동일한 이유로 id에도 @param 명시를 권장합니다.
-Optional<WaybleZone> findDetailById(Long id);
+Optional<WaybleZone> findDetailById(@Param("id") Long id);🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java
around lines 24 to 31, the method findDetailById(Long id) is missing the @Param
annotation on the id parameter. Add @Param("id") before the id parameter in the
method signature to explicitly bind the query parameter and improve clarity.
| // fetch graph로 시설/이미지/운영시간을 미리 로딩 (웨이블존 상세 조회) | ||
| var zone = waybleZoneRepository.findDetailById(waybleZoneId) | ||
| .orElseThrow(() -> new ApplicationException(WaybleZoneErrorCase.WAYBLE_ZONE_NOT_FOUND)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
주석과 실제 페치 전략이 다릅니다 (운영시간 미포함)
리포지토리 EntityGraph는 facility, waybleZoneImageList만 포함합니다. 운영시간은 트랜잭션 내 지연로딩으로 동작하며 쿼리가 추가로 나갑니다. 기존 이슈(#120)의 MultipleBagFetchException 회피 의도가 있었다면, 아래처럼 명확화하세요.
- 주석을 실제 전략으로 수정(운영시간은 지연로딩)하거나,
- 운영시간 컬렉션에 @batchsize(size=50) 등을 적용해 N+1을 완화,
- 또는 운영시간은 별도 리포지토리로 일괄 조회.
두 개 이상의 bag(List) 컬렉션을 한 번에 fetch join 하는 것은 피하세요.
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/wayblezone/service/WaybleZoneService.java
around lines 64 to 66, the comment states that facilities, images, and operating
hours are preloaded via fetch graph, but the actual fetch strategy only includes
facilities and images; operating hours are lazily loaded causing extra queries.
To fix this, either update the comment to reflect that operating hours are
lazily loaded, or apply @BatchSize(size=50) on the operating hours collection to
reduce N+1 queries, or alternatively fetch operating hours separately via
another repository method. Avoid fetch joining multiple bag collections
simultaneously to prevent MultipleBagFetchException.
| try { | ||
| return WaybleZoneType.valueOf(category.toUpperCase()); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new ApplicationException(WaybleZoneErrorCase.INVALID_CATEGORY); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
버그: 공백이 있는 영문 카테고리 실패 (trim 적용 누락)
valueOf 호출에 원본 category를 사용해, "cafe "처럼 공백이 있으면 INVALID 처리됩니다. 위에서 만든 v(=trim().toLowerCase())를 활용해야 합니다.
- return WaybleZoneType.valueOf(category.toUpperCase());
+ return WaybleZoneType.valueOf(v.toUpperCase());📝 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.
| try { | |
| return WaybleZoneType.valueOf(category.toUpperCase()); | |
| } catch (IllegalArgumentException e) { | |
| throw new ApplicationException(WaybleZoneErrorCase.INVALID_CATEGORY); | |
| } | |
| try { | |
| return WaybleZoneType.valueOf(v.toUpperCase()); | |
| } catch (IllegalArgumentException e) { | |
| throw new ApplicationException(WaybleZoneErrorCase.INVALID_CATEGORY); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/wayblezone/service/WaybleZoneService.java
lines 120 to 124, the code uses category.toUpperCase() directly in valueOf,
which fails if the category string has leading or trailing spaces. To fix this,
trim the category string before converting it to uppercase by using the already
trimmed and lowercased variable v (or apply trim() before toUpperCase()) so that
inputs like "cafe " are correctly processed and do not throw an INVALID_CATEGORY
error.
| container.innerHTML = logs.map(log => ` | ||
| <div class="log-entry"> | ||
| <div class="log-header"> | ||
| <span class="log-timestamp">${new Date(log.timestamp).toLocaleString()}</span> | ||
| <span class="log-level">${log.level}</span> | ||
| </div> | ||
| ${log.method && log.method.trim() ? ` | ||
| <div class="flex items-center space-x-4 mb-2"> | ||
| <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium">${log.method}</span> | ||
| <span class="text-blue-600 font-mono text-sm">${log.path || ''}</span> | ||
| </div> | ||
| ` : ''} | ||
| ${log.stackTrace && log.stackTrace.trim() ? ` | ||
| <div class="mb-2"> | ||
| <span class="bg-red-100 text-red-800 px-2 py-1 rounded text-xs font-medium">📍</span> | ||
| <span class="text-red-600 font-mono text-sm ml-2 log-stack-trace">${log.stackTrace}</span> | ||
| </div> | ||
| ` : ''} | ||
| ${log.exception && log.exception.trim() && log.exception !== log.message ? ` | ||
| <div class="log-exception-detail">${log.exception}</div> | ||
| ` : ''} | ||
| <div class="log-logger">${log.logger}</div> | ||
| <div class="log-message">${log.message}</div> | ||
| </div> | ||
| `).join(''); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
크리티컬: innerHTML 기반 로그 렌더링은 XSS 위험이 큽니다
로그 메시지/경로/예외 등은 사용자 입력이 포함될 수 있어 innerHTML로 직접 주입 시 Stored/Reflected XSS가 발생할 수 있습니다. 텍스트는 반드시 textContent로 넣거나, 검증된 sanitizer(예: DOMPurify)로 정화하세요.
안전한 DOM 빌드 예시(교체):
- } else {
- container.innerHTML = logs.map(log => `
- <div class="log-entry">
- <div class="log-header">
- <span class="log-timestamp">${new Date(log.timestamp).toLocaleString()}</span>
- <span class="log-level">${log.level}</span>
- </div>
- ${log.method && log.method.trim() ? `
- <div class="flex items-center space-x-4 mb-2">
- <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium">${log.method}</span>
- <span class="text-blue-600 font-mono text-sm">${log.path || ''}</span>
- </div>
- ` : ''}
- ${log.stackTrace && log.stackTrace.trim() ? `
- <div class="mb-2">
- <span class="bg-red-100 text-red-800 px-2 py-1 rounded text-xs font-medium">📍</span>
- <span class="text-red-600 font-mono text-sm ml-2 log-stack-trace">${log.stackTrace}</span>
- </div>
- ` : ''}
- ${log.exception && log.exception.trim() && log.exception !== log.message ? `
- <div class="log-exception-detail">${log.exception}</div>
- ` : ''}
- <div class="log-logger">${log.logger}</div>
- <div class="log-message">${log.message}</div>
- </div>
- `).join('');
- }
+ } else {
+ const frag = document.createDocumentFragment();
+ logs.forEach(log => {
+ const entry = document.createElement('div');
+ entry.className = 'log-entry';
+
+ const header = document.createElement('div');
+ header.className = 'log-header';
+ const ts = document.createElement('span');
+ ts.className = 'log-timestamp';
+ ts.textContent = new Date(log.timestamp).toLocaleString();
+ const lvl = document.createElement('span');
+ lvl.className = 'log-level';
+ lvl.textContent = log.level || '';
+ header.append(ts, lvl);
+ entry.appendChild(header);
+
+ if (log.method && log.method.trim()) {
+ const http = document.createElement('div');
+ http.className = 'flex items-center space-x-4 mb-2';
+ const m = document.createElement('span');
+ m.className = 'bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium';
+ m.textContent = log.method;
+ const p = document.createElement('span');
+ p.className = 'text-blue-600 font-mono text-sm';
+ p.textContent = log.path || '';
+ http.append(m, p);
+ entry.appendChild(http);
+ }
+
+ if (log.stackTrace && log.stackTrace.trim()) {
+ const st = document.createElement('div');
+ st.className = 'mb-2';
+ const pin = document.createElement('span');
+ pin.className = 'bg-red-100 text-red-800 px-2 py-1 rounded text-xs font-medium';
+ pin.textContent = '📍';
+ const loc = document.createElement('span');
+ loc.className = 'text-red-600 font-mono text-sm ml-2 log-stack-trace';
+ loc.textContent = log.stackTrace;
+ st.append(pin, loc);
+ entry.appendChild(st);
+ }
+
+ if (log.exception && log.exception.trim() && log.exception !== log.message) {
+ const ex = document.createElement('div');
+ ex.className = 'log-exception-detail';
+ ex.textContent = log.exception;
+ entry.appendChild(ex);
+ }
+
+ const logger = document.createElement('div');
+ logger.className = 'log-logger';
+ logger.textContent = log.logger || '';
+ const msg = document.createElement('div');
+ msg.className = 'log-message';
+ msg.textContent = log.message || '';
+ entry.append(logger, msg);
+ frag.appendChild(entry);
+ });
+ container.innerHTML = '';
+ container.appendChild(frag);
+ }선호안: DOMPurify 등을 도입하면 템플릿 문자열 유지도 가능하나, 관리자 페이지라도 신뢰하지 않는 입력은 무조건 escape가 원칙입니다.
📝 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.
| container.innerHTML = logs.map(log => ` | |
| <div class="log-entry"> | |
| <div class="log-header"> | |
| <span class="log-timestamp">${new Date(log.timestamp).toLocaleString()}</span> | |
| <span class="log-level">${log.level}</span> | |
| </div> | |
| ${log.method && log.method.trim() ? ` | |
| <div class="flex items-center space-x-4 mb-2"> | |
| <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium">${log.method}</span> | |
| <span class="text-blue-600 font-mono text-sm">${log.path || ''}</span> | |
| </div> | |
| ` : ''} | |
| ${log.stackTrace && log.stackTrace.trim() ? ` | |
| <div class="mb-2"> | |
| <span class="bg-red-100 text-red-800 px-2 py-1 rounded text-xs font-medium">📍</span> | |
| <span class="text-red-600 font-mono text-sm ml-2 log-stack-trace">${log.stackTrace}</span> | |
| </div> | |
| ` : ''} | |
| ${log.exception && log.exception.trim() && log.exception !== log.message ? ` | |
| <div class="log-exception-detail">${log.exception}</div> | |
| ` : ''} | |
| <div class="log-logger">${log.logger}</div> | |
| <div class="log-message">${log.message}</div> | |
| </div> | |
| `).join(''); | |
| } | |
| } else { | |
| const frag = document.createDocumentFragment(); | |
| logs.forEach(log => { | |
| const entry = document.createElement('div'); | |
| entry.className = 'log-entry'; | |
| const header = document.createElement('div'); | |
| header.className = 'log-header'; | |
| const ts = document.createElement('span'); | |
| ts.className = 'log-timestamp'; | |
| ts.textContent = new Date(log.timestamp).toLocaleString(); | |
| const lvl = document.createElement('span'); | |
| lvl.className = 'log-level'; | |
| lvl.textContent = log.level || ''; | |
| header.append(ts, lvl); | |
| entry.appendChild(header); | |
| if (log.method && log.method.trim()) { | |
| const http = document.createElement('div'); | |
| http.className = 'flex items-center space-x-4 mb-2'; | |
| const m = document.createElement('span'); | |
| m.className = 'bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium'; | |
| m.textContent = log.method; | |
| const p = document.createElement('span'); | |
| p.className = 'text-blue-600 font-mono text-sm'; | |
| p.textContent = log.path || ''; | |
| http.append(m, p); | |
| entry.appendChild(http); | |
| } | |
| if (log.stackTrace && log.stackTrace.trim()) { | |
| const st = document.createElement('div'); | |
| st.className = 'mb-2'; | |
| const pin = document.createElement('span'); | |
| pin.className = 'bg-red-100 text-red-800 px-2 py-1 rounded text-xs font-medium'; | |
| pin.textContent = '📍'; | |
| const loc = document.createElement('span'); | |
| loc.className = 'text-red-600 font-mono text-sm ml-2 log-stack-trace'; | |
| loc.textContent = log.stackTrace; | |
| st.append(pin, loc); | |
| entry.appendChild(st); | |
| } | |
| if (log.exception && log.exception.trim() && log.exception !== log.message) { | |
| const ex = document.createElement('div'); | |
| ex.className = 'log-exception-detail'; | |
| ex.textContent = log.exception; | |
| entry.appendChild(ex); | |
| } | |
| const logger = document.createElement('div'); | |
| logger.className = 'log-logger'; | |
| logger.textContent = log.logger || ''; | |
| const msg = document.createElement('div'); | |
| msg.className = 'log-message'; | |
| msg.textContent = log.message || ''; | |
| entry.append(logger, msg); | |
| frag.appendChild(entry); | |
| }); | |
| container.innerHTML = ''; | |
| container.appendChild(frag); | |
| } |
🤖 Prompt for AI Agents
In src/main/resources/templates/admin/log/error-logs.html around lines 259 to
284, the code uses innerHTML to inject log data directly, which poses a serious
XSS risk because log fields may contain untrusted user input. To fix this,
replace innerHTML usage with safe DOM manipulation methods by creating elements
and setting their textContent for all dynamic text fields like log message,
path, exception, and stack trace. Alternatively, integrate a sanitizer like
DOMPurify to clean the HTML before injecting it, ensuring no malicious scripts
can execute.
| <div class="mt-2"> | ||
| <div class="flex flex-wrap gap-2"> | ||
| <!-- 경사로 --> | ||
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasSlope}" | ||
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800"> | ||
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/> | ||
| </svg> | ||
| 경사로 | ||
| </div> | ||
|
|
||
| <!-- 문턱없음 --> | ||
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasNoDoorStep}" | ||
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> | ||
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/> | ||
| </svg> | ||
| 문턱없음 | ||
| </div> | ||
|
|
||
| <!-- 엘리베이터 --> | ||
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasElevator}" | ||
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800"> | ||
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/> | ||
| </svg> | ||
| 엘리베이터 | ||
| </div> | ||
|
|
||
| <!-- 테이블석 --> | ||
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasTableSeat}" | ||
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800"> | ||
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/> | ||
| </svg> | ||
| 테이블석 | ||
| </div> | ||
|
|
||
| <!-- 장애인화장실 --> | ||
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasDisabledToilet}" | ||
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800"> | ||
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/> | ||
| </svg> | ||
| 장애인화장실 | ||
| </div> | ||
|
|
||
| <!-- 층수 정보 --> | ||
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.floorInfo != null}" | ||
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800"> | ||
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/> | ||
| </svg> | ||
| <span th:text="${zone.facilityInfo.floorInfo}"></span> | ||
| </div> | ||
|
|
||
| <!-- 시설정보 없음 --> | ||
| <div th:if="${zone.facilityInfo == null}" | ||
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-500"> | ||
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | ||
| </svg> | ||
| 시설정보 없음 | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
시설정보 ‘없음’ 처리 보완 제안
현재 facilityInfo가 존재하지만 모든 속성이 비활성(false)이고 층수 정보도 없으면 아무 뱃지도 노출되지 않습니다. 이 경우에도 ‘시설정보 없음’을 표시하도록 조건을 확장해 주세요.
예시 수정안:
- <!-- 시설정보 없음 -->
- <div th:if="${zone.facilityInfo == null}"
+ <!-- 시설정보 없음: facilityInfo가 없거나, 모든 플래그가 false이며 층수 정보도 비어있을 때 -->
+ <div th:if="${zone.facilityInfo == null or (!zone.facilityInfo.hasSlope and !zone.facilityInfo.hasNoDoorStep and !zone.facilityInfo.hasElevator and !zone.facilityInfo.hasTableSeat and !zone.facilityInfo.hasDisabledToilet and (zone.facilityInfo.floorInfo == null or #strings.isEmpty(zone.facilityInfo.floorInfo)))}"
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-500">추가로 접근성 측면에서 아이콘 svg에는 aria-hidden="true"를, 텍스트가 없는 경우엔 sr-only 보조 텍스트를 고려해 주세요.
📝 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.
| <div class="mt-2"> | |
| <div class="flex flex-wrap gap-2"> | |
| <!-- 경사로 --> | |
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasSlope}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/> | |
| </svg> | |
| 경사로 | |
| </div> | |
| <!-- 문턱없음 --> | |
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasNoDoorStep}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/> | |
| </svg> | |
| 문턱없음 | |
| </div> | |
| <!-- 엘리베이터 --> | |
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasElevator}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/> | |
| </svg> | |
| 엘리베이터 | |
| </div> | |
| <!-- 테이블석 --> | |
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasTableSeat}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/> | |
| </svg> | |
| 테이블석 | |
| </div> | |
| <!-- 장애인화장실 --> | |
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.hasDisabledToilet}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/> | |
| </svg> | |
| 장애인화장실 | |
| </div> | |
| <!-- 층수 정보 --> | |
| <div th:if="${zone.facilityInfo != null and zone.facilityInfo.floorInfo != null}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/> | |
| </svg> | |
| <span th:text="${zone.facilityInfo.floorInfo}"></span> | |
| </div> | |
| <!-- 시설정보 없음 --> | |
| <div th:if="${zone.facilityInfo == null}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-500"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | |
| </svg> | |
| 시설정보 없음 | |
| </div> | |
| </div> | |
| <!-- 시설정보 없음: facilityInfo가 없거나, 모든 플래그가 false이며 층수 정보도 비어있을 때 --> | |
| <div th:if="${zone.facilityInfo == null | |
| or (!zone.facilityInfo.hasSlope | |
| and !zone.facilityInfo.hasNoDoorStep | |
| and !zone.facilityInfo.hasElevator | |
| and !zone.facilityInfo.hasTableSeat | |
| and !zone.facilityInfo.hasDisabledToilet | |
| and (zone.facilityInfo.floorInfo == null | |
| or #strings.isEmpty(zone.facilityInfo.floorInfo)))}" | |
| class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-500"> | |
| <svg class="h-3 w-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | |
| </svg> | |
| 시설정보 없음 | |
| </div> |
🤖 Prompt for AI Agents
In src/main/resources/templates/admin/wayblezone/wayble-zones.html between lines
124 and 188, update the condition for displaying the '시설정보 없음' badge to also
show when facilityInfo exists but all its boolean properties are false and
floorInfo is null. Modify the th:if condition to check for this scenario in
addition to facilityInfo being null. Also, add aria-hidden="true" to all SVG
icons for accessibility and include sr-only text for icons that do not have
visible text to assist screen readers.
✔️ 연관 이슈
📝 작업 내용
Summary by CodeRabbit
신규 기능
UI/UX 개선
버그 수정 및 개선
환경 및 설정
기타