Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions ServerlessFunction/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
implementation 'software.amazon.awssdk:url-connection-client'
implementation 'software.amazon.awssdk:ssm'
implementation 'software.amazon.awssdk:scheduler'
implementation 'software.amazon.awssdk:sqs'

// AWS X-Ray SDK (다운스트림 서비스 추적용)
implementation 'com.amazonaws:aws-xray-recorder-sdk-core:2.15.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.sns.SnsClient;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.ssm.SsmClient;

/**
Expand Down Expand Up @@ -60,7 +61,12 @@ public final class AwsClients {
private static final SsmClient SSM_CLIENT = SsmClient.builder()
.overrideConfiguration(XRAY_CONFIG)
.build();


// SQS
private static final SqsClient SQS_CLIENT = SqsClient.builder()
.overrideConfiguration(XRAY_CONFIG)
.build();

private AwsClients() {
// 인스턴스화 방지
}
Expand Down Expand Up @@ -104,4 +110,8 @@ public static ComprehendClient comprehend() {
public static SsmClient ssm() {
return SSM_CLIENT;
}

public static SqsClient sqs() {
return SQS_CLIENT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ public final class EnvConfig {
private EnvConfig() {
// 유틸리티 클래스 - 인스턴스화 방지
}


/**
* 선택적 환경 변수를 가져옵니다.
* 환경 변수가 설정되지 않은 경우 null을 반환합니다.
*
* @param name 환경 변수 이름
* @return 환경 변수 값 또는 null
*/
public static String get(String name) {
return System.getenv(name);
}

