Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2ba46e8
설정 파일 추가
lsryl13578 Sep 6, 2025
9ca4782
Ignore application.properties
lsryl13578 Sep 6, 2025
2185d31
설정 파일 수정
lsryl13578 Sep 6, 2025
453975d
Ignore application.properties
lsryl13578 Sep 6, 2025
788e58a
Update .gitignore
lsryl13578 Sep 6, 2025
ec4d5d1
Update application-example.properties
lsryl13578 Sep 6, 2025
58d60ca
feat: MultipartFile 설정 추가
lsryl13578 Sep 23, 2025
308390f
feat: 카카오 소셜 로그인 기능 추가 / build.gradle 버전 수정 및 의존성 추가
lsryl13578 Oct 31, 2025
e33312d
feat: JWT 설정, 카카오 OAuth 설정 관련 내용 추가
lsryl13578 Oct 31, 2025
3108c05
feat: JWT 설정 / 카카오 OAuth 설정 관련 내용 추가
lsryl13578 Oct 31, 2025
169bb6e
feat: JWT 설정 / 카카오 OAuth 관련 설정 추가
lsryl13578 Oct 31, 2025
6c3043a
refactor : 파일 및 gradle 정리
audwns03 Nov 1, 2025
88f7b21
refactor : 회원가입시 정보 한 번에 저장
audwns03 Nov 1, 2025
7f85470
refactor : 마이페이지 리팩토링
audwns03 Nov 2, 2025
55c7422
feat : 성정체성 추가
audwns03 Nov 3, 2025
03caae4
feat : 지역 카테고리 추가
audwns03 Nov 4, 2025
14d39ac
feat : 자기소개 카테고리 추가
audwns03 Nov 5, 2025
cedbf24
feat : 사진 저장 기능 추가
audwns03 Nov 7, 2025
347cecb
feat : 사주궁합 api 연결 코드 추가
audwns03 Nov 8, 2025
e2e6cfc
feat : 매칭하기 기능 추가
audwns03 Nov 8, 2025
ce06862
feat : spring ai 기능 추가
audwns03 Nov 8, 2025
d9efac3
feat : 오늘의 운세 기능 추가
audwns03 Nov 8, 2025
65100b9
refactor : dto 파일 정리
audwns03 Nov 8, 2025
845a074
feat : 대화 주제 추천 + 데이트코스 추천 기능 추가
audwns03 Nov 9, 2025
38b23de
refactor : 카카오 로그인 관련 코드 리팩토링
audwns03 Nov 15, 2025
3aabcd6
feat : 스웨거 설정 추가
audwns03 Nov 15, 2025
5fdedd8
fix : 카카오 로그인 버그 수정
audwns03 Nov 15, 2025
371fb71
feat : 스웨거 애노테이션 추가
audwns03 Nov 15, 2025
1673169
feat : 실시간 채팅 구현
audwns03 Nov 16, 2025
ad18925
feat : 오늘의 운세 캐시 기능
audwns03 Nov 21, 2025
e37a0c8
feat : 궁합점수 채팅방 내 조회
audwns03 Nov 23, 2025
3ab11f4
AuthController 앱기반 수정 (#4)
audwns03 Nov 28, 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
27 changes: 22 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
### Project Files ###
HELP.md

### Gradle ###
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
Expand Down Expand Up @@ -28,12 +31,26 @@ out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
nbbuild/
dist/
nbdist/
.nb-gradle/

### VS Code ###
.vscode/

*.yml
### OS Files ###
.DS_Store
Thumbs.db

### Logs ###
*.log
logs/

### Environment & Config Files ###
.env
application.properties
application.yml

### Others ###
*.class
28 changes: 27 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.5'
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
}

Expand All @@ -22,20 +22,46 @@ configurations {

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
maven {
name = 'Central Portal Snapshots'
url = 'https://central.sonatype.com/repository/maven-snapshots/'
}
}

ext {
set('springAiVersion', "1.0.1")
}

dependencies {
//공통
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//스웨거
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'

//상렬이거
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

//내거
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT")
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'

}

