Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
85b19b7
[Feat] 소셜 로그인 및 토큰 재발급 API 개발 (#24)
soonge2 Apr 30, 2025
1ba733f
[Chore] dev 배포 환경 준비 및 테스트용 기능 추가 (#26)
soonge2 May 1, 2025
1ee33cc
[Feat] 프론트-백-AI 서버 간 연동 테스트를 위한 엔드포인트 추가 (#27)
soonge2 May 1, 2025
e78e1ff
chore: MockAiControllerTest 삭제 및 local 프로필 추가
soonge2 May 1, 2025
376019a
chore: .gitignore에 .env 파일 및 로컬 실행 스크립트(.sh) 추가
soonge2 May 1, 2025
b03b651
[Feat] JWT 인증 필터 구현 (#39)
soonge2 May 1, 2025
1d7ebcc
[Feat] 로그아웃 API 구현 및 인증 실패 시 401 응답 처리 (#40)
soonge2 May 1, 2025
a463912
[Feat] 투표 도메인 엔티티 및 리포지토리 생성 (#41)
soonge2 May 2, 2025
9c35c06
[Feat] 투표 등록 API 개발 (#42)
soonge2 May 3, 2025
be605d1
[Feat] 투표 내용 조회 API 개발 (#43)
soonge2 May 3, 2025
85525e0
[Feat] 투표 참여 API 개발 (#45)
soonge2 May 3, 2025
4855271
[Feat] 투표 결과 조회 API 개발 (#46)
soonge2 May 4, 2025
c785df7
[Feat] Dev 환경에 Jwt 인증 및 예외 처리 필터 적용 (#48)
soonge2 May 4, 2025
c586342
[CICD] Dev 서버용 배포 파이프라인 생성 (#49)
yunabyte May 4, 2025
2b02301
[Feat] 진행 중인 투표 목록 조회 API 개발 (#50)
soonge2 May 5, 2025
c8466ac
[Feat] 내가 만든 투표 목록 조회 API 개발 (#57)
soonge2 May 6, 2025
c33ff95
[Feat] 내가 참여한 투표 목록 조회 API 개발 (#58)
soonge2 May 6, 2025
d9045ad
[Feat] 가입한 그룹 라벨 조회 API 개발 (#59)
soonge2 May 6, 2025
d0aa66e
[Feat] 가입한 그룹 목록 조회 API 개발 (#60)
soonge2 May 6, 2025
4d5c325
[Feat] 그룹 가입 API 개발 (#61)
soonge2 May 6, 2025
b97d9df
[Feat] 새 그룹 생성 API 개발 (#62)
soonge2 May 6, 2025
93a467e
[Feat] 회원정보 조회/수정 API 개발 (#63)
soonge2 May 6, 2025
87764f5
[Feat] 회원 탈퇴 API 개발 (#64)
soonge2 May 7, 2025
591c23d
[CICD] 환경변수 추가 주입 과정 추가
May 7, 2025
d68a20e
[Feat] 랜덤 닉네임 생성 기능 구현 (#65)
soonge2 May 7, 2025
6836a4d
[Chore] Dev CORS에 localhost 추가
soonge2 May 7, 2025
437f9d0
[Fix] 투표 결과 비율 계산에 double 타입 적용
soonge2 May 7, 2025
ae487de
[Feat] 카카오 로그인 시 동적 redirect URI 허용 및 검증 추가 (#67)
soonge2 May 7, 2025
259f6e0
[Refactor] OAuth ID 타입 Long → String으로 변경
soonge2 May 7, 2025
31eec6d
[Feat] 투표 등록 시 익명 기능 추가 (#70)
soonge2 May 8, 2025
3c65947
[Feat] AI 기반 투표 내용 검열 기능 추가 (#71)
soonge2 May 8, 2025
0b4f008
[Fix] USER_NOT_FOUND 에러 상태 코드 변경 (404 → 401)
soonge2 May 8, 2025
22b9b8b
[Fix] 진행 중인 투표 조회 시 PENDING/REJECTED 필터링 추가
soonge2 May 8, 2025
64b3c3b
[Chore] prod 환경의 JPA ddl-auto 옵션을 none → update로 변경
soonge2 May 8, 2025
fd47983
Merge branch 'main' into develop
soonge2 May 8, 2025
cb38c1f
fix: 익명 투표일 경우 adminVote 강제 비표시 처리, 투표 내용 조회 시 adminVote 응답
soonge2 May 8, 2025
81fc6a1
fix: 투표 참여 목록에서 기권한 항목 제외 처리
soonge2 May 8, 2025
741df3a
Merge branch 'main' into develop
soonge2 May 8, 2025
09d0809
[CICD] PR 생성 시 리뷰어 자동 등록 (#76)
yunabyte May 10, 2025
6809ee1
[Feat] 사용자 피드백 등록 API 개발 (#78)
soonge2 May 11, 2025
ccd41ba
[Fix] 투표 종료 시각 UTC 저장 및 로컬 환경 처리 개선 (#80)
soonge2 May 11, 2025
d65da0f
[Fix] 투표 상태 응답을 종료 시각 기반으로 계산하도록 수정 (#82)
soonge2 May 11, 2025
af5d870
[Fix] #81 병합 후 REJECTED, PENDING 상태 반영 누락 수정 (#83)
soonge2 May 11, 2025
070c07f
[Fix] Vote 종료 시각 UTC 저장 누락 수정 (#84)
soonge2 May 11, 2025
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
8 changes: 8 additions & 0 deletions .github/auto_assign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
addReviewers: true
numberOfReviewers: 2
reviewers:
- soonge2
- yunabyte
- Mason-P-ark

addAssignees: false
22 changes: 22 additions & 0 deletions .github/workflows/pr-auto-reviewer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Auto assign reviewer

on:
pull_request:
branches:
- main
- develop


permissions:
pull-requests: write


jobs:
assign-reviewer:
runs-on: ubuntu-latest

steps:
- name: Assign reviewer
uses: kentaro-m/auto-assign-action@v1.2.1
with:
configuration-path: ".github/auto_assign.yml"
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.moa.moa_server.domain.feedback.controller;

import com.moa.moa_server.domain.feedback.dto.request.FeedbackCreateRequest;
import com.moa.moa_server.domain.feedback.service.FeedbackService;
import com.moa.moa_server.domain.global.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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
@RequiredArgsConstructor
@RequestMapping("/api/v1/user-feedback")
public class FeedbackController {

private final FeedbackService feedbackService;

@PostMapping
public ResponseEntity<ApiResponse> createFeedback(
@AuthenticationPrincipal Long userId,
@RequestBody FeedbackCreateRequest request
) {
feedbackService.createFeedback(userId, request);
return ResponseEntity
.status(201)
.body(new ApiResponse("SUCCESS", null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.moa.moa_server.domain.feedback.dto.request;

public record FeedbackCreateRequest (
String content
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.moa.moa_server.domain.feedback.entity;

import com.moa.moa_server.domain.global.entity.BaseTimeEntity;
import com.moa.moa_server.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Feedback extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

@Column(length = 500, nullable = false)
private String content;

public static Feedback create(User user, String content) {
return Feedback.builder()
.user(user)
.content(content)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.moa.moa_server.domain.feedback.handler;

import com.moa.moa_server.domain.global.exception.BaseErrorCode;
import org.springframework.http.HttpStatus;

public enum FeedbackErrorCode implements BaseErrorCode {
INVALID_INPUT(HttpStatus.BAD_REQUEST);

private final HttpStatus status;

FeedbackErrorCode(HttpStatus status) { this.status = status; }

public HttpStatus getStatus() { return status; }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.moa.moa_server.domain.feedback.handler;

import com.moa.moa_server.domain.global.exception.BaseErrorCode;
import com.moa.moa_server.domain.global.exception.BaseException;

public class FeedbackException extends BaseException {
public FeedbackException(BaseErrorCode errorCode) { super(errorCode); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.moa.moa_server.domain.feedback.repository;

import com.moa.moa_server.domain.feedback.entity.Feedback;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FeedbackRepository extends JpaRepository<Feedback, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.moa.moa_server.domain.feedback.service;

import com.moa.moa_server.domain.feedback.dto.request.FeedbackCreateRequest;
import com.moa.moa_server.domain.feedback.entity.Feedback;
import com.moa.moa_server.domain.feedback.repository.FeedbackRepository;
import com.moa.moa_server.domain.feedback.util.FeedbackValidator;
import com.moa.moa_server.domain.user.entity.User;
import com.moa.moa_server.domain.user.handler.UserErrorCode;
import com.moa.moa_server.domain.user.handler.UserException;
import com.moa.moa_server.domain.user.repository.UserRepository;
import com.moa.moa_server.domain.user.util.AuthUserValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class FeedbackService {

private final FeedbackRepository feedbackRepository;
private final UserRepository userRepository;

@Transactional
public void createFeedback(Long userId, FeedbackCreateRequest request) {
// 유저 조회 및 검증
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
AuthUserValidator.validateActive(user);

//
FeedbackValidator.validateContent(request.content());

Feedback feedback = Feedback.create(user, request.content());

feedbackRepository.save(feedback);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moa.moa_server.domain.feedback.util;

import com.moa.moa_server.domain.feedback.handler.FeedbackErrorCode;
import com.moa.moa_server.domain.feedback.handler.FeedbackException;

public class FeedbackValidator {

public static void validateContent(String content){
if (content == null || content.isBlank() || content.length() > 500 || content.length() < 2) {
throw new FeedbackException(FeedbackErrorCode.INVALID_INPUT);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public record VoteDetailResponse(
String content,
String imageUrl,
LocalDateTime createdAt,
LocalDateTime closedAt
LocalDateTime closedAt,
int adminVote
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static ActiveVoteItem from(Vote vote) {
vote.getImageUrl(),
vote.getCreatedAt(),
vote.getClosedAt(),
vote.isAdminVote() ? 1 : 0,
vote.isAnonymous() ? 0 : (vote.isAdminVote() ? 1 : 0),
vote.getVoteType().name()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ public record MyVoteItem(
) {

public static MyVoteItem from(Vote vote, List<VoteOptionResultWithId> results) {
String status;
if (vote.getVoteStatus() == Vote.VoteStatus.REJECTED || vote.getVoteStatus() == Vote.VoteStatus.PENDING) {
status = vote.getVoteStatus().name();
} else {
status = vote.getClosedAt().isAfter(LocalDateTime.now()) ? "OPEN" : "CLOSED";
}

return new MyVoteItem(
vote.getId(),
vote.getGroup().getId(),
vote.getContent(),
vote.getVoteStatus().name(),
status,
vote.getCreatedAt(),
vote.getClosedAt(),
results
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/moa/moa_server/domain/vote/entity/Vote.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ public enum VoteType {
}

public static Vote createUserVote(User user, Group group, String content, String imageUrl,
LocalDateTime closedAt, boolean anonymous, boolean adminVote) {
LocalDateTime closedAt, boolean anonymous, VoteStatus status, boolean adminVote) {
return Vote.builder()
.user(user)
.group(group)
.content(content)
.imageUrl(imageUrl)
.closedAt(closedAt)
.anonymous(anonymous)
.voteStatus(VoteStatus.PENDING)
.voteStatus(status)
.adminVote(adminVote)
.voteType(VoteType.USER)
.lastAnonymousNumber(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public List<Vote> findSubmittedVotes(User user, List<Group> groups, @Nullable Cl

BooleanBuilder builder = new BooleanBuilder()
.and(voteResponse.user.eq(user))
.and(voteResponse.optionNumber.ne(0)) // 기권 제외
.and(vote.group.in(groups));

if (cursor != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@
import com.moa.moa_server.domain.vote.util.VoteValidator;
import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
Expand All @@ -45,6 +50,9 @@
@RequiredArgsConstructor
public class VoteService {

@Value("${spring.profiles.active:}")
private String activeProfile;

private static final int DEFAULT_PAGE_SIZE = 10;
private static final int DEFAULT_UNAUTHENTICATED_PAGE_SIZE = 3;

Expand Down Expand Up @@ -82,22 +90,32 @@ public Long createVote(Long userId, VoteCreateRequest request) {
VoteValidator.validateContent(request.content());
VoteValidator.validateImageUrl(request.imageUrl());
String imageUrl = request.imageUrl().isBlank() ? null : request.imageUrl().trim();
VoteValidator.validateClosedAt(request.closedAt());

// 투표 종료 시간 변환
ZonedDateTime koreaTime = request.closedAt().atZone(ZoneId.of("Asia/Seoul"));
LocalDateTime utcTime = koreaTime.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
VoteValidator.validateClosedAt(utcTime);

// VoteStatus 결정
Vote.VoteStatus status = "local".equals(activeProfile) ? Vote.VoteStatus.OPEN : Vote.VoteStatus.PENDING;

// Vote 생성 및 저장
Vote vote = Vote.createUserVote(
user,
group,
request.content(),
imageUrl,
request.closedAt(),
utcTime,
request.anonymous(),
status,
adminVote
);
voteRepository.save(vote);

// AI 서버로 검열 요청
voteModerationService.requestModeration(vote.getId(), vote.getContent());
// AI 서버로 검열 요청 (로컬 환경 제외)
if (!"local".equals(activeProfile)) {
voteModerationService.requestModeration(vote.getId(), vote.getContent());
}

return vote.getId();
}
Expand Down Expand Up @@ -170,7 +188,8 @@ public VoteDetailResponse getVoteDetail(Long userId, Long voteId) {
vote.getContent(),
vote.getImageUrl(),
vote.getCreatedAt(),
vote.getClosedAt()
vote.getClosedAt(),
vote.isAnonymous() ? 0 : (vote.isAdminVote() ? 1 : 0)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.moa.moa_server.domain.vote.handler.VoteException;

import java.time.LocalDateTime;
import java.time.ZoneOffset;

public class VoteValidator {

Expand All @@ -23,7 +24,7 @@ public static void validateImageUrl(String imageUrl) {
}

public static void validateClosedAt(LocalDateTime closedAt) {
if (closedAt == null || !closedAt.isAfter(LocalDateTime.now())) {
if (closedAt == null || !closedAt.isAfter(LocalDateTime.now(ZoneOffset.UTC))) {
throw new VoteException(VoteErrorCode.INVALID_TIME); }
}
}