Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e49304e
🗃️ feat: 투표 시스템 데이터베이스 스키마 및 엔티티 구현
goalSetter09 Sep 7, 2025
c40e6db
🗃️ feat: 투표 시스템 Repository 레이어 구현
goalSetter09 Sep 7, 2025
831a508
✨ feat: implement VotingService for similarity vote participation
goalSetter09 Sep 7, 2025
d55f271
♻️ refactor: Object[] 제거하고 타입 안전한 Repository 메서드로 분리
goalSetter09 Sep 7, 2025
a1949b4
refactor: 투표 기능 응답 단순화
goalSetter09 Sep 7, 2025
aab9ada
feat: 투표 결과 조회 API 구현
goalSetter09 Sep 7, 2025
e29ada3
refactor: ai 디렉터리로부터 ticket 분리 및 AiRequestTicket -> Ticket으로 이름 변경
yooooonshine Sep 10, 2025
83cb62f
✨ feat: implement vote participation API endpoint
goalSetter09 Sep 12, 2025
0d4237c
✨ feat: 투표 시스템 기본 인프라 구축
goalSetter09 Sep 12, 2025
b3191a4
✨ feat: 파생 게시물 생성과 투표 시스템 연동
goalSetter09 Sep 12, 2025
8308a27
🗃️ feat: 투표-게시물 연결을 위한 SimilarityVoteEntity에 derivedPostId 필드 추가
goalSetter09 Sep 12, 2025
701cb9f
🔗 feat: AiDerivedPostService에서 투표 생성 시 파생 게시물 ID 연결
goalSetter09 Sep 12, 2025
329b590
✨ feat: VotingService에 투표 완료 시 파생 게시물 상태 자동 업데이트 기능 구현
goalSetter09 Sep 12, 2025
6e4cb1c
🩹 fix: AiDerivedPostController에서 originalImageId 매개변수 누락 오류 해결
goalSetter09 Sep 12, 2025
f5a6c11
🔧 fix: VoteProperties 및 관련 클래스에서 가중치 타입을 Long에서 int로 변경
goalSetter09 Sep 12, 2025
7fdde55
feat: AI 채팅 기능 구현
yooooonshine Sep 12, 2025
a80e9a9
refactor: 코드 변경에 따른 테스트 코드 변경
yooooonshine Sep 12, 2025
fd2be5d
feat: AI 관련 기능 구현
yooooonshine Sep 12, 2025
c47060a
✨ feat: 랜덤 투표 조회 API 구현
goalSetter09 Sep 14, 2025
a45bd6f
🔧 refactor: Entity 이름 수정 및 코드 스타일 개선
goalSetter09 Sep 14, 2025
d4ffe00
refactor: 불필요한 매서드 삭제
goalSetter09 Sep 14, 2025
6c87535
refactor: jpql 오류 해결
goalSetter09 Sep 14, 2025
2d85758
✨ feat: AiDerivedPostService 및 컨트롤러에서 originalImageId 매개변수 추가
goalSetter09 Sep 14, 2025
46006ab
refactor: S3 로직 AWS sdk2로 업그레이드
yooooonshine Sep 15, 2025
80ce579
feat: AiController의 ErrorCode 업데이트
yooooonshine Sep 15, 2025
9469674
refactor: Spring AI 제외, webClient로 변경
yooooonshine Sep 15, 2025
ac0f27b
feat: s3 변경에 따른 테스트코드 변경
yooooonshine Sep 15, 2025
9caded1
refactor: CallbackImageSaveUrlRequest에서 imageUsagePurpose 제거
yooooonshine Sep 15, 2025
d887a4f
feat: AI 관련 기능 Validation 추가
yooooonshine Sep 15, 2025
2312424
fix: Prefix 제거로 인한 오류 수정
yooooonshine Sep 15, 2025
52ed38d
Merge pull request #169 from Modic-2025/feature/167-implement-ai-chat
yooooonshine Sep 15, 2025
4a0d8b8
🗃️ feat: 투표 시스템 데이터베이스 스키마 및 엔티티 구현
goalSetter09 Sep 7, 2025
4ed1000
🗃️ feat: 투표 시스템 Repository 레이어 구현
goalSetter09 Sep 7, 2025
dbcd1a7
fix: merge conflict 해결
goalSetter09 Sep 21, 2025
f5819f8
♻️ refactor: Object[] 제거하고 타입 안전한 Repository 메서드로 분리
goalSetter09 Sep 7, 2025
64f2e32
refactor: 투표 기능 응답 단순화
goalSetter09 Sep 7, 2025
4deb807
feat: 투표 결과 조회 API 구현
goalSetter09 Sep 7, 2025
5802210
✨ feat: implement vote participation API endpoint
goalSetter09 Sep 12, 2025
c05465f
✨ feat: 투표 시스템 기본 인프라 구축
goalSetter09 Sep 12, 2025
73cc987
✨ feat: 파생 게시물 생성과 투표 시스템 연동
goalSetter09 Sep 12, 2025
96009c0
🗃️ feat: 투표-게시물 연결을 위한 SimilarityVoteEntity에 derivedPostId 필드 추가
goalSetter09 Sep 12, 2025
1b64f9d
🔗 feat: AiDerivedPostService에서 투표 생성 시 파생 게시물 ID 연결
goalSetter09 Sep 12, 2025
6843494
✨ feat: VotingService에 투표 완료 시 파생 게시물 상태 자동 업데이트 기능 구현
goalSetter09 Sep 12, 2025
66848d5
🩹 fix: AiDerivedPostController에서 originalImageId 매개변수 누락 오류 해결
goalSetter09 Sep 12, 2025
b1bf987
🔧 fix: VoteProperties 및 관련 클래스에서 가중치 타입을 Long에서 int로 변경
goalSetter09 Sep 12, 2025
b4774a9
✨ feat: 랜덤 투표 조회 API 구현
goalSetter09 Sep 14, 2025
48d2f60
🔧 refactor: Entity 이름 수정 및 코드 스타일 개선
goalSetter09 Sep 14, 2025
fdf2779
refactor: 불필요한 매서드 삭제
goalSetter09 Sep 14, 2025
9548afc
refactor: jpql 오류 해결
goalSetter09 Sep 14, 2025
d342ce0
fix: merge conflict 해결
goalSetter09 Sep 14, 2025
6ffff57
fix: AiChatImageEntity request_id 인덱스 제거
goalSetter09 Sep 21, 2025
474ab1a
🐛 fix: AiRequest, CreatedAiImageEntity → AiChatImageEntity 엔티티 변경으로 인…
goalSetter09 Sep 21, 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
50 changes: 36 additions & 14 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ configurations {

repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
maven { url "https://repo.spring.io/snapshot" }
}
Comment on lines 22 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Milestone/Snapshot 저장소 전역 추가는 공급망 리스크

