Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.time.LocalDateTime;

@Entity
@Getter
@Table(name = "guests_tickets")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GuestTicketEntity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface GuestTicketRepository extends JpaRepository<GuestTicketEntity, Long> {

Optional<GuestTicketEntity> findByGuestTicketCode(final String guestTicketCode);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.permitseoul.permitserver.domain.admin.timetable.base.api.dto.res;

public record TimetableInfoResponse(
String eventId,
long timetableId,
String timetableStartDate, // (2025-11-03)
String timetableStartTime, //(15:30)
Expand All @@ -10,7 +11,8 @@ public record TimetableInfoResponse(
String notionCategoryDataSourceId,
String notionStageDataSourceId
) {
public static TimetableInfoResponse of(final long timetableId,
public static TimetableInfoResponse of(final String eventId,
final long timetableId,
final String timetableStartDate,
final String timetableStartTime,
final String timetableEndDate,
Expand All @@ -19,6 +21,7 @@ public static TimetableInfoResponse of(final long timetableId,
final String notionCategoryDataSourceId,
final String notionStageDataSourceId) {
return new TimetableInfoResponse(
eventId,
timetableId,
timetableStartDate,
timetableStartTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.permitseoul.permitserver.global.external.notion.exception.NotFoundNotionResponseException;
import com.permitseoul.permitserver.global.response.code.ErrorCode;
import com.permitseoul.permitserver.global.util.LocalDateTimeFormatterUtil;
import com.permitseoul.permitserver.global.util.SecureUrlUtil;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -34,6 +35,7 @@ public class AdminTimetableService {
private final AdminTimetableFacade adminTimetableFacade;
private final AdminTimetableRetriever adminTimetableRetriever;
private final AdminTimetableUpdater adminTimetableUpdater;
private final SecureUrlUtil secureUrlUtil;

public void saveInitialTimetableInfo(final long eventId,
final LocalDateTime timetableStartAt,
Expand Down Expand Up @@ -94,6 +96,7 @@ public TimetableInfoResponse getTimetableInfo(final long eventId) {
}

return TimetableInfoResponse.of(
secureUrlUtil.encode(eventId),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for getTimetableInfo method definition and usages
rg -n --type java 'getTimetableInfo' -B 2 -A 10

Repository: PERMIT-SEOUL/permit-server

Length of output: 4130


🏁 Script executed:

#!/bin/bash
# Find TimetableInfoResponse class definition
fd -t f 'TimetableInfoResponse.java'

Repository: PERMIT-SEOUL/permit-server

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Look at the AdminTimetableService file around line 99 to understand context
sed -n '85,115p' src/main/java/com/permitseoul/permitserver/domain/admin/timetable/base/api/service/AdminTimetableService.java

Repository: PERMIT-SEOUL/permit-server

Length of output: 1467


🏁 Script executed:

#!/bin/bash
# Examine TimetableInfoResponse class
cat src/main/java/com/permitseoul/permitserver/domain/admin/timetable/base/api/dto/res/TimetableInfoResponse.java

Repository: PERMIT-SEOUL/permit-server

Length of output: 1598


🏁 Script executed:

#!/bin/bash
# Check git history to see if eventId field was recently added
cd src/main/java/com/permitseoul/permitserver/domain/admin/timetable/base/api/dto/res && \
git log --oneline -20 -- TimetableInfoResponse.java 2>/dev/null || echo "Git history not available"

Repository: PERMIT-SEOUL/permit-server

Length of output: 117


API 응답 구조 변경 사항을 검토하세요.

TimetableInfoResponseeventId 필드가 새로 추가되었으며, 이로 인해 기존 API 응답 구조가 변경되었습니다. 이 변경사항은 API 응답 스키마를 변경하므로, 클라이언트(프론트엔드, 외부 시스템 등)의 JSON 직렬화/역직렬화 로직에 영향을 미칠 수 있습니다. 버전 관리나 하위 호환성 유지 방안을 검토하세요.

timetable.getTimetableId(),
LocalDateTimeFormatterUtil.formatyyyyMMdd(timetable.getStartAt()),
LocalDateTimeFormatterUtil.formatHHmm(timetable.getStartAt()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
public final class QrCodeUtil {
private static final int QR_SIZE = 256;
private static final String PNG = "PNG";
private static final String URL_PATH = "/entry/";
private static final String URL_PATH = "/entry/guest";
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

🏁 Script executed:

find . -name "QrCodeUtil.java" -type f -exec wc -l {} \;

Repository: PERMIT-SEOUL/permit-server

Length of output: 151


🏁 Script executed:

find . -name "QrCodeUtil.java" -type f -exec cat -n {} \;

Repository: PERMIT-SEOUL/permit-server

Length of output: 2509


🏁 Script executed:

# More specific search for /entry/ paths in controllers
rg -n --type=java '"/entry' -B 5 -A 5

Repository: PERMIT-SEOUL/permit-server

Length of output: 1349


🏁 Script executed:

# Search for any @GetMapping or @PostMapping with path variables
rg -n --type=java '@(Get|Post|Put|Patch|Delete)Mapping' -B 2 -A 5 | grep -E '(Mapping|entry|guest|ticketCode|@)' | head -100

Repository: PERMIT-SEOUL/permit-server

Length of output: 15668


🏁 Script executed:

# Search for any route or endpoint that contains "/entry"
rg -n --type=java 'RequestMapping|RestController|Controller' ./src/main/java/com/permitseoul/permitserver/domain/guest/ -B 1 -A 5

Repository: PERMIT-SEOUL/permit-server

Length of output: 2622


🏁 Script executed:

# Search for any TODO or FIXME comments related to entry or guest
rg -n 'TODO|FIXME' --type=java | grep -i 'entry\|guest' | head -20

Repository: PERMIT-SEOUL/permit-server

Length of output: 52


🏁 Script executed:

# Check git log for recent changes to QrCodeUtil
git log --oneline -20 -- src/main/java/com/permitseoul/permitserver/domain/admin/util/QrCodeUtil.java 2>/dev/null || echo "Git history not available"

Repository: PERMIT-SEOUL/permit-server

Length of output: 116


URL 경로에 구분자가 누락되어 QR 코드가 잘못된 URL을 생성합니다.

Line 31에서 baseUrl + URL_PATH + ticketCode로 URL을 구성하므로, 현재 변경사항은 다음과 같은 잘못된 URL을 생성합니다:

  • 결과: https://example.com/entry/guestTICKET123 (guest와 티켓코드 사이에 / 없음)
  • 예상: https://example.com/entry/guest/TICKET123

이로 인해 QR 코드를 스캔했을 때 라우팅 실패나 404 에러가 발생합니다. 경로 구분자를 추가하세요:

-    private static final String URL_PATH = "/entry/guest";
+    private static final String URL_PATH = "/entry/guest/";
📝 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
private static final String URL_PATH = "/entry/guest";
private static final String URL_PATH = "/entry/guest/";
🤖 Prompt for AI Agents
In src/main/java/com/permitseoul/permitserver/domain/admin/util/QrCodeUtil.java
around line 26, the constant URL_PATH is missing a trailing slash which causes
URLs like https://example.com/entry/guestTICKET123; change the constant to
include the path separator (e.g. "/entry/guest/") so concatenation baseUrl +
URL_PATH + ticketCode yields /guest/{ticketCode}; also defensively handle
possible double slashes by trimming a trailing slash from baseUrl before
concatenation or by using a proper URL join helper.



public static byte[] generatePng(final String baseUrl, final String ticketCode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package com.permitseoul.permitserver.domain.eventimage;

public abstract class EventImageBaseException extends RuntimeException {
import com.permitseoul.permitserver.global.exception.PermitGlobalException;

public abstract class EventImageBaseException extends PermitGlobalException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.permitseoul.permitserver.domain.guest;

import com.permitseoul.permitserver.global.exception.PermitGlobalException;

public abstract class GuestBaseException extends PermitGlobalException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.permitseoul.permitserver.domain.guest.api;

import com.permitseoul.permitserver.domain.guest.api.exception.GuestApiException;
import com.permitseoul.permitserver.global.response.ApiResponseUtil;
import com.permitseoul.permitserver.global.response.BaseResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice(basePackages = "com.permitseoul.permitserver.domain.guest")
public class GuestExceptionHandler {

@ExceptionHandler(GuestApiException.class)
public ResponseEntity<BaseResponse<?>> handleGuestApiException(final GuestApiException e) {
return ApiResponseUtil.failure(e.getErrorCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.permitseoul.permitserver.domain.guest.api.controller;


import com.permitseoul.permitserver.domain.guest.api.dto.req.GuestTicketConfirmRequest;
import com.permitseoul.permitserver.domain.guest.api.service.GuestService;
import com.permitseoul.permitserver.global.response.ApiResponseUtil;
import com.permitseoul.permitserver.global.response.BaseResponse;
import com.permitseoul.permitserver.global.response.code.SuccessCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/guests")
@RequiredArgsConstructor
public class GuestController {
private final GuestService guestService;

//도어용 게스트 티켓 유효성 검증 api
@GetMapping("/tickets/door/validation/{ticketCode}")
public ResponseEntity<BaseResponse<?>> validateGuestTicket(
@PathVariable final String ticketCode
) {
return ApiResponseUtil.success(SuccessCode.OK, guestService.validateGuestTicket(ticketCode));
}

//도어용 게스트 티켓 스텝 확인 api
@PostMapping("/tickets/door/staff/confirm")
public ResponseEntity<BaseResponse<?>> confirmGuestTicketByStaffAtDoor(
@RequestBody @Valid GuestTicketConfirmRequest guestTicketConfirmRequest
) {
guestService.confirmGuestTicketByStaff(guestTicketConfirmRequest.ticketCode(), guestTicketConfirmRequest.checkCode());
return ApiResponseUtil.success(SuccessCode.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.permitseoul.permitserver.domain.guest.api.dto.req;

import jakarta.validation.constraints.NotBlank;

public record GuestTicketConfirmRequest(
@NotBlank(message = "ticketCode가 비어있습니다.")
String ticketCode,

@NotBlank(message = "checkCode가 비어있습니다.")
String checkCode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.permitseoul.permitserver.domain.guest.api.dto.res;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.permitseoul.permitserver.domain.ticket.api.dto.res.DoorValidateUserTicket;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

사용하지 않는 import 제거 필요

DoorValidateUserTicket import가 사용되지 않습니다. 제거해 주세요.

 import com.fasterxml.jackson.annotation.JsonFormat;
-import com.permitseoul.permitserver.domain.ticket.api.dto.res.DoorValidateUserTicket;

 import java.time.LocalDateTime;
📝 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
import com.permitseoul.permitserver.domain.ticket.api.dto.res.DoorValidateUserTicket;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
🤖 Prompt for AI Agents
src/main/java/com/permitseoul/permitserver/domain/guest/api/dto/res/GuestTicketValidateResponse.java
lines 4-4: the import of
com.permitseoul.permitserver.domain.ticket.api.dto.res.DoorValidateUserTicket is
unused; remove this import statement from the file and run a build/IDE
organize-imports to ensure no other unused imports remain.


import java.time.LocalDateTime;

public record GuestTicketValidateResponse(
String eventName,
String ticketName
) {
public static GuestTicketValidateResponse of(final String eventName) {
return new GuestTicketValidateResponse(eventName, "Guest Ticket");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.permitseoul.permitserver.domain.guest.api.exception;

import com.permitseoul.permitserver.domain.guest.GuestBaseException;
import com.permitseoul.permitserver.global.response.code.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public abstract class GuestApiException extends GuestBaseException {
private final ErrorCode errorCode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.permitseoul.permitserver.domain.guest.api.exception;

import com.permitseoul.permitserver.global.response.code.ErrorCode;

public class GuestNotFoundException extends GuestApiException {
public GuestNotFoundException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.permitseoul.permitserver.domain.guest.api.exception;

import com.permitseoul.permitserver.global.response.code.ErrorCode;

public class GuestTicketIllegalException extends GuestApiException{
public GuestTicketIllegalException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.permitseoul.permitserver.domain.guest.api.service;

import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.GuestTicketStatus;
import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.entity.GuestTicketEntity;
import com.permitseoul.permitserver.domain.admin.guestticket.core.exception.GuestTicketNotFoundException;
import com.permitseoul.permitserver.domain.event.core.component.EventRetriever;
import com.permitseoul.permitserver.domain.event.core.domain.Event;
import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException;
import com.permitseoul.permitserver.domain.guest.api.dto.res.GuestTicketValidateResponse;
import com.permitseoul.permitserver.domain.guest.api.exception.GuestNotFoundException;
import com.permitseoul.permitserver.domain.guest.api.exception.GuestTicketIllegalException;
import com.permitseoul.permitserver.domain.guest.core.component.GuestRetriever;
import com.permitseoul.permitserver.domain.guest.core.component.GuestUpdater;
import com.permitseoul.permitserver.domain.guest.core.domain.GuestTicket;
import com.permitseoul.permitserver.domain.ticket.api.exception.NotFoundTicketException;
import com.permitseoul.permitserver.domain.ticket.core.exception.TicketNotFoundException;
import com.permitseoul.permitserver.domain.tickettype.core.exception.TicketTypeNotfoundException;
import com.permitseoul.permitserver.global.response.code.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;

@Service
@RequiredArgsConstructor
public class GuestService {
private final GuestRetriever guestRetriever;
private final EventRetriever eventRetriever;
private final GuestUpdater guestUpdater;

public GuestTicketValidateResponse validateGuestTicket(final String ticketCode) {
try {
final GuestTicket guestTicket = guestRetriever.findGuestTicketByTicketCode(ticketCode);
validateGuestTicketStatus(guestTicket.getStatus());

final Event event = eventRetriever.findEventById(guestTicket.getEventId());
return GuestTicketValidateResponse.of(event.getName());
} catch (GuestTicketNotFoundException e) {
throw new GuestNotFoundException(ErrorCode.NOT_FOUND_GUEST_TICKET);
} catch (EventNotfoundException e) {
throw new GuestNotFoundException(ErrorCode.NOT_FOUND_EVENT);
}
}

@Transactional
public void confirmGuestTicketByStaff(final String ticketCode, final String checkCodeFromStaff) {
try {
final GuestTicketEntity guestTicketEntity = guestRetriever.findGuestTicketEntityByTicketCode(ticketCode);
validateGuestTicketStatus(guestTicketEntity.getStatus());

final Event event = eventRetriever.findEventById(guestTicketEntity.getEventId());
validateGuestTicketByCheckCode(event.getTicketCheckCode(), checkCodeFromStaff);

guestUpdater.updateGuestTicketStatus(guestTicketEntity, GuestTicketStatus.USED);
} catch (GuestTicketNotFoundException e) {
throw new GuestNotFoundException(ErrorCode.NOT_FOUND_GUEST_TICKET);
} catch (EventNotfoundException e) {
throw new GuestNotFoundException(ErrorCode.NOT_FOUND_EVENT);
}
}

private void validateGuestTicketStatus(final GuestTicketStatus status) {
if(status == GuestTicketStatus.USED) {
throw new GuestTicketIllegalException(ErrorCode.CONFLICT_ALREADY_USED_TICKET);
}
}

private void validateGuestTicketByCheckCode(final String checkCode, final String checkCodeFromStaff) {
if(!Objects.equals(checkCode, checkCodeFromStaff)) {
throw new GuestTicketIllegalException(ErrorCode.BAD_REQUEST_TICKET_CHECK_CODE_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.permitseoul.permitserver.domain.guest.core.component;

import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.entity.GuestTicketEntity;
import com.permitseoul.permitserver.domain.admin.guestticket.core.exception.GuestTicketNotFoundException;
import com.permitseoul.permitserver.domain.admin.guestticket.core.repository.GuestTicketRepository;
import com.permitseoul.permitserver.domain.guest.core.domain.GuestTicket;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class GuestRetriever {
private final GuestTicketRepository guestTicketRepository;

public GuestTicket findGuestTicketByTicketCode(final String ticketCode) {
return GuestTicket.fromEntity(guestTicketRepository.findByGuestTicketCode(ticketCode).orElseThrow(GuestTicketNotFoundException::new));
}

public GuestTicketEntity findGuestTicketEntityByTicketCode(final String ticketCode) {
return guestTicketRepository.findByGuestTicketCode(ticketCode).orElseThrow(GuestTicketNotFoundException::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.permitseoul.permitserver.domain.guest.core.component;

import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.GuestTicketStatus;
import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.entity.GuestTicketEntity;
import org.springframework.stereotype.Component;

@Component
public class GuestUpdater {

public void updateGuestTicketStatus(final GuestTicketEntity guestTicketEntity, final GuestTicketStatus guestTicketStatus) {
guestTicketEntity.updateStatus(guestTicketStatus);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.permitseoul.permitserver.domain.guest.core.domain;

import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.GuestTicketStatus;
import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.entity.GuestTicketEntity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.time.LocalDateTime;

@Getter
@RequiredArgsConstructor
public class GuestTicket {
private final Long guestTicketId;
private final long eventId;
private final long guestId;
private final String guestTicketCode;
private final GuestTicketStatus status;
private final LocalDateTime usedTime;

public static GuestTicket fromEntity(final GuestTicketEntity guestTicketEntity) {
return new GuestTicket(
guestTicketEntity.getGuestTicketId(),
guestTicketEntity.getEventId(),
guestTicketEntity.getGuestId(),
guestTicketEntity.getGuestTicketCode(),
guestTicketEntity.getStatus(),
guestTicketEntity.getUsedTime()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public class SecurityConfig {
"/api/tickets/info/*",
"/api/tickets/door/staff/confirm",
"/api/tickets/door/validation/*",
"/api/notion/**"
"/api/notion/**",
"api/guests/**"
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

치명적: 경로 패턴에 선행 슬래시(/)가 누락되었습니다.

Line 43의 "api/guests/**" 패턴에 선행 슬래시가 없어 Spring Security의 requestMatchers가 올바르게 매칭하지 못합니다. 이로 인해 게스트 API 엔드포인트가 화이트리스트에 추가되지 않아 인증이 필요하게 되며, 이 PR의 핵심 기능이 작동하지 않습니다.

다음 diff를 적용하여 수정하세요:

-            "api/guests/**"
+            "/api/guests/**"
📝 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
"api/guests/**"
"/api/guests/**"
🤖 Prompt for AI Agents
In src/main/java/com/permitseoul/permitserver/global/config/SecurityConfig.java
around line 43, the path pattern "api/guests/**" is missing a leading slash so
Spring Security requestMatchers won't match; update the pattern to include the
leading slash ("/api/guests/**") so the guest API endpoints are correctly
whitelisted by the security configuration.

};

private static final String[] adminURIList = {
Expand Down