Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public ResponseEntity<BaseResponse<?>> getUserBuyTicketInfo(
}

//도어용 티켓 검증 api
@PostMapping("/confirm")
@PostMapping("/door/staff/confirm")
public ResponseEntity<BaseResponse<?>> getUserBuyTicketInfo(
@RequestBody @Valid TicketConfirmRequest ticketConfirmRequest
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ public static List<Ticket> generatePublicTickets(final List<ReservationTicket> r

return reservationTicketList.stream()
.flatMap(reservationTicket -> {
// 1️⃣ 현재 예약 티켓의 타입 ID로 대응되는 TicketTypeEntity 찾기
// 현재 예약 티켓의 타입 ID로 대응되는 TicketTypeEntity 찾기
final TicketTypeEntity ticketTypeEntity = ticketTypeEntities.stream()
.filter(t -> t.getTicketTypeId() == reservationTicket.getTicketTypeId())
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("해당 ticketTypeId에 대한 TicketTypeEntity를 찾을 수 없습니다."));

// 2️⃣ 실제 티켓 생성
// 실제 티켓 생성
return IntStream.range(0, reservationTicket.getCount())
.mapToObj(i -> Ticket.builder()
.userId(userId)
Expand All @@ -38,8 +38,8 @@ public static List<Ticket> generatePublicTickets(final List<ReservationTicket> r
.status(TicketStatus.RESERVED)
.ticketPrice(
hasCoupon
? reservation.getTotalAmount() // ✅ 쿠폰 사용 시: 전체 예약 금액
: ticketTypeEntity.getTicketPrice() // ✅ 일반 결제 시: 티켓 단가
? reservation.getTotalAmount()
: ticketTypeEntity.getTicketPrice()
)
Comment on lines +41 to 43
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

쿠폰 사용 시 각 티켓 가격을 예약 총액으로 설정 — 과금 오류 위험

hasCoupon일 때 개별 티켓의 ticketPricereservation.getTotalAmount()(예약 전체 금액)로 설정됩니다. 여러 장 생성 시 각 티켓이 총액을 개별적으로 갖게 되어 과금/정산 오류가 발생할 수 있습니다.

빠른 안전조치(권장):

-                                            hasCoupon
-                                                    ? reservation.getTotalAmount()
-                                                    : ticketTypeEntity.getTicketPrice()
+                                            ticketTypeEntity.getTicketPrice()

정확한 할인 배분이 필요하면, 전체 원가 합계 대비 reservation.getTotalAmount() 비율로 티켓 단가를 비례 배분(반올림 규칙 포함)하는 로직을 별도 단계에서 계산해 주입하는 방식을 제안드립니다. 비즈니스 룰 확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# Find and display TicketGenerator.java with context around lines 41-43
fd 'TicketGenerator.java' --type f -x cat {}

Length of output: 2618


🏁 Script executed:

#!/bin/bash
# Search for ticketPrice usage patterns to understand how it's used
rg -n 'ticketPrice' --type java -C 5

Length of output: 18145


🏁 Script executed:

#!/bin/bash
# Search for coupon/discount related logic
rg -n -i 'hasCoupon|coupon|discount' --type java -C 3 | head -100

Length of output: 13848


🏁 Script executed:

#!/bin/bash
# Check how many tickets are created per reservation
rg -n 'new.*Ticket|createTicket|for.*ticket' --type java -C 2 | head -80

Length of output: 10511


🏁 Script executed:

#!/bin/bash
# Look for getTotalAmount usage
rg -n 'getTotalAmount' --type java -C 3

Length of output: 7105


🏁 Script executed:

#!/bin/bash
# Search for getTicketPrice usage
rg -n 'getTicketPrice' --type java -C 3

Length of output: 8937


쿠폰 적용 시 각 티켓 가격이 예약 총액으로 설정됨 — 중복 청구 위험 (치명적)

hasCoupon일 때 IntStream.range(0, reservationTicket.getCount())를 통해 생성되는 각 티켓이 reservation.getTotalAmount()(예약 전체 금액)로 설정됩니다. 별도 검증으로 쿠폰 구매는 티켓 1장으로 제한되어 있지만, 제너레이터가 이를 강제하지 않아 다음과 같은 위험이 있습니다:

  • 검증 우회/실패 시 여러 장이 생성되면 각 티켓이 전체 금액을 개별 보유
  • 예: 3장 구매, 쿠폰 적용, 총액 30,000원 → 각 티켓 ticketPrice = 30,000원 → DB 합계 = 90,000원 (300% 과금)
  • ticketPrice는 DB 저장 및 정산 쿼리(sumTicketPriceByTicketTypeIdAndStatuses)에 사용되므로 회계 데이터 오염

권장 조치:

검증 실패 시에도 안전하도록 제너레이터 내부에 방어 로직 추가:

ticketPrice(
-    hasCoupon
-        ? reservation.getTotalAmount()
-        : ticketTypeEntity.getTicketPrice()
+    ticketTypeEntity.getTicketPrice()  // 기본값 사용
)

또는 쿠폰 할인을 별도 단계에서 명시적으로 처리(금액 비례 배분, 반올림 규칙 포함)하고 검증 계층에서만 쿠폰 복수 구매 방지.

📝 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
? reservation.getTotalAmount()
: ticketTypeEntity.getTicketPrice()
)
ticketTypeEntity.getTicketPrice()
)
🤖 Prompt for AI Agents
In
src/main/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketGenerator.java
around lines 41-43, the generator sets each ticket's price to
reservation.getTotalAmount() when hasCoupon is true, causing overcharging if
multiple tickets are created; add defensive logic so that when a coupon is
applied you do NOT assign the entire reservation total to every ticket: either
enforce a single-ticket rule inside the generator (treat count>1 as count=1 for
coupon price), or compute a per-ticket share = reservation.getTotalAmount() /
actualCount with a deterministic rounding strategy (e.g., floor for all and add
remainder to first ticket) and assign ticketPrice per ticket accordingly;
implement the check and per-ticket pricing calculation in this method so even if
upstream validation fails, DB-stored ticketPrice values remain correct and
accounting queries stay accurate.

.build());
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class SecurityConfig {
"/api/events/*/timetables",
"/api/events/timetables/*",
"/api/tickets/info/*",
"/api/tickets/confirm"
"/api/tickets/door/staff/confirm"
};

private static final String[] adminURIList = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,38 @@ public class DevRequestResponseLoggingFilter implements Filter {

private final DiscordSender discordSender;

private final static String HEALTH_CHECK_URL = "/actuator/health";

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

// 요청,응답을 여러 번 읽을 수 있도록 래핑
final ContentCachingRequestWrapper req = new ContentCachingRequestWrapper((HttpServletRequest) request);
final ContentCachingResponseWrapper res = new ContentCachingResponseWrapper((HttpServletResponse) response);

Exception exception = null;

final long start = System.currentTimeMillis();
try {
chain.doFilter(req, res);
} finally {
} catch (Exception e) {
exception = e;
log.error("🚨 [DevRequestResponseLoggingFilter] 요청 중 예외 발생: {}", e.getMessage(), e);
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
finally {
final long duration = System.currentTimeMillis() - start;
final String uri = req.getRequestURI();
if (!uri.startsWith("/actuator/health")) {
final String jsonLog = HttpReqResLogJsonBuilder.buildJsonLog(req, res, duration);
if (!uri.startsWith(HEALTH_CHECK_URL)) {
final String jsonLog = HttpReqResLogJsonBuilder.buildJsonLog(req, res, duration, exception);
discordSender.send(jsonLog);
}
res.copyBodyToResponse();
}

if (exception != null) {
if (exception instanceof ServletException se) throw se;
if (exception instanceof IOException ioe) throw ioe;
throw new ServletException(exception);
}
Comment on lines +38 to +57
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

예외 재던지기(rethrow) 전에 응답을 커밋함 → double‑commit 위험

catch에서 상태코드 500 설정 후 finally에서 copyBodyToResponse()로 응답을 커밋하고, 그 다음 예외를 다시 던지면 DispatcherServlet/에러 처리기가 추가로 에러 바디를 쓰지 못해 경고가 나거나 동작이 꼬일 수 있습니다. 예외 발생 시에는 응답 복사/커밋을 건너뛰고, 상태코드 설정도 이 필터가 아닌 예외 처리기에게 맡기는 편이 안전합니다.

권장 패치:

-        } catch (Exception e) {
+        } catch (Exception e) {
             exception = e;
             log.error("🚨 [DevRequestResponseLoggingFilter] 요청 중 예외 발생: {}", e.getMessage(), e);
-            res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-        }
-        finally {
+        }
+        finally {
             final long duration = System.currentTimeMillis() - start;
             final String uri = req.getRequestURI();
             if (!uri.startsWith(HEALTH_CHECK_URL)) {
                 final String jsonLog = HttpReqResLogJsonBuilder.buildJsonLog(req, res, duration, exception);
-                discordSender.send(jsonLog);
+                try {
+                    discordSender.send(jsonLog);
+                } catch (Exception sendEx) {
+                    log.warn("Discord 전송 실패: {}", sendEx.toString());
+                }
             }
-            res.copyBodyToResponse();
+            if (exception == null) {
+                res.copyBodyToResponse();
+            }
         }
 
         if (exception != null) {
             if (exception instanceof ServletException se) throw se;
             if (exception instanceof IOException ioe) throw ioe;
             throw new ServletException(exception);
         }
🤖 Prompt for AI Agents
In
src/main/java/com/permitseoul/permitserver/global/filter/DevRequestResponseLoggingFilter.java
around lines 38-57, the filter currently sets the response status in the catch
block and always calls res.copyBodyToResponse() in finally, which can commit the
response before the exception is rethrown; remove the
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) from the catch (keep
capturing the exception and logging), and change the finally block so that when
exception != null you do NOT call res.copyBodyToResponse() (and avoid committing
the response); only build/send the JSON log and copy the buffered body to the
actual response when exception == null (if you still want to notify Discord on
exceptions, send a log without committing the response), then after finally
rethrow the captured exception as currently implemented.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ public final class HttpReqResLogJsonBuilder {
private static final String FIELD_DURATION = "duration_ms";
private static final String FIELD_REQUEST_BODY = "request_body";
private static final String FIELD_RESPONSE_BODY = "response_body";
private static final String FIELD_EXCEPTION = "exception";
private static final String FIELD_STACKTRACE = "stacktrace";
private static final String LOG_TYPE_HTTP = "HTTP";

public static String buildJsonLog(final ContentCachingRequestWrapper request,
final ContentCachingResponseWrapper response,
final long duration) {
final long duration,
final Exception exception) {
try {
final Map<String, Object> logMap = new LinkedHashMap<>();

Expand All @@ -47,6 +50,12 @@ public static String buildJsonLog(final ContentCachingRequestWrapper request,
logMap.put(FIELD_REQUEST_BODY, extractBody(request.getContentAsByteArray()));
logMap.put(FIELD_RESPONSE_BODY, extractBody(response.getContentAsByteArray()));


if (exception != null) {
logMap.put(FIELD_EXCEPTION, exception.getClass().getSimpleName() + ": " + exception.getMessage());
logMap.put(FIELD_STACKTRACE, getStackTraceAsString(exception));
}
Comment on lines +53 to +57
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

예외/스택트레이스 로그 추가는 좋습니다. 다만 민감정보 마스킹이 필요합니다.

요청/응답 바디, 예외 메시지에 비밀번호·토큰·이메일 등 PII/시크릿이 포함될 수 있습니다. Discord로 외부 전송되는 만큼 기본 마스킹을 추가해 주세요(키 기반 마스킹: password, pass, pwd, token, accessToken, refreshToken, authorization, email, phone 등).

예시(핵심 아이디어):

  • JSON이면 ObjectMapper로 파싱 → 키 매칭 시 값 마스킹 → 문자열화
  • 비‑JSON은 정규식 기반 최소 마스킹

원하는 경우 보조 헬퍼 redactSensitive(String)/redactSensitive(Map<String,Object>)를 만들어 FIELD_REQUEST_BODY/FIELD_RESPONSE_BODY/FIELD_EXCEPTION 적용이 필요합니다.


return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(logMap);
} catch (Exception e) {
return "{\"error\":\"failed to format log\"}";
Expand All @@ -60,4 +69,16 @@ private static String extractBody(byte[] arr) {
? body.substring(0, MAX_LENGTH) + OVER_MAX_LENGTH
: body;
}

private static String getStackTraceAsString(final Exception e) {
final StringBuilder sb = new StringBuilder();
for (StackTraceElement el : e.getStackTrace()) {
sb.append(el.toString()).append("\n");
if (sb.length() > 1500) { // Discord 메시지 길이 제한 방지
sb.append("...more");
break;
}
}
return sb.toString();
}
}