릴리즈가 아닌 아티팩트를 전역으로 받아들이면 예기치 않은 상향/호환 깨짐 위험이 있습니다. 필요한 모듈 범위로 국소화하거나 제거를 권장합니다.

  • 특정 의존성에 한정해 maven { url ... }를 개별 서브프로젝트나 필요 Task에만 선언
  • 해제 시 캐시를 비워 재현성 확인
🤖 Prompt for AI Agents
In build.gradle around lines 22-26, the global addition of Spring
milestone/snapshot repositories increases supply-chain risk; remove these global
maven entries and instead declare them only where needed (either in the specific
subproject build.gradle or a specific task), or restrict their scope using a
content filter (e.g., configure the repository with content { includeModule(...)
/ includeGroup(...) } in the subproject or in dependencyResolutionManagement) so
only the required artifact(s) are fetched from milestone/snapshot; after making
the change, clear Gradle caches (e.g., ./gradlew --refresh-dependencies or
delete ~/.gradle/caches) and verify builds for reproducibility.


// 의존성 버전 강제 고정
configurations.all {
resolutionStrategy {
// AWS SDK v2 및 다른 라이브러리 버전을 강제 고정하여 잠재적 충돌 방지
force 'software.amazon.awssdk:cloudfront:2.26.14',
'software.amazon.awssdk:s3:2.26.14',
'software.amazon.awssdk:url-signer:2.26.14'
force 'io.jsonwebtoken:jjwt-api:0.12.5'
force "io.swagger.core.v3:swagger-annotations-jakarta:2.2.36"
force "io.swagger.core.v3:swagger-core-jakarta:2.2.36"
force "io.swagger.core.v3:swagger-models-jakarta:2.2.36"
}
}

