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
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,15 @@ jobs:
APP_JWT_ACCESS_TOKEN_EXPIRATION_SECONDS: 3600
APP_JWT_REFRESH_TOKEN_EXPIRATION_SECONDS: 1209600
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_OAUTH_CLIENT_SECRET: ${{secrets.GOOGLE_OAUTH_CLIENT_SECRET}}
GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}
APP_OAUTH2_REDIRECT_SUCCESS_URL: ${{ secrets.OAUTH2_REDIRECT_SUCCESS_URL }}
APP_OAUTH2_EXTRA_INFO_URL: ${{ secrets.EXTRA_INFO_URL }}
FIREBASE_SERVICE_ACCOUNT_PATH: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PATH }}
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
envs: DATABASE_HOST,DATABASE_USERNAME,DATABASE_PASSWORD,APP_JWT_SECRET,APP_JWT_ACCESS_TOKEN_EXPIRATION_SECONDS,APP_JWT_REFRESH_TOKEN_EXPIRATION_SECONDS, APP_OAUTH2_REDIRECT_SUCCESS_URL, APP_OAUTH2_EXTRA_INFO_URL, GOOGLE_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET
envs: DATABASE_HOST,DATABASE_USERNAME,DATABASE_PASSWORD,APP_JWT_SECRET,APP_JWT_ACCESS_TOKEN_EXPIRATION_SECONDS,APP_JWT_REFRESH_TOKEN_EXPIRATION_SECONDS,APP_OAUTH2_REDIRECT_SUCCESS_URL,APP_OAUTH2_EXTRA_INFO_URL,GOOGLE_CLIENT_ID,GOOGLE_OAUTH_CLIENT_SECRET,FIREBASE_SERVICE_ACCOUNT_PATH
script: |
curl -fsSL https://raw.githubusercontent.com/JECT-Study/JECT2-4th-Server/main/scripts/setup-docker-caddy.sh \
| bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.ject.vs.config.CookieProperties;
import com.ject.vs.config.JwtProperties;
import com.ject.vs.util.CookieUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -14,6 +16,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "인증", description = "인증 관련 API")
@RestController
@RequiredArgsConstructor
public class AuthController {
Expand All @@ -22,6 +25,7 @@ public class AuthController {
private final JwtProperties jwtProperties;
private final CookieProperties cookieProperties;

@Operation(summary = "토큰 재발급", description = "Refresh Token으로 Access Token을 재발급합니다.")
@PostMapping("/auth/reissue")
public ResponseEntity<Void> reissue(HttpServletRequest request, HttpServletResponse response) {
String refreshToken = cookieUtil.getCookieValue(request, CookieUtil.CookieType.REFRESH_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
import com.ject.vs.notification.adapter.web.dto.RegisterPushTokenRequest;
import com.ject.vs.notification.port.in.PushTokenUseCase;
import com.ject.vs.vote.exception.UnauthorizedException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "푸시 알림", description = "푸시 토큰 등록/해제 API (회원 전용)")
@RestController
@RequestMapping("/api/devices")
@RequiredArgsConstructor
public class DeviceController {

private final PushTokenUseCase useCase;

@Operation(summary = "푸시 토큰 등록", description = "FCM/APNs 디바이스 토큰을 등록합니다. 투표 종료 시 푸시 알림 발송에 사용됩니다.")
@PostMapping("/push-token")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void register(
Expand All @@ -25,6 +29,7 @@ public void register(
useCase.register(userId, request.token(), request.platform());
}

@Operation(summary = "푸시 토큰 해제", description = "등록된 모든 푸시 토큰을 해제합니다.")
@DeleteMapping("/push-token")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void unregisterAll(@AuthenticationPrincipal Long userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
import com.ject.vs.notification.port.in.NotificationCommandUseCase;
import com.ject.vs.notification.port.in.NotificationQueryUseCase;
import com.ject.vs.vote.exception.UnauthorizedException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "알림", description = "알림 관련 API (회원 전용)")
@RestController
@RequestMapping("/api/notifications")
@RequiredArgsConstructor
Expand All @@ -19,6 +22,7 @@ public class NotificationController {
private final NotificationQueryUseCase queryUseCase;
private final NotificationCommandUseCase commandUseCase;

@Operation(summary = "알림 목록 조회", description = "알림 목록을 조회합니다. 커서 기반 페이지네이션을 지원합니다.")
@GetMapping
public NotificationListResponse getList(
@AuthenticationPrincipal Long userId,
Expand All @@ -28,12 +32,14 @@ public NotificationListResponse getList(
return NotificationListResponse.from(queryUseCase.getList(userId, cursor, size));
}

@Operation(summary = "읽지 않은 알림 수 조회", description = "읽지 않은 알림의 개수를 조회합니다.")
@GetMapping("/unread-count")
public UnreadCountResponse getUnreadCount(@AuthenticationPrincipal Long userId) {
if (userId == null) throw new UnauthorizedException();
return new UnreadCountResponse(queryUseCase.getUnreadCount(userId));
}

@Operation(summary = "알림 읽음 처리", description = "특정 알림을 읽음 처리합니다.")
@PostMapping("/{notificationId}/read")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void markAsRead(
Expand All @@ -43,6 +49,7 @@ public void markAsRead(
commandUseCase.markAsRead(notificationId, userId);
}

@Operation(summary = "모든 알림 읽음 처리", description = "모든 알림을 읽음 처리합니다.")
@PostMapping("/read-all")
public ReadAllResponse markAllAsRead(@AuthenticationPrincipal Long userId) {
if (userId == null) throw new UnauthorizedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,30 @@
import com.ject.vs.notification.port.in.NotificationPromptUseCase;
import com.ject.vs.notification.port.in.NotificationPromptUseCase.PromptStatusResult;
import com.ject.vs.vote.exception.UnauthorizedException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "알림 권한 프롬프트", description = "알림 권한 요청 프롬프트 관련 API (회원 전용)")
@RestController
@RequestMapping("/api/me/notification-prompt")
@RequiredArgsConstructor
public class NotificationPromptController {

private final NotificationPromptUseCase useCase;

@Operation(summary = "프롬프트 표시 여부 조회", description = "알림 권한 요청 프롬프트를 표시해야 하는지 여부를 조회합니다.")
@GetMapping("/status")
public PromptStatusResponse getStatus(@AuthenticationPrincipal Long userId) {
if (userId == null) throw new UnauthorizedException();
PromptStatusResult result = useCase.getStatus(userId);
return new PromptStatusResponse(result.shouldShow(), result.totalParticipationCount());
}

@Operation(summary = "프롬프트 거절 기록", description = "사용자가 알림 권한 프롬프트를 거절했음을 기록합니다.")
@PostMapping("/dismissed")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void recordDismissed(@AuthenticationPrincipal Long userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,29 @@
import com.ject.vs.notification.adapter.web.dto.UpdateNotificationSettingRequest;
import com.ject.vs.notification.port.in.NotificationSettingUseCase;
import com.ject.vs.vote.exception.UnauthorizedException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "알림 설정", description = "알림 설정 관련 API (회원 전용)")
@RestController
@RequestMapping("/api/me/notification-setting")
@RequiredArgsConstructor
public class NotificationSettingController {

private final NotificationSettingUseCase useCase;

@Operation(summary = "알림 설정 조회", description = "현재 알림 설정 상태를 조회합니다.")
@GetMapping
public NotificationSettingResponse get(@AuthenticationPrincipal Long userId) {
if (userId == null) throw new UnauthorizedException();
return NotificationSettingResponse.from(useCase.get(userId));
}

@Operation(summary = "알림 설정 변경", description = "푸시 알림 ON/OFF 설정을 변경합니다.")
@PutMapping
public NotificationSettingResponse update(
@AuthenticationPrincipal Long userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.ject.vs.user.adapter.web.dto.UserProfileRequest;
import com.ject.vs.user.adapter.web.dto.UserProfileResponse;
import com.ject.vs.user.port.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -17,40 +19,46 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "사용자", description = "사용자 프로필 관련 API")
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;

@Operation(summary = "추가 정보 설정", description = "사용자 추가 정보(닉네임, 성별, 생년월일)를 설정합니다.")
@PostMapping("/me/profile")
public ResponseEntity<UserProfileResponse> setupInfo(@AuthenticationPrincipal Long userId, @RequestBody UserExtraInfo userExtraInfo) {
UserProfileResponse response = userService.setupAdditionalInfo(userExtraInfo, userId);

return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@Operation(summary = "닉네임 중복 확인", description = "닉네임 사용 가능 여부를 확인합니다.")
@PostMapping("/nickname/check")
public ResponseEntity<NicknameCheckResponse> isUniqueNickname(@AuthenticationPrincipal Long userId, @RequestBody UserNicknameRec nickname) {
NicknameCheckResponse response = userService.checkNickname(nickname.nickname(), userId);

return ResponseEntity.ok(response);
}

@Operation(summary = "닉네임 추천", description = "사용 가능한 랜덤 닉네임을 추천합니다.")
@GetMapping("/nickname/suggest")
public ResponseEntity<UserNicknameRec> suggestNickname(@AuthenticationPrincipal Long userId) {
UserNicknameRec response = userService.suggestNickname(userId);

return ResponseEntity.ok(response);
}

@Operation(summary = "내 프로필 조회", description = "로그인한 사용자의 프로필 정보를 조회합니다.")
@GetMapping("/me")
public ResponseEntity<UserProfileResponse> getMyProfile(@AuthenticationPrincipal Long userId) {
UserProfileResponse response = userService.getUserProfile(userId);

return ResponseEntity.ok(response);
}

@Operation(summary = "기본 프로필 초기화", description = "사용자 기본 프로필 정보를 초기화합니다.")
@PostMapping("/info")
public ResponseEntity<UserProfileDefaultResponse> initializeDefaultProfile(@AuthenticationPrincipal Long userId, @RequestBody UserProfileRequest userInfo) {
return ResponseEntity.ok(userService.initializeDefaultProfile(userId, userInfo));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
import com.ject.vs.vote.adapter.web.dto.FreeVotesResponse;
import com.ject.vs.vote.domain.GuestFreeVote;
import com.ject.vs.vote.port.GuestFreeVoteService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "비회원 무료 투표", description = "비회원 무료 투표권 관련 API")
@RestController
@RequestMapping("/api/me")
@RequiredArgsConstructor
public class GuestFreeVoteController {

private final GuestFreeVoteService guestFreeVoteService;

@Operation(summary = "잔여 무료 투표권 조회", description = "비회원의 잔여 무료 투표권 수를 조회합니다. 회원은 remainingFreeVotes가 null로 응답됩니다.")
@GetMapping("/free-votes")
public FreeVotesResponse getFreeVotes(@AnonymousId String anonymousId) {
public FreeVotesResponse getFreeVotes(@Parameter(hidden = true) @AnonymousId String anonymousId) {
return new FreeVotesResponse(
guestFreeVoteService.remaining(anonymousId),
GuestFreeVote.totalFreeVotes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
import com.ject.vs.vote.port.in.ImmersiveVoteCommandUseCase;
import com.ject.vs.vote.port.in.ImmersiveVoteQueryUseCase;
import com.ject.vs.vote.port.in.VoteResultQueryUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "몰입형 투표", description = "몰입형 투표 피드 관련 API")
@RestController
@RequestMapping("/api/immersive-votes")
@RequiredArgsConstructor
Expand All @@ -23,31 +27,35 @@ public class ImmersiveVoteController {
private final ImmersiveVoteQueryUseCase immersiveVoteQueryUseCase;
private final VoteResultQueryUseCase voteResultQueryUseCase;

@Operation(summary = "몰입형 투표 피드 조회", description = "스와이프 형식의 몰입형 투표 피드를 조회합니다. 커서 기반 페이지네이션을 지원합니다.")
@GetMapping
public ImmersiveFeedResponse getFeed(
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "10") int size,
@AuthenticationPrincipal Long userId,
@AnonymousId String anonymousId) {
@Parameter(hidden = true) @AnonymousId String anonymousId) {
return ImmersiveFeedResponse.from(immersiveVoteQueryUseCase.getFeed(cursor, size, userId, anonymousId));
}

@Operation(summary = "투표 참여/취소", description = "투표에 참여하거나 같은 옵션 재클릭 시 취소합니다. 비회원은 5회까지 무료 투표 가능합니다.")
@PostMapping("/{voteId}/participate")
public ImmersiveParticipateResponse participateOrCancel(
@PathVariable Long voteId,
@AuthenticationPrincipal Long userId,
@AnonymousId String anonymousId,
@Parameter(hidden = true) @AnonymousId String anonymousId,
@RequestBody @Valid ParticipateRequest request) {
ImmersiveVoteCommandUseCase.ImmersiveParticipateResult result =
immersiveVoteCommandUseCase.participateOrCancel(voteId, userId, anonymousId, request.optionId());
return ImmersiveParticipateResponse.from(result);
}

@Operation(summary = "실시간 투표 현황 조회", description = "투표 후 실시간 비율 갱신을 위한 폴링 API입니다.")
@GetMapping("/{voteId}/live")
public ImmersiveLiveResponse getLive(@PathVariable Long voteId) {
return ImmersiveLiveResponse.from(immersiveVoteQueryUseCase.getLive(voteId));
}

@Operation(summary = "공유 링크 생성", description = "투표 공유를 위한 링크를 생성합니다.")
@GetMapping("/{voteId}/share")
public ShareLinkResponse getShareLink(@PathVariable Long voteId) {
return ShareLinkResponse.from(voteResultQueryUseCase.getShareLink(voteId));
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/com/ject/vs/vote/adapter/web/VoteController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
import com.ject.vs.vote.exception.UnauthorizedException;
import com.ject.vs.vote.port.VoteDetailQueryService;
import com.ject.vs.vote.port.in.VoteCommandUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "일반형 투표", description = "일반형 투표 관련 API")
@RestController
@RequestMapping("/api/votes")
@RequiredArgsConstructor
Expand All @@ -19,6 +23,7 @@ public class VoteController {
private final VoteCommandUseCase voteCommandUseCase;
private final VoteDetailQueryService voteDetailQueryService;

@Operation(summary = "투표 생성", description = "새로운 투표를 생성합니다. 회원만 가능합니다.")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public VoteCreateResponse create(
Expand All @@ -28,26 +33,29 @@ public VoteCreateResponse create(
return VoteCreateResponse.from(voteCommandUseCase.create(request.toCommand()));
}

@Operation(summary = "투표 상세 조회", description = "투표 상세 정보를 조회합니다. 투표 전에는 결과(voteCount/ratio)가 null로 응답됩니다.")
@GetMapping("/{voteId}")
public VoteDetailResponse getDetail(
@PathVariable Long voteId,
@AuthenticationPrincipal Long userId,
@AnonymousId String anonymousId) {
@Parameter(hidden = true) @AnonymousId String anonymousId) {
return VoteDetailResponse.from(voteDetailQueryService.getDetail(voteId, userId, anonymousId));
}

@Operation(summary = "투표 참여", description = "투표에 참여합니다. 비회원은 5회까지 무료 투표 가능합니다.")
@PostMapping("/{voteId}/participate")
public ParticipateResponse participate(
@PathVariable Long voteId,
@AuthenticationPrincipal Long userId,
@AnonymousId String anonymousId,
@Parameter(hidden = true) @AnonymousId String anonymousId,
@RequestBody @Valid ParticipateRequest request) {
VoteCommandUseCase.ParticipateResult result = userId != null
? voteCommandUseCase.participateAsMember(voteId, userId, request.optionId())
: voteCommandUseCase.participateAsGuest(voteId, anonymousId, request.optionId());
return ParticipateResponse.from(result);
}

@Operation(summary = "다시 투표하기", description = "투표를 취소합니다. 회원만 가능합니다.")
@DeleteMapping("/{voteId}/participate")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void cancel(
Expand Down
Loading
Loading