Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0ea288c
docs: README에 백엔드 아키텍처 그림 추가
gihhyeon Mar 17, 2026
5388090
docs: Replace back-end architecture image
gihhyeon Mar 17, 2026
73191ed
Update back-end architecture image in README
gihhyeon Mar 17, 2026
113438c
chore: 맛집 상세 조회 k6 기반 테스트 세팅
gihhyeon Mar 23, 2026
834f4da
Merge branch 'develop' of https://github.com/knu-matzip/Matzip-Server…
gihhyeon Mar 23, 2026
572e2d2
Update project duration to current operation
gihhyeon Mar 27, 2026
e768c5b
refactor: DTO 네이밍 통일 및 와일드카드 import 정리
gihhyeon Aug 23, 2026
deb9aeb
Merge pull request #136 from knu-matzip/refactor/dto-naming-import-co…
gihhyeon Aug 23, 2026
e0796c4
refactor: place DTO를 역할별 위치로 재배치
gihhyeon Aug 25, 2026
1b7aee4
Merge pull request #138 from knu-matzip/refactor/place-dto-placement
gihhyeon Aug 26, 2026
7f17dee
chore: PR 테스트 검증을 위한 CI 워크플로우 추가
gihhyeon Aug 27, 2026
30d6f8f
Merge pull request #139 from knu-matzip/chore/ci-setup
gihhyeon Aug 27, 2026
2723b13
refactor: place 도메인을 레이어드 표준 패키지로 통일
gihhyeon Sep 2, 2026
809cf28
refactor: auth 도메인을 레이어드 표준 패키지로 통일
gihhyeon Sep 2, 2026
56eed27
Merge pull request #147 from knu-matzip/refactor/142-place-layered
gihhyeon Sep 2, 2026
ec309c6
Merge pull request #148 from knu-matzip/refactor/143-auth-layered
gihhyeon Sep 2, 2026
7c873e9
refactor: user 도메인을 레이어드 표준 패키지로 통일
gihhyeon Sep 2, 2026
879dc76
Merge pull request #149 from knu-matzip/refactor/144-user-layered
gihhyeon Sep 2, 2026
7cb25ec
refactor: lottery 도메인 dto·client 위치를 레이어드 표준으로 정리
gihhyeon Sep 2, 2026
ef4114a
refactor: admin 도메인 dto 위치를 레이어드 표준으로 정리
gihhyeon Sep 2, 2026
365a987
Merge pull request #150 from knu-matzip/refactor/145-lottery-layered
gihhyeon Sep 2, 2026
158a7a6
Merge pull request #151 from knu-matzip/refactor/146-admin-layered
gihhyeon Sep 2, 2026
65424bf
docs: CLAUDE.md에 아키텍처 표준과 작업 규칙 문서화
gihhyeon Sep 2, 2026
99190a9
chore: PR 템플릿을 What/Why/How 구조로 개편
gihhyeon Sep 2, 2026
13ac64b
Merge pull request #152 from knu-matzip/docs/claude-md
gihhyeon Sep 3, 2026
95c6b40
refactor: 예외 처리 정리 및 GlobalExceptionHandler 표준화
gihhyeon Sep 3, 2026
f887756
Merge pull request #154 from knu-matzip/refactor/153-exception-cleanup
gihhyeon Sep 3, 2026
578f32c
refactor: 외부 API 호출에 공통 타임아웃 설정 추가
gihhyeon Sep 4, 2026
48ee1a3
Merge pull request #156 from knu-matzip/refactor/155-external-api-tim…
gihhyeon Sep 4, 2026
0361401
refactor: 좋아요 수 증감을 원자적 UPDATE로 변경해 lost-update 방지
gihhyeon Sep 4, 2026
4ae986f
test: 좋아요 동시성 테스트 견고성 개선 (PR #158 리뷰 반영)
gihhyeon Sep 7, 2026
67f8809
Merge pull request #158 from knu-matzip/refactor/157-like-count-atomic
gihhyeon Sep 7, 2026
4f551f1
fix: 오늘의 맛집 조회수 랭킹이 빈 값을 반환하는 문제 폴백으로 해결
gihhyeon Sep 7, 2026
97800f8
Merge pull request #159 from knu-matzip/fix/50-today-ranking-fallback
gihhyeon Sep 8, 2026
eedcb79
feat: 비즈니스 이벤트 로깅 인프라 추가
gihhyeon Sep 8, 2026
cfad1f1
feat: 비즈니스 이벤트 계측 지점 연결
gihhyeon Sep 8, 2026
5573990
test: 이벤트 로깅 테스트 추가
gihhyeon Sep 8, 2026
394691f
chore: event_log prod DDL 및 검증 쿼리 추가
gihhyeon Sep 8, 2026
5eac05b
Merge pull request #161 from knu-matzip/feat/160-analytics-event-logging
gihhyeon Sep 9, 2026
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
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.git
build/
out/
.gradle/
Dockerfile
31 changes: 17 additions & 14 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
## 📌 PR 제목
간결하고 명확하게 요약
## 요약
<!-- 이 PR을 한두 문장으로 요약 -->

## ✅ 작업 내용
## What — 무엇을 바꿨나
<!-- 변경한 내용을 구체적으로. 여러 개면 목록으로 -->
-

- [ ] 작업 1: 어떤 기능을 구현했는지
- [ ] 작업 2: 어떤 버그를 수정했는지
## Why — 왜 필요한가
<!-- 배경·문제·목적. 이 변경이 없으면 어떤 문제가 있는지 -->

## 🔍 체크리스트
## How — 어떻게 구현했나
<!-- 접근 방식과 핵심 설계 결정. 버린 대안이 있으면 그 이유. 리뷰어가 집중해서 볼 지점 -->

- [ ] 코드에 불필요한 주석/코드 제거
- [ ] 리뷰어가 이해할 수 있도록 설명 추가
## 체크리스트
- [ ] 불필요한 주석/코드 제거
- [ ] 테스트 추가/갱신 및 통과 (`./gradlew test`)
- [ ] 공개 계약 영향 확인 (엔드포인트·응답 형태 변경 시 범위 명시)

## 🎯 관련 이슈
## 관련 이슈
- close #

- close #이슈번호

## 💬 기타 공유하고 싶은 내용

기타 공유할 만한 이슈, 고민 등 자유롭게 작성해주세요.
## 기타 / 리뷰 포인트
<!-- 고민한 점, 논의가 필요한 부분, 스크린샷 등 -->
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
pull_request:
branches:
- develop
- main
push:
branches:
- develop

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

permissions:
contents: read
checks: write
pull-requests: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'

- name: Grant execute permission for gradlew
run: chmod +x ./gradlew

- name: Run tests
run: ./gradlew test

- name: Publish test report
uses: mikepenz/action-junit-report@v5
if: always()
with:
report_paths: 'build/test-results/test/TEST-*.xml'
check_name: 'Test Report'
96 changes: 96 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# KNU-Matzip Server

이 저장소에서 작업할 때 지키는 규칙을 정리한다. 사용자의 명시적 지시가 있으면 그것을 우선한다.

KNU-Matzip 백엔드. Spring Boot 3.4 (Java 17) + JPA + MySQL.

## 구조

단일 Gradle 모듈이며 `com.matzip` 아래를 **도메인별로 나눈(package-by-domain)** 구조다.
도메인 패키지 내부는 **고전 3계층 레이어드 아키텍처**로 통일한다. 의존은 위에서 아래로 단방향, 직접 호출한다.

```text
controller -> service -> repository -> DB
(client: DB가 아닌 외부 연동 어댑터 — service가 직접 호출)
```

- 도메인 패키지: `auth`(카카오 로그인/JWT 발급), `user`(프로필), `place`(맛집 등록·조회·찜·검색), `lottery`(응모 이벤트), `admin`(맛집 승인/거절), `common`(공통).
- **하위 계층이 상위 계층에 정의된 인터페이스(port)를 구현하는 의존성 역전 구조를 쓰지 않는다** (헥사고날 아님). 인터페이스가 필요하면 그 인터페이스는 자신이 속한 계층에 둔다.
- 도메인 패키지 내부 표준:
- `controller/` — Presentation. `@RestController`. 요청/응답 바디는 `dto/`의 DTO 사용.
- `service/` — Business. `@Service` 구현체. **인터페이스 없이 구현체만 둔다** (실제 교체가 필요한 예외에만 인터페이스 추가, 그 인터페이스도 이 계층 소유).
- `repository/` — Data Access. Spring Data JPA 리포지토리(인터페이스 = DAO, 구현은 프레임워크 생성). DB와 직접 통신.
- `domain/` — 도메인 모델. JPA 엔티티(`domain/entity/`)와 enum/값 타입. 여러 계층에서 공유.
- `dto/` — 전송 객체. `dto/request/`, `dto/response/`로 나눈다. 요청/응답 양쪽이 공유하는 조각은 `dto/` 루트에 둔다.
- `client/` (필요할 때만) — DB가 아닌 외부 연동 어댑터(외부 API 클라이언트·캐시 등). 외부 응답 파싱 DTO는 `client/.../dto`.
- `event/` (필요할 때만) — 스프링 애플리케이션 이벤트/리스너. 특정 계층에 속하지 않는 교차 관심사라 전용 패키지로 둔다.

## 구현 규칙

### 컨트롤러와 응답

- 모든 컨트롤러 응답은 `common/response/ApiResponse<T>`로 감싼다. 팩토리: `ApiResponse.success(data)`, `successWithoutData()`(DELETE/PATCH 등), `successWithEmptyList()`. 응답 형태는 `{ status, timestamp, data, error }`.
- 비즈니스 예외는 `BusinessException`(+ `ErrorCode` enum)을 던지고, `GlobalExceptionHandler`가 중앙에서 `ApiResponse` 에러 형태로 변환한다. **컨트롤러에서 try/catch 하지 않는다.**

### 인증

- Stateless JWT. `JwtAuthenticationFilter`가 토큰을 검증해 `UserPrincipal`을 세팅한다(세션 없음, CSRF off).
- `SecurityConfig`상 대부분 엔드포인트는 `permitAll`, 일부 이벤트 응모 경로만 인증 필요.
- 인증 여부는 컨트롤러/서비스단에서 `UserPrincipal` 유무로 분기한다.

### 서비스

- `service/`에는 `@Service` 구현체만 둔다. 상위(controller)가 하위(service, repository)를 직접 호출한다.
- 외부 provider·캐시 등 DB가 아닌 연동은 `client/`의 어댑터를 service가 직접 사용한다.

### 영속성

- JPA 엔티티는 `domain/entity/`에 둔다.
- 모든 엔티티는 `common/entity/BaseEntity`를 상속해 `createdAt`/`updatedAt` 감사(auditing)를 일관되게 적용한다(`@MappedSuperclass` + `AuditingEntityListener`, PK는 `IDENTITY`).
- 스키마 반영은 프로파일을 따른다: `local`은 `ddl-auto: update`, `prod`는 `validate`.

### DTO

- 모든 DTO는 `Dto` 접미사를 붙인다. 유일한 예외는 응답 래퍼 `ApiResponse`.
- HTTP 요청/응답 DTO는 `dto/request`·`dto/response`. 요청/응답이 공유하는 조각은 `dto/` 루트. 외부 API 응답 파싱 DTO는 해당 `client/.../dto`.
- 프레임워크·엔티티 타입을 응답 DTO 대신 그대로 노출하지 않는다.

### 설정과 알림

- 프로퍼티는 `@ConfigurationProperties` 클래스(`common/config/`의 `JwtProperties`, `KakaoProperties`, `ImageProperties`, `DiscordWebhookProperties` 등)로 바인딩한다.
- 맛집 등록/당첨 알림은 `common/infra/discord/DiscordWebhookSender`. 비동기는 `AsyncConfiguration`, 스케줄러는 `SchedulerConfiguration`.

## 작업 방식

1. 작업 전에 관련 코드, 테스트, 설정을 읽고 현재 동작을 근거로 삼는다.
2. 새 작업 이슈는 GitHub Issues에 생성하고, 이슈 번호를 브랜치/PR 맥락에 사용한다(예: `refactor/142-place-layered`).
3. 구현 전에 사용자에게 작업 계획을 먼저 공유하고 확인을 받는다.
4. PR을 올리기 전에 변경 내용과 검증 결과를 사용자에게 먼저 리뷰받는다.
5. 요구사항을 추측해 메우지 않는다. 모호함이 결과나 공개 계약(엔드포인트·응답 형태)을 바꾸면 선택지와 영향을 명시하고 확인한다.
6. 요구 범위를 충족하는 최소 변경만 한다.
7. 변경 위험에 맞는 테스트를 추가/수정하고 검증한다. 검증하지 못한 항목을 완료로 표현하지 않는다.

## 실행

- 로컬 실행: `SPRING_PROFILES_ACTIVE=local ./gradlew bootRun`
- 로컬 MySQL(`localhost:3306/knu_matzip`)과 env var가 필요하다: `DB_USER`, `DB_PASS`, `JWT_SECRET_KEY`, `KAKAO_CLIENT_ID`, `STATE_SECRET_KEY`.

## 검증

- 빌드(테스트 포함): `./gradlew build`
- 전체 테스트: `./gradlew test`
- 단일 테스트: `./gradlew test --tests "com.matzip.place.service.PlaceReadServiceTest"` (메서드까지: `...PlaceServiceTest.메서드명`)
- 테스트는 `src/test/resources/application.yml`의 H2 인메모리 + 더미 시크릿으로 완결적이라 외부 DB·시크릿 없이 돈다.

## 커밋

- 커밋 메시지는 `type: 제목` 형식으로 쓴다. 예: `refactor: place 도메인을 레이어드 표준 패키지로 통일`
- `type`: `feat`, `fix`, `refactor`, `chore`, `docs`
- 제목은 한국어로 간결하게 쓰고 끝에 마침표를 붙이지 않는다.
- 하나의 커밋은 하나의 논리적 변경만 담는다.

## Pull Request

- PR base branch는 항상 `develop`으로 잡는다. `main` push가 운영 배포(EC2) 트리거다.
- PR 제목은 이슈 유형 접두사를 붙인다(예: `[refactor] ...`, `[feature] ...`).
- PR 본문은 `.github/pull_request_template.md` 형식(작업 내용, 체크리스트, 관련 이슈)을 따르고 `close #이슈번호`로 연결한다.
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 1. 베이스 이미지 설정 (JDK 환경)
FROM openjdk:17-jdk-slim

# 2. 작업 디렉토리 설정
WORKDIR /app

# 3. 빌드된 jar 파일을 컨테이너로 복사
# (build/libs/ 안에 있는 .jar 파일을 app.jar로 복사)
COPY build/libs/*.jar app.jar

# 4. 앱 실행
ENTRYPOINT ["java", "-jar", "app.jar"]
14 changes: 14 additions & 0 deletions Dockerfile.loadtest
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM gradle:8.10.2-jdk17 AS builder

WORKDIR /workspace
COPY . .
RUN gradle bootJar --no-daemon

FROM eclipse-temurin:17-jre-jammy

WORKDIR /app
COPY --from=builder /workspace/build/libs/*.jar /app/app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

### 🗓️ 기간

2025.09 ~ 2025.11
2025.09 ~ 현재 [운영 중]

### ⚙️ 기술 스택

Expand All @@ -23,6 +23,11 @@
![React](https://go-skill-icons.vercel.app/api/icons?i=ts,nextjs,zustand,reactquery,tailwind,storybook,pnpm,turborepo,googleanalytics,sentry&perline=5)


### 🥶 Back-end Architecture
<img width="1265" height="1047" alt="knu-matzip excalidraw" src="https://github.com/user-attachments/assets/18840a9c-4d9d-4b55-9344-57d201d0a89e" />



### 📱 주요 기능

#### 1. 맛집 탐색
Expand Down
22 changes: 11 additions & 11 deletions src/main/java/com/matzip/admin/controller/AdminController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.matzip.admin.controller;

import com.matzip.admin.controller.request.PlaceRegisterRequestReviewRequest;
import com.matzip.admin.controller.response.PlaceRegisterRequestDetailResponse;
import com.matzip.admin.controller.response.PlaceRegisterRequestsResponse;
import com.matzip.admin.dto.request.PlaceRegisterRequestReviewRequestDto;
import com.matzip.admin.dto.response.PlaceRegisterRequestDetailResponseDto;
import com.matzip.admin.dto.response.PlaceRegisterRequestsResponseDto;
import com.matzip.admin.service.AdminPlaceRegisterRequestService;
import com.matzip.common.exception.BusinessException;
import com.matzip.common.exception.code.ErrorCode;
import com.matzip.common.response.ApiResponse;
// import com.matzip.common.security.UserPrincipal;
import com.matzip.place.infra.repository.PlaceRepository;
import com.matzip.place.repository.PlaceRepository;
// import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -32,21 +32,21 @@ public AdminController(PlaceRepository placeRepository, AdminPlaceRegisterReques
}

@GetMapping("/requests/places")
public ApiResponse<List<PlaceRegisterRequestsResponse>> findPlaceRegisterRequests() {
List<PlaceRegisterRequestsResponse> data = placeRepository.findPendingPlaces()
public ApiResponse<List<PlaceRegisterRequestsResponseDto>> findPlaceRegisterRequests() {
List<PlaceRegisterRequestsResponseDto> data = placeRepository.findPendingPlaces()
.stream()
.map(PlaceRegisterRequestsResponse::from)
.map(PlaceRegisterRequestsResponseDto::from)
.toList();

return ApiResponse.success(data);
}

@GetMapping("/requests/places/{placeId}")
public ApiResponse<PlaceRegisterRequestDetailResponse> findPlaceRegisterRequestDetail(
public ApiResponse<PlaceRegisterRequestDetailResponseDto> findPlaceRegisterRequestDetail(
@PathVariable("placeId") Long placeId
) {
PlaceRegisterRequestDetailResponse data = placeRepository.findByIdWithCategoriesAndTags(placeId)
.map(PlaceRegisterRequestDetailResponse::from)
PlaceRegisterRequestDetailResponseDto data = placeRepository.findByIdWithCategoriesAndTags(placeId)
.map(PlaceRegisterRequestDetailResponseDto::from)
.orElseThrow(() -> new BusinessException(ErrorCode.PLACE_NOT_FOUND));

return ApiResponse.success(data);
Expand All @@ -55,7 +55,7 @@ public ApiResponse<PlaceRegisterRequestDetailResponse> findPlaceRegisterRequestD
@PostMapping("/requests/places/{placeId}/review")
public ApiResponse<?> reviewPlaceRegisterRequest(
@PathVariable("placeId") Long placeId,
@RequestBody PlaceRegisterRequestReviewRequest request
@RequestBody PlaceRegisterRequestReviewRequestDto request
// , @AuthenticationPrincipal UserPrincipal admin
) {
adminPlaceRegisterRequestService.review(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.matzip.admin.dto.request;

import com.matzip.admin.domain.RequestReviewStatus;

public record PlaceRegisterRequestReviewRequestDto(RequestReviewStatus status, String rejectedReason) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.matzip.admin.controller.response;
package com.matzip.admin.dto.response;

import com.matzip.place.domain.entity.Menu;
import com.matzip.place.domain.entity.Place;
Expand All @@ -12,7 +12,7 @@
import java.util.List;

@Builder
public record PlaceRegisterRequestDetailResponse(Long placeId, String placeName, LocalDate requestDate, List<PhotoDto> photos,
public record PlaceRegisterRequestDetailResponseDto(Long placeId, String placeName, LocalDate requestDate, List<PhotoDto> photos,
String address, LocationDto location, String description,
List<MenusResponse> menus, List<CategoryDto> categories, List<TagDto> tags) {

Expand All @@ -31,8 +31,8 @@ public static MenusResponse from(Menu menu) {
}
}

public static PlaceRegisterRequestDetailResponse from(Place place) {
return PlaceRegisterRequestDetailResponse.builder()
public static PlaceRegisterRequestDetailResponseDto from(Place place) {
return PlaceRegisterRequestDetailResponseDto.builder()
.placeId(place.getId())
.placeName(place.getName())
.requestDate(place.getCreatedAt().toLocalDate())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.matzip.admin.controller.response;
package com.matzip.admin.dto.response;

import com.matzip.place.domain.Campus;
import com.matzip.place.domain.entity.Place;
Expand All @@ -10,11 +10,11 @@
import java.util.List;

@Builder
public record PlaceRegisterRequestsResponse(Long placeId, String placeName, LocalDate requestDate, Campus campus,
public record PlaceRegisterRequestsResponseDto(Long placeId, String placeName, LocalDate requestDate, Campus campus,
List<CategoryDto> categories, List<TagDto> tags) {

public static PlaceRegisterRequestsResponse from(Place place) {
return PlaceRegisterRequestsResponse.builder()
public static PlaceRegisterRequestsResponseDto from(Place place) {
return PlaceRegisterRequestsResponseDto.builder()
.placeId(place.getId())
.placeName(place.getName())
.requestDate(place.getCreatedAt().toLocalDate())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.matzip.admin.event;

import com.matzip.common.analytics.AnalyticsRecorder;
import com.matzip.common.analytics.domain.EventType;
import com.matzip.common.analytics.domain.TargetType;
import com.matzip.common.exception.BusinessException;
import com.matzip.common.exception.code.ErrorCode;
import com.matzip.lottery.service.LotteryEventService;
import com.matzip.place.domain.entity.Place;
import com.matzip.place.infra.repository.PlaceRepository;
import com.matzip.place.repository.PlaceRepository;
import com.matzip.user.domain.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
Expand All @@ -17,10 +20,12 @@ public class RequestReviewEventListener {

private final LotteryEventService lotteryEventService;
private final PlaceRepository placeRepository;
private final AnalyticsRecorder analyticsRecorder;

public RequestReviewEventListener(LotteryEventService lotteryEventService, PlaceRepository placeRepository) {
public RequestReviewEventListener(LotteryEventService lotteryEventService, PlaceRepository placeRepository, AnalyticsRecorder analyticsRecorder) {
this.lotteryEventService = lotteryEventService;
this.placeRepository = placeRepository;
this.analyticsRecorder = analyticsRecorder;
}

@Async
Expand All @@ -36,6 +41,7 @@ public void enterCurrentEventOnApproval(RequestReviewEvent event) {
boolean entered = lotteryEventService.enterCurrentEventOnPlaceApproval(registrant.getId(), place.getId());
if (entered) {
log.info("[자동 응모 완료] userId: {}, placeId: {}", registrant.getId(), place.getId());
analyticsRecorder.record(EventType.EVENT_ENTERED, registrant.getId(), TargetType.PLACE, place.getId());
return;
}
log.info("[자동 응모 스킵] 진행 중 이벤트 없음 또는 이미 응모됨. userId: {}, placeId: {}", registrant.getId(), place.getId());
Expand Down
Loading
Loading