dependencies {
Expand All @@ -33,40 +49,46 @@ dependencies {
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
implementation 'com.google.guava:guava:30.1.1-jre'
implementation 'com.google.guava:guava:33.2.1-jre'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'com.amazonaws:aws-java-sdk-cloudfront:1.12.772'
// AWS SDK v2 (버전은 resolutionStrategy로 관리)
implementation 'software.amazon.awssdk:s3'
implementation 'software.amazon.awssdk:cloudfront'

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6'
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.12") {
exclude group: "io.swagger.core.v3", module: "swagger-annotations"
}

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0'
runtimeOnly 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2'
runtimeOnly 'org.glassfish.jaxb:jaxb-runtime:4.0.5'

implementation 'org.springframework.boot:spring-boot-starter-mail'

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Amqp
implementation 'org.springframework.boot:spring-boot-starter-amqp:3.4.2'
// Amqp (버전은 Spring Boot가 관리)
implementation 'org.springframework.boot:spring-boot-starter-amqp'
// Redisson(Redis Client)
implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'
// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.5.0'
implementation 'org.redisson:redisson-spring-boot-starter:3.32.0'
// OAuth (버전은 Spring Boot가 관리)
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// metric
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-cloudwatch2")
implementation("io.micrometer:micrometer-registry-cloudwatch2:1.13.4")

// webClient용 라이브러리, WebFlux 사용x
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
Comment on lines +88 to 90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

WebFlux 스타터 동시 사용 주의

MVC+WebFlux 스타터 동시 포함은 서버 이중 탑재(톰캣+Netty)와 클래스패스 부피 증가를 야기합니다. WebClient만 필요하면 spring-boot-starter-webflux 사용은 가능하지만, 런타임 충돌 여부를 확인하세요.

다음으로 내장 서버 충돌 여부를 점검해주세요.


🏁 Script executed:

#!/bin/bash
rg -nPI 'Netty|Reactor Netty|undertow' -g 'build/**/reports/*' -S || true

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Searching repository for WebFlux / Web starters and embedded servers =="
rg -nPI --hidden -S --glob '!**/build/**' --glob '!**/target/**' --glob '!**/.git/**' -C3 'spring-boot-starter-webflux|spring-boot-starter-web|spring-boot-starter-tomcat|spring-boot-starter-undertow|spring-boot-starter-jetty|spring-boot-starter-reactor-netty|io\.projectreactor\.netty|reactor-netty|reactor\.netty|netty|undertow|jetty' || true

echo
echo "== Searching for WebClient usage =="
rg -nPI --hidden -S --glob '!**/build/**' --glob '!**/target/**' --glob '!**/.git/**' -C2 'org\.springframework\.web\.reactive\.function\.client\.WebClient|WebClient' || true

echo
echo "== Searching for spring.main.web-application-type settings =="
rg -nPI --hidden -S --glob '!**/build/**' --glob '!**/target/**' --glob '!**/.git/**' -C2 'spring\.main\.web-application-type|web-application-type' || true

echo
echo "== List of build files found (build.gradle*, build.gradle.kts, pom.xml, settings.gradle*) =="
find . -maxdepth 6 -type f \( -name 'build.gradle*' -o -name 'build.gradle.kts' -o -name 'pom.xml' -o -name 'settings.gradle*' \) -print | sed 's|^\./||' || true

echo
echo "== Show dependency lines from found build files =="
for f in $(find . -maxdepth 6 -type f \( -name 'build.gradle*' -o -name 'build.gradle.kts' -o -name 'pom.xml' \)); do
  echo "---- $f ----"
  rg -nPI --hidden -S -n 'spring-boot-starter-webflux|spring-boot-starter-web|spring-boot-starter-tomcat|spring-boot-starter-undertow|spring-boot-starter-jetty|spring-boot-starter-reactor-netty|io\.projectreactor\.netty|reactor-netty|reactor\.netty|netty|undertow|jetty' "$f" || true
done

Length of output: 971


WebFlux 스타터 동시 포함 확인 — 즉시 수정 필요

build.gradle에 'org.springframework.boot:spring-boot-starter-web' (44행)과 'org.springframework.boot:spring-boot-starter-webflux' (89행)가 동시에 선언되어 있습니다. 톰캣 + Reactor Netty 이중 내장 서버, 클래스패스 증가 및 포트/런타임 충돌 가능성이 있습니다.

  • 조치(권장): WebClient만 필요하면 spring-boot-starter-webflux를 제거하고 대신 org.springframework:spring-webflux 모듈만 추가하거나, webflux 스타터에서 Reactor Netty(io.projectreactor.netty) 의존성을 exclude 하세요.
  • 검증: 또는 spring.main.web-application-type=servlet으로 강제하거나 ./gradlew dependencies --configuration runtimeClasspath / dependencyInsight로 의존성 트리를 확인하고 애플리케이션 로그에서 Netty/Tomcat 기동 여부를 확인하세요.
🤖 Prompt for AI Agents
In build.gradle around lines 88-90 (and note web starter at line 44), you
currently include both 'org.springframework.boot:spring-boot-starter-web' and
'org.springframework.boot:spring-boot-starter-webflux', which causes embedded
Tomcat + Reactor Netty conflict; fix by removing the webflux starter if you only
need WebClient and instead add the lighter 'org.springframework:spring-webflux'
dependency OR keep the starter but exclude Reactor Netty (exclude group
'io.projectreactor.netty', artifact 'reactor-netty') so only servlet stack runs;
after the change verify with ./gradlew dependencies --configuration
runtimeClasspath or dependencyInsight and confirm the app starts with only
Tomcat (or only Netty) in logs.


tasks.named('test') {
useJUnitPlatform()
}
}
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
6 changes: 2 additions & 4 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package hanium.modic.backend.domain.ai.service;
package hanium.modic.backend.common.amqp.service;

