Skip to content
Open
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 @@ -95,6 +95,17 @@ class SecurityConfig(

// Poll - 내 여론조사 조회는 인증 필요 (더 구체적인 규칙을 먼저 선언)
httpRequest.requestMatchers("GET", "/poll/v1/my").authenticated()
// Poll - 내가 참여한 투표 목록은 인증 필요
httpRequest.requestMatchers("GET", "/poll/v1/public/response/my").authenticated()
httpRequest.requestMatchers("GET", "/poll/v1/system/response/my").authenticated()

// Poll Response - 비로그인 투표 허용 (서비스에서 검증)
httpRequest.requestMatchers("POST", "/poll/v1/*/response").permitAll()
httpRequest.requestMatchers("PUT", "/poll/v1/*/response").permitAll()
httpRequest.requestMatchers("DELETE", "/poll/v1/*/response").permitAll()

// Poll Result 조회 - 비로그인도 허용 (서비스에서 참여 여부 검증)
httpRequest.requestMatchers("GET", "/poll/v1/*/result").permitAll()

// Poll 조회 - GET만 인증 불필요
httpRequest.requestMatchers("GET", "/poll/v1/**").permitAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class PollCommandController(
- MULTIPLE_CHOICE: 다중 선택 (options 필수, minSelections/maxSelections 설정 가능)
- SCORE: 점수제 (options 불필요, minScore/maxScore 설정)

**비로그인 투표 (allowAnonymousVote):**
- true: 비로그인 사용자도 anonymous_session 쿠키로 투표 가능
- false (기본): 로그인한 사용자만 투표 가능

**주의사항:**
- startAt과 endAt은 필수입니다
- 점수제가 아닌 경우 최소 2개 이상의 옵션이 필요합니다
Expand Down Expand Up @@ -88,6 +92,10 @@ class PollCommandController(
- MULTIPLE_CHOICE: 다중 선택 (options 필수, minSelections/maxSelections 설정 가능)
- SCORE: 점수제 (options 불필요, minScore/maxScore 설정)

**비로그인 투표 (allowAnonymousVote):**
- true: 비로그인 사용자도 anonymous_session 쿠키로 투표 가능
- false (기본): 로그인한 사용자만 투표 가능

**주의사항:**
- DRAFT 상태에서는 응답을 받을 수 없습니다
- 나중에 수정하여 시작/종료 시간을 설정하고 IN_PROGRESS 상태로 전환할 수 있습니다
Expand Down Expand Up @@ -145,6 +153,7 @@ class PollCommandController(
- endAt은 필수입니다
- 점수제가 아닌 경우 최소 2개 이상의 옵션이 필요합니다
- 시스템 여론조사는 공개 목록에 노출되지 않으며 별도 API로 조회합니다
- 비로그인 투표(allowAnonymousVote)는 지원하지 않습니다 (항상 로그인 필수)
"""
)
@ApiResponses(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ class PollQueryController(
- EXPIRED: 기간만료만

**투표 여부 (isVoted):**
- 로그인한 경우: 각 여론조사에 투표했는지 여부 (true/false)
- 비로그인: 항상 false
- 로그인한 경우: accountId 기반으로 투표 여부 판단
- 비로그인 + anonymous_session 쿠키: 쿠키 기반으로 투표 여부 판단
- 비로그인 + 쿠키 없음: 항상 false

**비로그인 투표 (allowAnonymousVote):**
- allowAnonymousVote=true인 여론조사는 비로그인 사용자도 투표 가능
- anonymous_session 쿠키가 있으면 해당 쿠키로 투표 여부를 판단합니다
"""
)
@ApiResponses(
Expand Down Expand Up @@ -108,16 +113,24 @@ class PollQueryController(
@RequestParam(defaultValue = "ALL") status: PollStatusFilter = PollStatusFilter.ALL,
@Parameter(hidden = true)
@AuthenticationPrincipal userDetails: CustomUserDetails?,
@Parameter(
description = "비로그인 사용자 식별용 쿠키 (서버가 Set-Cookie로 발급, 브라우저가 자동 전송. 투표 여부 판단에 사용)",
example = "550e8400-e29b-41d4-a716-446655440000",
required = false
)
@CookieValue("anonymous_session", required = false) pollSession: String?,
): HttpApiResponse<SliceContent<PollListResponse>> {
val accountId = userDetails?.user?.accountId
val anonymousSessionId = if (accountId == null) pollSession else null
val response = pollQueryFacadeUseCase.getPollListByType(
pollType = type,
accountId = accountId,
size = size,
nextCursor = nextCursor,
keyword = keyword ?: "",
sortType = sort,
statusFilter = status
statusFilter = status,
anonymousSessionId = anonymousSessionId
)
return HttpApiResponse.of(response)
}
Expand All @@ -133,6 +146,8 @@ class PollQueryController(
- 상태 정보 (DRAFT, IN_PROGRESS, EXPIRED, CANCELLED 등)
- 기간 정보 (시작/종료 일시)
- 생성자 정보
- allowAnonymousVote: 비로그인 투표 허용 여부
- isVoted: 현재 사용자의 투표 여부 (비로그인 시 anonymous_session 쿠키로 판단)
"""
)
@ApiResponses(
Expand All @@ -155,9 +170,16 @@ class PollQueryController(
@PathVariable pollId: Long,
@Parameter(hidden = true)
@AuthenticationPrincipal userDetails: CustomUserDetails?,
@Parameter(
description = "비로그인 사용자 식별용 쿠키 (서버가 Set-Cookie로 발급, 브라우저가 자동 전송. 투표 여부 판단에 사용)",
example = "550e8400-e29b-41d4-a716-446655440000",
required = false
)
@CookieValue("anonymous_session", required = false) pollSession: String?,
): HttpApiResponse<PollDetailResponse> {
val accountId = userDetails?.user?.accountId
val response = pollQueryFacadeUseCase.getPollDetail(pollId, accountId)
val anonymousSessionId = if (accountId == null) pollSession else null
val response = pollQueryFacadeUseCase.getPollDetail(pollId, accountId, anonymousSessionId)
return HttpApiResponse.of(response)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import io.swagger.v3.oas.annotations.media.Schema
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.Cookie
import jakarta.servlet.http.HttpServletResponse
import org.springframework.core.env.Environment
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*
import java.util.UUID

@Tag(name = "여론조사 응답", description = "여론조사 응답 제출, 수정, 삭제 API")
@RestController
@RequestMapping("/poll/v1")
class PollResponseCommandController(
private val pollResponseCommandUseCase: PollResponseCommandUseCase,
private val environment: Environment,
) {
@Operation(
summary = "여론조사 응답 제출",
Expand All @@ -30,11 +35,19 @@ class PollResponseCommandController(
- MULTIPLE_CHOICE: {"responseType": "MULTIPLE_CHOICE", "optionIds": [1, 2, 3]}
- SCORE: {"responseType": "SCORE", "scoreValue": 8}

**비로그인 투표 (anonymous_session 쿠키):**
- 여론조사의 allowAnonymousVote=true인 경우, 로그인 없이도 투표 가능
- 비로그인 사용자는 서버가 발급하는 anonymous_session 쿠키로 식별됩니다
- 최초 투표 시 쿠키가 없으면 서버가 자동 발급 (Set-Cookie 응답 헤더)
- 이후 요청 시 브라우저가 쿠키를 자동 전송하여 동일 사용자 식별
- 쿠키 속성: HttpOnly, Secure, Max-Age=1년
- 민심투표(SYSTEM)는 비로그인 투표 불가 (항상 로그인 필수)

**주의사항:**
- responseType은 여론조사의 responseType과 일치해야 합니다
- 이미 응답한 경우 에러가 발생합니다 (updateResponse 사용)
- 투표 기간(startAt ~ endAt)에만 응답 가능합니다
- 인증된 사용자만 응답 가능합니다
- isRevotable 설정은 로그인/비로그인 동일하게 적용됩니다
"""
)
@ApiResponses(
Expand All @@ -51,7 +64,7 @@ class PollResponseCommandController(
),
ApiResponse(
responseCode = "401",
description = "인증 실패",
description = "인증 실패 (비로그인 투표가 허용되지 않는 경우)",
content = [Content(schema = Schema(implementation = HttpApiResponse::class))]
),
ApiResponse(
Expand All @@ -72,11 +85,31 @@ class PollResponseCommandController(
)
@RequestBody request: PollResponseSubmitRequest,
@Parameter(hidden = true)
@AuthenticationPrincipal userDetails: CustomUserDetails,
@AuthenticationPrincipal userDetails: CustomUserDetails?,
@Parameter(
description = "비로그인 사용자 식별용 쿠키 (최초 투표 시 서버가 Set-Cookie로 발급, 이후 브라우저가 자동 전송)",
example = "550e8400-e29b-41d4-a716-446655440000",
required = false
)
@CookieValue("anonymous_session", required = false) pollSession: String?,
@Parameter(hidden = true)
response: HttpServletResponse,
): HttpApiResponse<Long> {
val accountId = userDetails?.user?.accountId

// 비로그인 사용자: 쿠키 없으면 새로 발급
val anonymousSessionId = if (accountId == null) {
val sessionId = pollSession ?: UUID.randomUUID().toString()
if (pollSession == null) {
addPollSessionCookie(response, sessionId)
}
sessionId
} else null

val responseId = pollResponseCommandUseCase.submitResponse(
pollId = pollId,
accountId = userDetails.user.accountId,
accountId = accountId,
anonymousSessionId = anonymousSessionId,
request = request
)
return HttpApiResponse.of(responseId)
Expand All @@ -92,11 +125,15 @@ class PollResponseCommandController(
- MULTIPLE_CHOICE: {"responseType": "MULTIPLE_CHOICE", "optionIds": [1, 2, 3]}
- SCORE: {"responseType": "SCORE", "scoreValue": 8}

**비로그인 사용자:**
- anonymous_session 쿠키로 기존 응답을 식별하여 수정합니다
- 쿠키가 없는 경우 서버가 새로 발급합니다

**주의사항:**
- responseType은 여론조사의 responseType과 일치해야 합니다
- 응답하지 않은 경우 에러가 발생합니다 (submitResponse 사용)
- 투표 기간(startAt ~ endAt)에만 수정 가능합니다
- 재투표가 허용된 여론조사만 수정 가능합니다
- 재투표가 허용된(isRevotable=true) 여론조사만 수정 가능합니다
"""
)
@ApiResponses(
Expand Down Expand Up @@ -134,11 +171,30 @@ class PollResponseCommandController(
)
@RequestBody request: PollResponseSubmitRequest,
@Parameter(hidden = true)
@AuthenticationPrincipal userDetails: CustomUserDetails,
@AuthenticationPrincipal userDetails: CustomUserDetails?,
@Parameter(
description = "비로그인 사용자 식별용 쿠키 (최초 투표 시 서버가 Set-Cookie로 발급, 이후 브라우저가 자동 전송)",
example = "550e8400-e29b-41d4-a716-446655440000",
required = false
)
@CookieValue("anonymous_session", required = false) pollSession: String?,
@Parameter(hidden = true)
response: HttpServletResponse,
): HttpApiResponse<Long> {
val accountId = userDetails?.user?.accountId

val anonymousSessionId = if (accountId == null) {
val sessionId = pollSession ?: UUID.randomUUID().toString()
if (pollSession == null) {
addPollSessionCookie(response, sessionId)
}
sessionId
} else null

val responseId = pollResponseCommandUseCase.updateResponse(
pollId = pollId,
accountId = userDetails.user.accountId,
accountId = accountId,
anonymousSessionId = anonymousSessionId,
request = request
)
return HttpApiResponse.of(responseId)
Expand All @@ -149,10 +205,13 @@ class PollResponseCommandController(
description = """
이미 제출한 여론조사 응답을 삭제합니다.

**비로그인 사용자:**
- anonymous_session 쿠키로 기존 응답을 식별하여 삭제합니다

**주의사항:**
- 응답하지 않은 경우 에러가 발생합니다
- 투표 기간(startAt ~ endAt)에만 삭제 가능합니다
- 재투표가 허용된 여론조사만 삭제 가능합니다
- 재투표가 허용된(isRevotable=true) 여론조사만 삭제 가능합니다
- 삭제 후 다시 응답하려면 submitResponse를 사용하세요
"""
)
Expand Down Expand Up @@ -185,12 +244,33 @@ class PollResponseCommandController(
@Parameter(description = "여론조사 ID", required = true)
@PathVariable pollId: Long,
@Parameter(hidden = true)
@AuthenticationPrincipal userDetails: CustomUserDetails,
@AuthenticationPrincipal userDetails: CustomUserDetails?,
@Parameter(
description = "비로그인 사용자 식별용 쿠키 (최초 투표 시 서버가 Set-Cookie로 발급, 이후 브라우저가 자동 전송)",
example = "550e8400-e29b-41d4-a716-446655440000",
required = false
)
@CookieValue("anonymous_session", required = false) pollSession: String?,
): HttpApiResponse<Unit> {
val accountId = userDetails?.user?.accountId
val anonymousSessionId = if (accountId == null) pollSession else null

pollResponseCommandUseCase.deleteResponse(
pollId = pollId,
accountId = userDetails.user.accountId
accountId = accountId,
anonymousSessionId = anonymousSessionId
)
return HttpApiResponse.of(Unit)
}

private fun addPollSessionCookie(response: HttpServletResponse, sessionId: String) {
val isProduction = environment.activeProfiles.contains("prod")
val cookie = Cookie("anonymous_session", sessionId).apply {
path = "/"
isHttpOnly = true
maxAge = 365 * 24 * 60 * 60 // 1년
secure = isProduction
}
response.addCookie(cookie)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class PollResponseQueryController(
- SCORE: scoreValue
- 공통: createdAt, updatedAt

**비로그인 사용자:**
- anonymous_session 쿠키가 있으면 해당 쿠키로 myResponse를 조회합니다
- 비로그인 투표 후 같은 브라우저에서 결과 조회 시 내 응답 정보 확인 가능

**주의사항:**
- 투표에 참여한 사용자만 결과를 조회할 수 있습니다
- 진행 중이거나 종료된 여론조사의 결과를 확인할 수 있습니다
Expand Down Expand Up @@ -94,9 +98,17 @@ class PollResponseQueryController(
@Parameter(description = "여론조사 ID", required = true)
@PathVariable pollId: Long,
@Parameter(hidden = true)
@AuthenticationPrincipal userDetails: CustomUserDetails,
@AuthenticationPrincipal userDetails: CustomUserDetails?,
@Parameter(
description = "비로그인 사용자 식별용 쿠키 (서버가 Set-Cookie로 발급, 브라우저가 자동 전송. 내 응답 조회에 사용)",
example = "550e8400-e29b-41d4-a716-446655440000",
required = false
)
@CookieValue("anonymous_session", required = false) pollSession: String?,
): HttpApiResponse<PollResultResponse> {
val response = pollResultQueryUseCase.getPollResult(pollId, userDetails.user.accountId)
val accountId = userDetails?.user?.accountId
val anonymousSessionId = if (accountId == null) pollSession else null
val response = pollResultQueryUseCase.getPollResult(pollId, accountId, anonymousSessionId)
return HttpApiResponse.of(response)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ data class PublicPollCreateRequest(
@Schema(description = "여론조사 종료 일시", example = "2025-01-17T23:59:59", required = true)
val endAt: LocalDateTime,

@Schema(description = "비로그인 투표 허용 여부 (기본: false)", example = "false", required = false)
val allowAnonymousVote: Boolean = false,

@field:Valid
@Schema(description = "선택지 목록 (SINGLE_CHOICE, MULTIPLE_CHOICE일 때 필수)", required = false)
val options: List<PollOptionRequest>? = null,
Expand Down Expand Up @@ -75,6 +78,9 @@ data class PublicPollDraftCreateRequest(
@Schema(description = "재투표 가능 여부", example = "false", required = true)
val isRevotable: Boolean,

@Schema(description = "비로그인 투표 허용 여부 (기본: false)", example = "false", required = false)
val allowAnonymousVote: Boolean = false,

@field:Valid
@Schema(description = "선택지 목록 (SINGLE_CHOICE, MULTIPLE_CHOICE일 때 필수)", required = false)
val options: List<PollOptionRequest>? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
package com.futiland.vote.application.poll.dto.response

import com.futiland.vote.domain.poll.entity.*
import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime

@Schema(description = "여론조사 상세 응답")
data class PollDetailResponse(
@Schema(description = "여론조사 ID", example = "1")
val id: Long,
@Schema(description = "여론조사 제목", example = "좋아하는 과일은?")
val title: String,
@Schema(description = "여론조사 설명", example = "가장 좋아하는 과일을 선택하세요")
val description: String,
@Schema(description = "응답 유형", example = "SINGLE_CHOICE")
val responseType: ResponseType,
@Schema(description = "여론조사 상태", example = "IN_PROGRESS")
val status: PollStatus,
@Schema(description = "재투표 가능 여부", example = "true")
val isRevotable: Boolean,
@Schema(description = "비로그인 투표 허용 여부 (true: 비로그인 사용자도 투표 가능)", example = "false")
val allowAnonymousVote: Boolean,
@Schema(description = "시작 일시", example = "2024-01-01T00:00:00")
val startAt: LocalDateTime?,
@Schema(description = "종료 일시", example = "2024-12-31T23:59:59")
val endAt: LocalDateTime?,
@Schema(description = "생성 일시", example = "2024-01-01T00:00:00")
val createdAt: LocalDateTime,
@Schema(description = "전체 응답 수 (로그인 + 비로그인 합산)", example = "1234")
val responseCount: Long,
@Schema(description = "선택지 목록")
val options: List<PollOptionResponse>,
@Schema(description = "현재 사용자의 투표 여부 (로그인: accountId 기반, 비로그인: anonymous_session 쿠키 기반)", example = "true")
val isVoted: Boolean,
@Schema(description = "작성자 정보")
val creatorInfo: CreatorInfoResponse,
) {
companion object {
Expand All @@ -33,6 +50,7 @@ data class PollDetailResponse(
responseType = poll.responseType,
status = poll.status,
isRevotable = poll.isRevotable,
allowAnonymousVote = poll.allowAnonymousVote,
startAt = poll.startAt,
endAt = poll.endAt,
createdAt = poll.createdAt,
Expand Down
Loading
Loading