dependencyManagement {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/project/backend/BackendApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class BackendApplication {

public static void main(String[] args) {
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/project/backend/chat/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package project.backend.chat;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import project.backend.chat.dto.SendChatMessageRequest;

import java.security.Principal;

@Slf4j
@Controller
@RequiredArgsConstructor
public class ChatController {

private final ChatMessageService chatMessageService;

@MessageMapping("/chat/message")
public void handleMessage(SendChatMessageRequest messageRequest, Principal principal) {
// StompAuthChannelInterceptor가 'User.id' (String)를 넣어줌
String senderUserIdStr = principal.getName();
Long senderUserId = Long.parseLong(senderUserIdStr);

log.info("Message received from User.id {}: roomId={}, content={}",
senderUserId, messageRequest.getRoomId(), messageRequest.getContent());

chatMessageService.sendMessage(senderUserId, messageRequest);
}
}
97 changes: 97 additions & 0 deletions src/main/java/project/backend/chat/ChatMessageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package project.backend.chat;

import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import project.backend.chat.dto.ChatMessageDTO;
import project.backend.chat.dto.SendChatMessageRequest;
import project.backend.chat.entity.ChatMessage;
import project.backend.chat.entity.ChatRoom;
import project.backend.chat.repository.ChatMessageRepository;
import project.backend.chat.repository.ChatRoomRepository;
import project.backend.user.UserRepository; // KakaoUserRepository 제거
import project.backend.user.entity.User;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatMessageService {

private final ChatMessageRepository chatMessageRepository;
private final ChatRoomRepository chatRoomRepository;
private final UserRepository userRepository; // User 리포지토리 사용
private final SimpMessageSendingOperations messagingTemplate;

/**
* 메시지 전송 및 저장
* (이제 KakaoUser 관련 로직이 완전히 제거되었습니다)
*/
@Transactional
public void sendMessage(Long senderUserId, SendChatMessageRequest request) {

// 1. 발신자(Sender) 조회 (User.id로)
User senderUser = userRepository.findById(senderUserId)
.orElseThrow(() -> new EntityNotFoundException("Sender User not found: " + senderUserId));

// 2. 채팅방 조회
ChatRoom room = chatRoomRepository.findById(request.getRoomId())
.orElseThrow(() -> new EntityNotFoundException("ChatRoom not found: " + request.getRoomId()));

// 3. 수신자(Receiver) 조회
User receiverUser = room.getOtherParticipant(senderUser);
if (receiverUser == null) {
// 이 경우는 채팅방에 참여자가 1명이거나 잘못된 경우
log.error("Receiver not found in room: {}", request.getRoomId());
throw new EntityNotFoundException("Receiver not found in room");
}

// 4. 메시지 엔티티 생성 및 저장
ChatMessage message = new ChatMessage(room, senderUser, request.getContent());
chatMessageRepository.save(message);

// 5. 채팅방 마지막 메시지 업데이트 (목록 정렬용)
room.setLastMessage(message.getContent(), message.getTimestamp());
// (트랜잭션 종료 시 자동 저장됨)

// 6. DTO 변환
ChatMessageDTO messageDTO = ChatMessageDTO.fromEntity(message);

// 7. WebSocket으로 메시지 전송
// StompAuthChannelInterceptor에서 User.id를 String으로 Principal에 저장했으므로,
// .convertAndSendToUser의 첫 번째 인자(user)는 User.id의 String 값이어야 합니다.

// 수신자에게 전송
messagingTemplate.convertAndSendToUser(
String.valueOf(receiverUser.getId()), // 수신자의 User.id (String)
"/queue/chat", // 구독 주소
messageDTO // 전송할 메시지
);

// 발신자에게도 전송 (본인 화면 업데이트용)
messagingTemplate.convertAndSendToUser(
String.valueOf(senderUser.getId()), // 발신자의 User.id (String)
"/queue/chat",
messageDTO
);

log.info("Message sent from User {} to User {}", senderUser.getId(), receiverUser.getId());
}

/**
* 특정 채팅방의 메시지 내역 조회
*/
@Transactional(readOnly = true)
public List<ChatMessageDTO> getChatMessages(Long roomId) {
// TODO: (선택) roomId에 현재 로그인한 유저가 포함되어 있는지 확인하는 인가 로직 추가
return chatMessageRepository.findByChatRoomIdOrderByTimestampAsc(roomId)
.stream()
.map(ChatMessageDTO::fromEntity)
.collect(Collectors.toList());
}
}
103 changes: 103 additions & 0 deletions src/main/java/project/backend/chat/ChatRoomController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package project.backend.chat;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import project.backend.chat.dto.ChatMessageDTO;
import project.backend.chat.dto.ChatRoomDTO;
import project.backend.chat.dto.CreateChatRoomRequest;
import project.backend.kakaoLogin.KakaoUser;
import project.backend.pythonapi.dto.SajuResponse;

import java.util.List;

@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
@Tag(name = "채팅 API (REST)", description = "채팅방 생성, 목록 조회, 메시지 내역 조회")
@SecurityRequirement(name = "bearerAuth")
public class ChatRoomController {

private final ChatRoomService chatRoomService;
private final ChatMessageService chatMessageService;

@Operation(summary = "1:1 채팅방 생성 또는 조회")
@PostMapping("/room")
public ResponseEntity<ChatRoomDTO> createOrGetRoom(
@AuthenticationPrincipal KakaoUser kakaoUser,
@RequestBody CreateChatRoomRequest request) {

if (kakaoUser.getUser() == null) {
return ResponseEntity.status(403).build(); // 회원가입 미완료
}
Long currentUserId = kakaoUser.getUser().getId();

ChatRoomDTO room = chatRoomService.createOrGetRoom(currentUserId, request.getMatchedUserId());
return ResponseEntity.ok(room);
}

@Operation(summary = "내 채팅방 목록 조회")
@GetMapping("/rooms")
public ResponseEntity<List<ChatRoomDTO>> getMyChatRooms(@AuthenticationPrincipal KakaoUser kakaoUser) {

Long currentUserId = kakaoUser.getUser().getId();
List<ChatRoomDTO> rooms = chatRoomService.getUserChatRooms(currentUserId);
return ResponseEntity.ok(rooms);
}

@Operation(summary = "특정 채팅방 메시지 내역 조회")
@GetMapping("/room/{roomId}/messages")
public ResponseEntity<List<ChatMessageDTO>> getChatMessages(
@PathVariable Long roomId,
@AuthenticationPrincipal KakaoUser kakaoUser) {

// TODO: (선택) kakaoUser.getUser().getId()가 이 roomId에 접근 권한이 있는지 확인

List<ChatMessageDTO> messages = chatMessageService.getChatMessages(roomId);
return ResponseEntity.ok(messages);
}

@Operation(summary = "채팅방 나가기 (채팅방 및 대화 내역 삭제)",
description = "채팅방을 나갑니다. 1:1 채팅이므로 방 자체가 삭제되며, 상대방에게도 목록에서 사라집니다.")
@DeleteMapping("/room/{roomId}")
public ResponseEntity<Void> leaveChatRoom(
@AuthenticationPrincipal KakaoUser kakaoUser,
@PathVariable Long roomId) {

if (kakaoUser.getUser() == null) {
return ResponseEntity.status(403).build(); // Forbidden
}
Long currentUserId = kakaoUser.getUser().getId();

try {
chatRoomService.deleteChatRoom(currentUserId, roomId);
return ResponseEntity.ok().build(); // 성공 (200 OK)
} catch (EntityNotFoundException e) {
return ResponseEntity.notFound().build(); // 방이 없음 (404 Not Found)
} catch (AccessDeniedException e) {
return ResponseEntity.status(403).build(); // 권한 없음 (403 Forbidden)
}
}

@Operation(summary = "채팅방 궁합 점수 조회", description = "채팅방 ID를 통해 저장된 두 사람의 사주 궁합 결과를 조회")
@GetMapping("/room/{roomId}/saju")
public ResponseEntity<SajuResponse> getSajuInfo(
@PathVariable Long roomId,
@AuthenticationPrincipal KakaoUser kakaoUser) {

if (kakaoUser.getUser() == null) {
return ResponseEntity.status(403).build();
}
Long currentUserId = kakaoUser.getUser().getId();

SajuResponse response = chatRoomService.getSajuInfoInRoom(roomId, currentUserId);

return ResponseEntity.ok(response);
}
}
Loading
Loading