import static hanium.modic.backend.common.amqp.config.RabbitMqConfig.*;

Expand All @@ -7,7 +7,7 @@
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

import hanium.modic.backend.domain.ai.dto.AiImageRequestMessageDto;
import hanium.modic.backend.domain.ai.aiServer.dto.AiImageRequestMessageDto;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

Expand All @@ -17,8 +17,7 @@ public class MessageQueueService {

private final RabbitTemplate rabbitTemplate;

public void sendImageGenerationRequest(String requestId, String requestImageUrl, List<String> styleImageUrls) {
AiImageRequestMessageDto message = new AiImageRequestMessageDto(requestId, requestImageUrl, styleImageUrls);
public void sendImageGenerationRequest(Object message) {
rabbitTemplate.convertAndSend(
AI_IMAGE_REQUEST_EXCHANGE,
AI_IMAGE_REQUEST_ROUTING_KEY,
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/hanium/modic/backend/common/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public enum ErrorCode {
// Post
POST_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "P-001", "해당 포스트를 찾을 수 없습니다."),
POST_ROLE_EXCEPTION(HttpStatus.FORBIDDEN, "P-002", "포스트에 대한 권한이 없습니다."),
POST_ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "P-003", "포스트에 대한 접근 권한이 없습니다."),
NOT_AI_DERIVED_POST_EXCEPTION(HttpStatus.BAD_REQUEST, "P-004", "AI 파생 포스트가 아닙니다."),

// Post Like
CANNOT_LIKE_OWN_POST_EXCEPTION(HttpStatus.BAD_REQUEST, "PL-001", "자신의 게시글에는 하트를 할 수 없습니다."),
Expand Down Expand Up @@ -83,11 +85,25 @@ public enum ErrorCode {
AI_IMAGE_PERMISSION_ALREADY_EXISTS_EXCEPTION(HttpStatus.CONFLICT, "AI-009", "이미 AI 이미지 생성권을 구매했습니다."),
AI_IMAGE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "AI-010", "생성된 AI 이미지를 찾을 수 없습니다."),
AI_IMAGE_ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "AI-011", "AI 이미지에 대한 권한이 없습니다."),
POST_ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "P-003", "포스트에 대한 접근 권한이 없습니다."),
NOT_AI_DERIVED_POST_EXCEPTION(HttpStatus.BAD_REQUEST, "P-004", "AI 파생 포스트가 아닙니다."),
AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AI-012", "AI 서버와의 통신 중 에러가 발생하였습니다."),

// AI Chat
AI_CHAT_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "AC-001", "AI 채팅방을 찾을 수 없습니다."),