/**
* 필수 환경 변수를 가져옵니다.
* 환경 변수가 설정되지 않았거나 빈 문자열인 경우 IllegalStateException을 발생시킵니다.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.mzc.secondproject.serverless.common.util;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;

Expand All @@ -10,9 +12,25 @@
* JSON 파싱 관련 공통 유틸리티
*/
public class JsonUtil {


private static final Gson GSON = new GsonBuilder().create();

private JsonUtil() {
}

/**
* 객체를 JSON 문자열로 변환
*/
public static String toJson(Object obj) {
return GSON.toJson(obj);
}

/**
* JSON 문자열을 객체로 변환
*/
public static <T> T fromJson(String json, Class<T> clazz) {
return GSON.fromJson(json, clazz);
}

// 응답에서 JSON 부분만 추출
public static String extractJson(String response) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.mzc.secondproject.serverless.domain.badge.repository.BadgeRepository;
import com.mzc.secondproject.serverless.domain.badge.strategy.BadgeConditionStrategy;
import com.mzc.secondproject.serverless.domain.badge.strategy.BadgeConditionStrategyFactory;
import com.mzc.secondproject.serverless.domain.notification.service.NotificationPublisher;
import com.mzc.secondproject.serverless.domain.stats.model.UserStats;
import com.mzc.secondproject.serverless.domain.stats.repository.UserStatsRepository;
import org.slf4j.Logger;
Expand All @@ -24,20 +25,23 @@ public class BadgeService {

private final BadgeRepository badgeRepository;
private final UserStatsRepository userStatsRepository;

private final NotificationPublisher notificationPublisher;

/**
* 기본 생성자 (Lambda에서 사용)
*/
public BadgeService() {
this(new BadgeRepository(), new UserStatsRepository());
this(new BadgeRepository(), new UserStatsRepository(), NotificationPublisher.getInstance());
}

/**
* 의존성 주입 생성자 (테스트 용이성)
*/
public BadgeService(BadgeRepository badgeRepository, UserStatsRepository userStatsRepository) {
public BadgeService(BadgeRepository badgeRepository, UserStatsRepository userStatsRepository,
NotificationPublisher notificationPublisher) {
this.badgeRepository = badgeRepository;
this.userStatsRepository = userStatsRepository;
this.notificationPublisher = notificationPublisher;
}

/**
Expand Down Expand Up @@ -98,6 +102,15 @@ public List<UserBadge> checkAndAwardBadges(String userId, UserStats stats) {
badgeRepository.save(badge);
newBadges.add(badge);
logger.info("Badge awarded: userId={}, badge={}", userId, type.name());

// 알림 발행
notificationPublisher.publishBadgeEarned(
userId,
type.name(),
type.getName(),
type.getDescription(),
badge.getImageUrl()
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ public enum MessageType {

// 방 관련 메시지 타입
ROOM_STATUS_CHANGE("room_status_change", "방 상태 변경"),
HOST_CHANGE("host_change", "방장 변경");
HOST_CHANGE("host_change", "방장 변경"),

// 투표 관련 메시지 타입
POLL_CREATE("poll_create", "투표 생성"),
POLL_VOTE("poll_vote", "투표 참여"),
POLL_END("poll_end", "투표 종료"),

// 유틸리티 메시지 타입
CLEAR_CHAT("clear_chat", "채팅 삭제"),
LEAVE_ROOM("leave_room", "채팅방 나가기");

private final String code;
private final String displayName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.mzc.secondproject.serverless.domain.chatting.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.*;

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

/**
* 채팅방 투표 모델
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamoDbBean
public class Poll {

private String pk; // ROOM#{roomId}
private String sk; // POLL#{pollId}

private String pollId;
private String roomId;
private String question;
private List<String> options;
private Map<String, Integer> votes; // optionIndex -> count
private Map<String, Integer> userVotes; // userId -> optionIndex
private String createdBy;
private String createdAt;
private Boolean isActive;
private Long ttl;

@DynamoDbPartitionKey
@DynamoDbAttribute("PK")
public String getPk() {
return pk;
}

@DynamoDbSortKey
@DynamoDbAttribute("SK")
public String getSk() {
return sk;
}

/**
* 투표 추가
*/
public boolean addVote(String userId, int optionIndex) {
if (optionIndex < 0 || optionIndex >= options.size()) {
return false;
}

// 이미 투표했는지 확인
if (userVotes.containsKey(userId)) {
return false;
}

userVotes.put(userId, optionIndex);
votes.merge(String.valueOf(optionIndex), 1, Integer::sum);
return true;
}

/**
* 사용자가 이미 투표했는지 확인
*/
public boolean hasVoted(String userId) {
return userVotes != null && userVotes.containsKey(userId);
}

/**
* 총 투표 수
*/
public int getTotalVotes() {
if (votes == null) return 0;
return votes.values().stream().mapToInt(Integer::intValue).sum();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.mzc.secondproject.serverless.domain.chatting.repository;

import com.mzc.secondproject.serverless.common.config.AwsClients;
import com.mzc.secondproject.serverless.common.config.EnvConfig;
import com.mzc.secondproject.serverless.domain.chatting.model.Poll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
import software.amazon.awssdk.enhanced.dynamodb.Key;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;

import java.util.Optional;

/**
* Poll Repository
*/
public class PollRepository {

private static final Logger logger = LoggerFactory.getLogger(PollRepository.class);
private static final String TABLE_NAME = EnvConfig.getRequired("CHAT_TABLE_NAME");

private final DynamoDbTable<Poll> table;

public PollRepository() {
this.table = AwsClients.dynamoDbEnhanced().table(TABLE_NAME, TableSchema.fromBean(Poll.class));
}

public PollRepository(DynamoDbTable<Poll> table) {
this.table = table;
}

public void save(Poll poll) {
table.putItem(poll);
logger.debug("Saved poll: {}", poll.getPollId());
}

public Optional<Poll> findById(String roomId, String pollId) {
Key key = Key.builder()
.partitionValue("ROOM#" + roomId)
.sortValue("POLL#" + pollId)
.build();
Poll poll = table.getItem(key);
return Optional.ofNullable(poll);
}

/**
* 방의 활성 투표 조회
*/
public Optional<Poll> findActiveByRoomId(String roomId) {
return table.query(QueryConditional.sortBeginsWith(
Key.builder()
.partitionValue("ROOM#" + roomId)
.sortValue("POLL#")
.build()))
.items()
.stream()
.filter(poll -> Boolean.TRUE.equals(poll.getIsActive()))
.findFirst();
}

public void delete(String roomId, String pollId) {
Key key = Key.builder()
.partitionValue("ROOM#" + roomId)
.sortValue("POLL#" + pollId)
.build();
table.deleteItem(key);
logger.debug("Deleted poll: {}", pollId);
}
}
Loading