Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
75e407a
refactor(openapi): migrate from swagger annotations to static openapi…
uykhc Apr 14, 2026
8cebad2
refactor(openapi): migrate from swagger annotations to static openapi…
uykhc Apr 14, 2026
bd0c580
:memo: swagger 불일치 부분 수정
jaylovegood Apr 18, 2026
7d1eed7
merged
jaylovegood Apr 18, 2026
33f6630
:zap: 대기번호 계산 시 모든 대기 신청 건 로드하지 않도록 변경 (메모리 최적화)
jaylovegood May 2, 2026
adb4a94
:zap: 이벤트 상세정보 조회 시 user 테이블 쿼리 횟수 줄이기
jaylovegood May 2, 2026
f36546d
:zap: guest preview 구성 시 db에서 처음부터 5건만 조회하도록 수정 (메모리 최적화)
jaylovegood May 2, 2026
4186c11
:zap: event 목록 조회 시 N+1 쿼리 문제 해결
jaylovegood May 2, 2026
8080f1f
:zap: event 삭제 알림 대상 조회 시 처음부터 CONFIRMED+WAITLISTED만 가져오도록 변경
jaylovegood May 2, 2026
2cc4481
:zap: event 도메인 쿼리 최적화를 위한 repository method 추가
jaylovegood May 2, 2026
63b791f
:zap: reconcileWaitlist에서 불필요한 전체 waitlist신청 조회 제거 (메모리 최적화)
jaylovegood May 2, 2026
5ed23a6
:zap: registration create 시 user 테이블 조회 횟수 줄이기
jaylovegood May 2, 2026
0848edd
:zap: registration create 시 waitlist count 중복 조회 제거
jaylovegood May 2, 2026
4f822e0
:zap: registration delete 시 중복 lock 제거
jaylovegood May 2, 2026
9ff360a
:zap: 대기번호 계산 시 불필요한 메모리 로드 제거
jaylovegood May 2, 2026
936cfb1
:zap: reconcileWaitlist에서 user 조회 N+1 쿼리 문제 해결
jaylovegood May 2, 2026
09b6ffd
Merge branch 'dev' into refactor/openapi-yaml
uykhc May 2, 2026
fe12ce1
docs(openapi): fix Redocly lint errors for security scheme and operat…
uykhc May 2, 2026
2a90016
Merge remote-tracking branch 'origin/refactor/openapi-yaml' into refa…
uykhc May 2, 2026
b045dfc
refactor(swagger): remove leftover @Schema annotations for static Ope…
uykhc May 2, 2026
5e1d0d9
:bug: api spec에 맞지 않는 registration delete request dto 삭제
jaylovegood May 4, 2026
7cf4198
:bug: 참여 취소 시 권한 검증 fix
jaylovegood May 4, 2026
04ca119
:memo: 불일치 해결
jaylovegood May 4, 2026
c7120d3
Merge branch 'refactor/openapi-yaml' of https://github.com/wafflestud…
jaylovegood May 4, 2026
e4c5c72
:memo: 불일치 해결
jaylovegood May 4, 2026
8a06dfb
:fire: 안 쓰이는 RegisterRequest 삭제
jaylovegood May 4, 2026
616bbf6
Merge pull request #237 from wafflestudio/refactor/openapi-yaml
jaylovegood May 4, 2026
e45ead7
feat: prometheus 모니터링 설정 추가
uykhc May 5, 2026
810e397
:bug: 대기 순번 계산 방식 통일
jaylovegood May 5, 2026
ec36d13
Merge pull request #242 from wafflestudio/ref/241-쿼리-수-줄이기
jaylovegood May 5, 2026
a154461
Merge pull request #247 from wafflestudio/chore/prometheus-setup
jaylovegood May 5, 2026
22762d1
:bug: api spec에 맞지 않는 registration delete request dto 삭제
jaylovegood May 4, 2026
942b79e
:bug: 참여 취소 시 권한 검증 fix
jaylovegood May 4, 2026
0077189
Merge branch '244-참여-취소-요청-시-400' of https://github.com/wafflestudio/…
jaylovegood May 5, 2026
458048c
:memo: swagger에서 DeleteRegistrationRequest 제거
jaylovegood May 5, 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
2 changes: 2 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### 🧪 로컬 테스트 여부 (작업자 체크)
- [ ] 로컬에서 Swagger 혹은 테스트 코드로 동작을 확인했습니다.
- [ ] API 변경이 있다면 `src/main/resources/static/docs/openapi.yaml`을 함께 갱신했습니다.
- [ ] `/docs/swagger-ui.html`에서 변경 API가 의도대로 보이는지 확인했습니다.

### 📄 documentation 최신화 여부
> 변경사항과 관련된 API의 swagger documentation이 실제 동작과 일치하는지 확인합니다.
Expand Down
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [ ] CI 성공
- [ ] 환경 변수 설정 확인
- [ ] dev 서버에서 변경사항에 대해 의도대로 동작함을 확인
- [ ] API 변경이 있다면 `src/main/resources/static/docs/openapi.yaml` 반영 완료