// Vote
VOTE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "V-001", "해당 투표를 찾을 수 없습니다."),
DUPLICATE_VOTE_EXCEPTION(HttpStatus.BAD_REQUEST, "V-002", "이미 투표에 참여했습니다."),
VOTE_ALREADY_COMPLETED_EXCEPTION(HttpStatus.BAD_REQUEST, "V-003", "이미 완료된 투표입니다."),
VOTE_PERMISSION_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "V-004", "투표 권한이 없습니다."),
VOTE_NOT_IN_PROGRESS_EXCEPTION(HttpStatus.BAD_REQUEST, "V-005", "진행 중인 투표가 아닙니다."),
VOTE_DAILY_LIMIT_EXCEEDED_EXCEPTION(HttpStatus.BAD_REQUEST, "V-006", "일일 투표 한도를 초과했습니다."),
VOTE_SUMMARY_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "V-007", "투표 집계 정보를 찾을 수 없습니다."),
VOTE_UPDATE_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "V-008", "투표 집계 업데이트에 실패했습니다."),
NO_AVAILABLE_VOTES_EXCEPTION(HttpStatus.NOT_FOUND, "V-009", "참여 가능한 투표가 없습니다."),
;

private final HttpStatus status;
private final String code;
private final String message;

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import hanium.modic.backend.common.property.property.AiProperties;
import hanium.modic.backend.common.property.property.CloudFrontProperties;
import hanium.modic.backend.common.property.property.CorsProperties;
import hanium.modic.backend.common.property.property.EmailProperty;
Expand All @@ -12,6 +13,7 @@
import hanium.modic.backend.common.property.property.SecurityProperties;
import hanium.modic.backend.common.property.property.SwaggerProperties;
import hanium.modic.backend.common.property.property.TokenProperty;
import hanium.modic.backend.common.property.property.VoteProperties;

// 전역적으로 사용되는 상수
@Configuration
Expand All @@ -24,7 +26,9 @@
SecurityProperties.class,
CorsProperties.class,
RabbitMqProperties.class,
CloudFrontProperties.class
CloudFrontProperties.class,
AiProperties.class,
VoteProperties.class
})
public class PropertyConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package hanium.modic.backend.common.property.property;

