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
53 changes: 53 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Java CI/CD with Gradle

on:
push:
branches: [ "dev" ] # dev 브랜치에 푸시할 때 작동
workflow_dispatch: #

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'gradle' # 캐싱 추가: 빌드 속도가 훨씬 빨라집니다.

- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew build -x test

- name: Create .env file from Secret
run: |
cat <<'EOF' > .env
${{ secrets.ENV_VARIABLES }}
EOF

- name: Copy JAR and .env to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
# -plain.jar는 배포에 필요 없으므로 제외합니다.
source: "build/libs/*-SNAPSHOT.jar, .env"
target: "~/"
strip_components: 2

- name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
fuser -k 8080/tcp || true

chmod +x ~/start.sh
~/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.swyp.picke.domain.battle.controller;

import com.swyp.picke.domain.battle.dto.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.battle.dto.request.AdminBattleUpdateRequest;
import com.swyp.picke.domain.battle.dto.response.AdminBattleDeleteResponse;
import com.swyp.picke.domain.battle.dto.response.AdminBattleDetailResponse;
import com.swyp.picke.domain.battle.service.BattleService;
import com.swyp.picke.global.common.response.ApiResponse;
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.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "배틀 API (관리자)", description = "배틀 생성/수정/삭제 (관리자 전용)")
@RestController
@RequestMapping("/api/v1/admin/battles")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class AdminBattleController {

private final BattleService battleService;

@Operation(summary = "배틀 생성")
@PostMapping
public ApiResponse<AdminBattleDetailResponse> createBattle(
@RequestBody @Valid AdminBattleCreateRequest request,
@AuthenticationPrincipal Long adminUserId
) {
return ApiResponse.onSuccess(battleService.createBattle(request, adminUserId));
}

@Operation(summary = "배틀 수정 (변경 필드만 포함)")
@PatchMapping("/{battleId}")
public ApiResponse<AdminBattleDetailResponse> updateBattle(
@PathVariable Long battleId,
@RequestBody @Valid AdminBattleUpdateRequest request
) {
return ApiResponse.onSuccess(battleService.updateBattle(battleId, request));
}

@Operation(summary = "배틀 삭제")
@DeleteMapping("/{battleId}")
public ApiResponse<AdminBattleDeleteResponse> deleteBattle(
@PathVariable Long battleId
) {
return ApiResponse.onSuccess(battleService.deleteBattle(battleId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.swyp.picke.domain.battle.dto.request;

import com.swyp.picke.domain.battle.enums.BattleStatus;
import com.swyp.picke.domain.battle.enums.BattleType;
import java.time.LocalDate;
import java.util.List;

public record AdminBattleCreateRequest(
String title,
String titlePrefix,
String titleSuffix,
String summary,
String description,
String thumbnailUrl,
BattleType type,
BattleStatus status,
String itemA,
String itemADesc,
String itemB,
String itemBDesc,
LocalDate targetDate,
List<Long> tagIds,
List<AdminBattleOptionRequest> options
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.swyp.picke.domain.battle.dto.request;

import com.swyp.picke.domain.battle.enums.BattleOptionLabel;

import java.util.List;

public record AdminBattleOptionRequest(
BattleOptionLabel label,
String title,
String stance,
String representative,
String quote,
String imageUrl,
Boolean isCorrect,
List<Long> tagIds // 옵션 전용 태그 (철학자, 가치관 - 추후 사용자 유형 분석에 사용)
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.swyp.picke.domain.battle.dto.request;

import com.swyp.picke.domain.battle.enums.BattleStatus;
import java.time.LocalDate;
import java.util.List;

public record AdminBattleUpdateRequest(
String title,
String titlePrefix,
String titleSuffix,
String summary,
String description,
String thumbnailUrl,
String itemA,
String itemADesc,
String itemB,
String itemBDesc,
LocalDate targetDate,
Integer audioDuration,
BattleStatus status,
List<Long> tagIds,
List<AdminBattleOptionRequest> options
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.swyp.picke.domain.battle.dto.response;

import java.time.LocalDateTime;

/**
* 관리자 - 배틀 삭제 응답
* 역할: 배틀이 성공적으로 소프트 딜리트 되었는지 확인하고 삭제 시점을 반환합니다.
*/

public record AdminBattleDeleteResponse(
Boolean success, // 삭제 성공 여부
LocalDateTime deletedAt // 삭제 처리된 일시 (Soft Delete)
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.swyp.picke.domain.battle.dto.response;

import com.swyp.picke.domain.battle.enums.BattleCreatorType;
import com.swyp.picke.domain.battle.enums.BattleStatus;
import com.swyp.picke.domain.battle.enums.BattleType;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

/**
* 관리자 - 배틀 상세 상세 조회 응답
* 역할: 관리자가 배틀의 모든 설정 값(상태, 생성자 타입, 수정일 등)을 확인하고 수정할 때 사용합니다.
*/

public record AdminBattleDetailResponse(
Long battleId,
String title,
String titlePrefix,
String titleSuffix,
String summary,
String description,
String thumbnailUrl,
BattleType type,
String itemA,
String itemADesc,
String itemB,
String itemBDesc,
LocalDate targetDate,
BattleStatus status,
BattleCreatorType creatorType,
List<BattleTagResponse> tags,
List<BattleOptionResponse> options,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.swyp.picke.domain.battle.enums;

public enum BattleType {
BATTLE, QUIZ, VOTE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.swyp.picke.domain.tag.dto.request;

import com.swyp.picke.domain.tag.enums.TagType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record TagRequest(
@NotBlank(message = "태그 이름을 입력해주세요.")
String name,

@NotNull(message = "태그 타입을 선택해주세요.")
TagType type
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.swyp.picke.domain.tag.dto.response;

import java.time.LocalDateTime;

public record TagDeleteResponse(
boolean success,
LocalDateTime deletedAt
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.swyp.picke.domain.tag.dto.response;

import com.swyp.picke.domain.tag.enums.TagType;
import java.time.LocalDateTime;

public record TagResponse(
Long tagId,
String name,
TagType type,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.swyp.picke.domain.test.controller;

import com.swyp.picke.domain.oauth.jwt.JwtProvider;
import com.swyp.picke.global.common.response.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/test")
@RequiredArgsConstructor
public class TestController {

private final JwtProvider jwtProvider;

@GetMapping("/response")
public ApiResponse<List<String>> testResponse() {
List<String> teamMembers = List.of("주천수", "팀원2", "팀원3", "팀원4");
return ApiResponse.onSuccess(teamMembers);
}

@GetMapping("/token")
public ApiResponse<Map<String, String>> getTestToken(
@RequestParam(defaultValue = "1") Long userId
) {
String token = jwtProvider.createAccessToken(userId, "USER");
return ApiResponse.onSuccess(Map.of("accessToken", token));
}
}
87 changes: 87 additions & 0 deletions src/main/java/com/swyp/picke/domain/vote/entity/Vote.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.swyp.picke.domain.vote.entity;

import com.swyp.picke.domain.battle.entity.Battle;
import com.swyp.picke.domain.battle.entity.BattleOption;
import com.swyp.picke.domain.user.entity.User;
import com.swyp.picke.global.common.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "votes")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Vote extends BaseEntity {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "battle_id", nullable = false)
private Battle battle;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pre_vote_option_id")
private BattleOption preVoteOption;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_vote_option_id")
private BattleOption postVoteOption;

@Column(name = "is_tts_listened", nullable = false)
private Boolean isTtsListened = false;

@Builder
private Vote(User user, Battle battle, BattleOption preVoteOption,
BattleOption postVoteOption, Boolean isTtsListened) {
this.user = user;
this.battle = battle;
this.preVoteOption = preVoteOption;
this.postVoteOption = postVoteOption;
this.isTtsListened = isTtsListened != null ? isTtsListened : false;
}

/**
* 최초 투표(사전 투표) 시 사용하는 정적 팩토리 메서드
*/
public static Vote createPreVote(User user, Battle battle, BattleOption option) {
return Vote.builder()
.user(user)
.battle(battle)
.preVoteOption(option)
.isTtsListened(false)
// status 설정 삭제됨
.build();
}

/**
* 사전 투표 옵션 수정 메서드
*/
public void updatePreVote(BattleOption preVoteOption) {
this.preVoteOption = preVoteOption;
}

/**
* 사후 투표 업데이트
*/
public void doPostVote(BattleOption postOption) {
this.postVoteOption = postOption;
// status 업데이트 삭제됨
}

/**
* TTS 청취 상태 업데이트
*/
public void completeTts() {
this.isTtsListened = true;
}
}
Loading