### 📝 업데이트 사항 요약

Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node 20
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Lint OpenAPI YAML
run: npx @redocly/cli@latest lint src/main/resources/static/docs/openapi.yaml

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
Expand Down
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("org.mindrot:jbcrypt:0.4")
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
Expand Down Expand Up @@ -73,6 +72,10 @@ dependencies {
// OCI Vault - SDK 직접 사용 (waffle-oci-vault 2.1.0은 Spring Boot 3.5와 호환 안됨)
implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1")
implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1")

// prometheus 의존성 추가
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
}

kotlin {
Expand Down
17 changes: 17 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ services:
limits:
memory: 64M

prometheus:
image: prom/prometheus:latest
container_name: prometheus-dev
restart: unless-stopped
ports:
- "127.0.0.1:9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data_dev:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
depends_on:
app:
condition: service_healthy

volumes:
mysql_data_dev:
redis_data_dev:
prometheus_data_dev:
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import com.wafflestudio.spring2025.common.image.dto.ImageUploadResponse
import com.wafflestudio.spring2025.common.image.service.ImageService
import com.wafflestudio.spring2025.domain.auth.LoggedInUser
import com.wafflestudio.spring2025.domain.user.model.User
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
Expand All @@ -22,21 +18,12 @@ import org.springframework.web.multipart.MultipartFile
class ImageController(
private val imageService: ImageService,
) {
@Operation(summary = "이미지 업로드", description = "서버 저장소에 이미지를 업로드합니다.")
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "이미지 업로드 성공"),
ApiResponse(responseCode = "400", description = "잘못된 요청 (파일 누락/형식 오류 등)"),
ApiResponse(responseCode = "401", description = "인증 실패 (유효하지 않은 토큰)"),
],
)
@PostMapping(
consumes = [MediaType.MULTIPART_FORM_DATA_VALUE],
)
fun uploadImage(
@Parameter(hidden = true) @LoggedInUser user: User?,
@LoggedInUser user: User?,
@RequestPart("image") image: MultipartFile,
@Parameter(description = "이미지를 저장할 상위 경로", required = false)
@RequestParam(name = "prefix", required = false)
prefix: String?,
): ResponseEntity<ImageUploadResponse> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import com.wafflestudio.spring2025.domain.auth.dto.LoginResponse
import com.wafflestudio.spring2025.domain.auth.dto.SignupRequest
import com.wafflestudio.spring2025.domain.auth.service.AuthService
import com.wafflestudio.spring2025.domain.auth.service.EmailVerificationService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
Expand All @@ -19,23 +15,10 @@ import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/auth")
@Tag(name = "Auth", description = "인증 API")
class AuthController(
private val authService: AuthService,
private val emailVerificationService: EmailVerificationService,
) {
@Operation(
summary = "이메일 회원가입",
description = "새로운 사용자를 등록하고 인증 이메일을 발송합니다. 이메일 인증 후 가입이 완료됩니다.",
)
@ApiResponses(
value = [
ApiResponse(responseCode = "204", description = "인증 이메일 발송 성공"),
ApiResponse(responseCode = "400", description = "잘못된 요청 (유효하지 않은 email/password/name)"),
ApiResponse(responseCode = "409", description = "이미 존재하는 email"),
ApiResponse(responseCode = "503", description = "이메일 서비스 장애"),
],
)
@PostMapping("/signup")
fun signup(
@RequestBody signupRequest: SignupRequest,
Expand All @@ -51,16 +34,6 @@ class AuthController(
return ResponseEntity.noContent().build()
}

@Operation(
summary = "이메일 인증",
description = "이메일로 받은 인증 코드를 검증하고 회원가입을 완료합니다",
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "인증 성공, 유저 등록 완료"),
ApiResponse(responseCode = "400", description = "유효하지 않거나 만료된 인증 코드"),
],
)
@PostMapping("/email-verification/{verificationCode}")
fun verifyEmail(
@PathVariable verificationCode: String,
Expand All @@ -69,13 +42,6 @@ class AuthController(
return ResponseEntity.ok().build()
}

@Operation(summary = "로그인", description = "email로 로그인하여 JWT 토큰을 발급받습니다")
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "로그인 성공, JWT 토큰 반환"),
ApiResponse(responseCode = "401", description = "인증 실패"),
],
)
@PostMapping("/login")
fun login(
@RequestBody loginRequest: LoginRequest,
Expand All @@ -84,12 +50,6 @@ class AuthController(
return ResponseEntity.ok(LoginResponse(token))
}

@Operation(summary = "로그아웃", description = "현재 JWT 토큰을 블랙리스트에 추가하여 로그아웃합니다")
@ApiResponses(
value = [
ApiResponse(responseCode = "204", description = "로그아웃 성공"),
],
)
@PostMapping("/logout")
fun logout(request: HttpServletRequest): ResponseEntity<Void> {
val token = resolveToken(request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,17 @@ import com.wafflestudio.spring2025.domain.auth.dto.LoginResponse
import com.wafflestudio.spring2025.domain.auth.dto.SocialLoginRequest
import com.wafflestudio.spring2025.domain.auth.model.SocialProvider
import com.wafflestudio.spring2025.domain.auth.service.SocialAuthService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("api/auth/social")
@RequestMapping("/api/auth/social")
class SocialAuthController(
private val socialAuthService: SocialAuthService,
) {
@Operation(summary = "소셜 로그인", description = "소셜 로그인을 요청합니다.")
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "소셜 로그인 성공"),
ApiResponse(responseCode = "401", description = "인증 실패"),
ApiResponse(responseCode = "409", description = "이미 가입된 이메일"),
],
)
@PostMapping
suspend fun social(
@RequestBody socialLoginRequest: SocialLoginRequest,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package com.wafflestudio.spring2025.domain.auth.dto

import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "로그인 요청")
data class LoginRequest(
@Schema(description = "사용자 이메일", example = "user@example.com", required = true)
val email: String,
@Schema(description = "사용자 비밀번호", example = "mypassword", required = true)
val password: String,
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
package com.wafflestudio.spring2025.domain.auth.dto

import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "회원가입 요청")
data class SignupRequest(
@Schema(description = "사용자 이메일", example = "user@example.com", required = true)
val email: String,
@Schema(description = "사용자 이름", example = "홍길동", required = true)
val name: String,
@Schema(description = "비밀번호", example = "password123", required = true)
val password: String,
@Schema(description = "프로필 이미지 URL", example = "https://cdn.example.com/profile.png")
val profileImage: String? = null,
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package com.wafflestudio.spring2025.domain.auth.dto

import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "소셜 로그인 요청")
data class SocialLoginRequest(
@Schema(description = "소셜로그인 제공자", example = "GOOGLE", required = true)
val provider: String,
@Schema(description = "인가 코드 (authorization code)", required = true)
val code: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import com.wafflestudio.spring2025.domain.event.dto.response.MyEventsInfiniteRes
import com.wafflestudio.spring2025.domain.event.dto.response.UpdateEventResponse
import com.wafflestudio.spring2025.domain.event.service.EventService
import com.wafflestudio.spring2025.domain.user.model.User
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
Expand All @@ -26,12 +24,10 @@ import java.time.Instant

@RestController
@RequestMapping("/api/events")
@Tag(name = "Event", description = "이벤트 관리 API")
class EventController(
private val eventService: EventService,
) {
@AuthRequired
@Operation(summary = "이벤트 생성", description = "새로운 이벤트를 생성합니다")
@PostMapping // POST /api/events
fun create(
@LoggedInUser user: User,
Expand All @@ -54,7 +50,6 @@ class EventController(
return ResponseEntity.ok(CreateEventResponse(publicId))
}

@Operation(summary = "이벤트 상세 조회", description = "이벤트 상세 정보를 조회합니다")
@GetMapping("/{publicId}") // GET /api/events/{publicId}
fun getById(
@LoggedInUser user: User?, // nullable
Expand All @@ -69,10 +64,6 @@ class EventController(
}

@AuthRequired
@Operation(
summary = "내가 만든 이벤트 목록 조회 (무한 스크롤)",
description = "로그인된 사용자가 생성한 이벤트 목록을 무한 스크롤 방식(cursor)으로 조회합니다",
)
@GetMapping("/me")
fun getMyEvents(
@LoggedInUser user: User,
Expand All @@ -89,7 +80,6 @@ class EventController(
}

@AuthRequired
@Operation(summary = "이벤트 수정", description = "이벤트를 수정합니다")
@PutMapping("/{publicId}") // PUT /api/events/{publicId}
fun update(
@LoggedInUser user: User,
Expand All @@ -116,7 +106,6 @@ class EventController(
}

@AuthRequired
@Operation(summary = "이벤트 삭제", description = "이벤트를 삭제합니다")
@DeleteMapping("/{publicId}") // DELETE /api/events/{publicId}
fun delete(
@LoggedInUser user: User,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,20 @@
package com.wafflestudio.spring2025.domain.event.dto.core

import com.wafflestudio.spring2025.domain.event.model.Event
import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "일정 정보")
data class EventDto(
@Schema(description = "일정 ID (publicId)")
val id: String,
@Schema(description = "이벤트 제목")
val title: String,
@Schema(description = "이벤트 설명")
val description: String?,
@Schema(description = "장소")
val location: String?,
@Schema(description = "시작 시간 (epoch milliseconds)")
val startsAt: Long?,
@Schema(description = "종료 시간 (epoch milliseconds)")
val endsAt: Long?,
@Schema(description = "정원")
val capacity: Int?,
@Schema(description = "대기 명단 사용 여부")
val waitlistEnabled: Boolean,
@Schema(description = "신청 시작 시간 (epoch milliseconds)")
val registrationStartsAt: Long?,
@Schema(description = "신청 마감 시간 (epoch milliseconds)")
val registrationEndsAt: Long,
@Schema(description = "작성자 ID")
val createdBy: Long,
@Schema(description = "생성 일시 (epoch milliseconds)")
val createdAt: Long?,
@Schema(description = "수정 일시 (epoch milliseconds)")
val updatedAt: Long?,
) {
constructor(event: Event) : this(
Expand Down
Loading
Loading