import org.springframework.boot.context.properties.ConfigurationProperties;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@ConfigurationProperties(prefix = "ai")
public class AiProperties {

private OpenAi openai = new OpenAi();

@Getter
@Setter
public static class OpenAi {
private String apiKey;
private String model;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hanium.modic.backend.common.property.property;

import org.springframework.boot.context.properties.ConfigurationProperties;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@ConfigurationProperties(prefix = "vote.similarity")
public class VoteProperties {

private int minTotalWeight;
private int aiVoteWeight;
private int humanVoteWeight;
private Boolean enableAiAssessment;
private int maxVotesPerUserPerDay;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class LockManager {
private final String USER_TICKET_PREFIX = "lock:user:ticket:";
private final String POST_LIKE_PREFIX = "lock:postlike:";
private final String AI_PERMISSION_PREFIX = "lock:ai:perm:";
private final String VOTE_SUMMARY_PREFIX = "lock:vote:summary:";

public void userLock(long userId, Runnable block) throws LockException {
exec.withLock(USER_PREFIX + userId, block);
Expand All @@ -53,4 +54,8 @@ public void aiRequestTicketLock(long userId, Runnable block) throws LockExceptio
public void aiImagePermissionLock(long userId, long postId, Runnable block) throws LockException {
exec.withLock(AI_PERMISSION_PREFIX + userId + ":" + postId, block);
}

public void voteSummaryLock(long voteId, Runnable block) throws LockException {
exec.withLock(VOTE_SUMMARY_PREFIX + voteId, block);
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,28 @@
package hanium.modic.backend.domain.ai.service;

import static hanium.modic.backend.common.error.ErrorCode.*;
package hanium.modic.backend.common.sse.service;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import hanium.modic.backend.common.error.ErrorCode;
import hanium.modic.backend.common.error.exception.AppException;
import hanium.modic.backend.domain.ai.domain.AiRequestEntity;
import hanium.modic.backend.domain.ai.repository.AiRequestRepository;
import lombok.RequiredArgsConstructor;

// SSE(서버-발행 이벤트) 연결 및 메시지 전송을 관리하는 서비스
@Service
@RequiredArgsConstructor
public class AiRequestEmitterService {

private final AiRequestRepository aiRequestRepository;
public class EmitterService {

/**
* SseEmitter 관리 맵
* 해당 맵은 단일 서버를 가정한 map이며, 멀티 서버 환경에서는 Redis등을 활용한 별도의 관리가 필요
*/
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();

// requestId와 SseEmitter를 ,emitter관리 맵에 저장
public void addEmitter(Long userId, String requestId, SseEmitter emitter) {
AiRequestEntity aiRequest = aiRequestRepository.findByRequestId(requestId)
.orElseThrow(() -> new AppException(ErrorCode.AI_REQUEST_NOT_FOUND));

if (!Objects.equals(aiRequest.getUserId(), userId)) {
throw new AppException(USER_ROLE_EXCEPTION);
}

// requestId와 SseEmitter를 ,emitter관리 맵에 저장
public void addEmitter(String requestId, SseEmitter emitter) {
emitters.put(requestId, emitter);
}

Expand All @@ -52,7 +38,6 @@ public void sendToClient(String requestId, Object data) {
try {
emitter.send(SseEmitter.event()
.id(requestId)
.name("image")
.data(data));
emitter.complete();
emitters.remove(requestId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package hanium.modic.backend.domain.ai.aiChat.dto;

import java.time.LocalDateTime;

/**
* 컨텍스트 초기화 응답 DTO
*/
public record ChatContextResetResponse(
Long roomId,
LocalDateTime contextResetAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package hanium.modic.backend.domain.ai.aiChat.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/**
* 채팅 메시지 전송 요청 DTO
*/
@Schema(description = "채팅 메시지 전송 요청")
public record ChatMessageRequest(
@Schema(description = "채팅 메시지 내용", example = "안녕하세요! 이 그림을 더 밝게 만들어주세요.", required = true)
@NotBlank(message = "메시지 내용은 필수입니다.")
String textContent,

@Schema(description = "첨부할 이미지 ID (선택사항)", example = "123")
Long aiChatImageId // 이미지 첨부 시에만 (optional)
) {
}
Loading