diff --git a/.gitignore b/.gitignore index 177801c..7bb8852 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ out/ .vscode/ mydb.db +testdb.mv.db .env github-actions-key github-actions-key.pub \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2c11f97..6b75dec 100644 --- a/build.gradle +++ b/build.gradle @@ -24,18 +24,21 @@ repositories { } dependencies { + // 기본 웹, 유효성 검사, Lombok implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' // 추가 + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // 데이터베이스 (JPA, MySQL) implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'com.mysql:mysql-connector-j' implementation 'org.hibernate.orm:hibernate-community-dialects' + // 보안 (Spring Security) implementation 'org.springframework.boot:spring-boot-starter-security' - // JWT 관련 의존성 추가 + // JWT (토큰) implementation 'io.jsonwebtoken:jjwt-api:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' @@ -43,16 +46,23 @@ dependencies { // test testImplementation 'io.rest-assured:rest-assured:5.5.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured:5.5.0' runtimeOnly 'com.h2database:h2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + implementation 'org.springframework.boot:spring-boot-starter-websocket' // mqtt implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'org.json:json:20240303' + + // 1. .env 파일을 읽기 위한 라이브러리 + implementation 'io.github.cdimascio:java-dotenv:5.2.2' + + // 2. @EnableJpaAuditing을 사용하기 위한 Spring Data JPA + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' } tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/ReceiptPowerServerApplication.java b/src/main/java/com/cagong/receiptpowerserver/ReceiptPowerServerApplication.java index 96ea406..9d4c191 100644 --- a/src/main/java/com/cagong/receiptpowerserver/ReceiptPowerServerApplication.java +++ b/src/main/java/com/cagong/receiptpowerserver/ReceiptPowerServerApplication.java @@ -1,13 +1,20 @@ +// ReceiptPowerServerApplication.java + package com.cagong.receiptpowerserver; +import io.github.cdimascio.dotenv.Dotenv; // import 추가 import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class ReceiptPowerServerApplication { - - public static void main(String[] args) { - SpringApplication.run(ReceiptPowerServerApplication.class, args); - } - -} + public static void main(String[] args) { + Dotenv.configure() + .ignoreIfMissing() // .env 파일이 없어도(예: 배포 환경) 오류 없이 통과 + .systemProperties() // .env의 모든 변수를 System.setProperty()로 자동 등록 + .load(); + SpringApplication.run(ReceiptPowerServerApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/Cafe.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/Cafe.java index a334ecc..d030a8a 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/Cafe.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/Cafe.java @@ -1,6 +1,7 @@ package com.cagong.receiptpowerserver.domain.cafe; import com.cagong.receiptpowerserver.domain.cafe.dto.CafeRequest; +import com.cagong.receiptpowerserver.domain.cafe.dto.CafeUpdateRequest; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -16,6 +17,9 @@ public class Cafe { @Column(name = "cafe_id") private Long id; + // [추가] 카카오 장소 ID를 저장할 필드. 카페가 중복 저장되는 것을 막는다. + @Column(unique = true) + private String name; private String address; @@ -27,15 +31,15 @@ public class Cafe { private String phoneNumber; @Builder - public Cafe(String name, String address, double latitude, double longitude, String phoneNumber) { + public Cafe(String name, double latitude, double longitude, String address, String phoneNumber) { this.name = name; - this.address = address; this.latitude = latitude; - this. longitude = longitude; + this.longitude = longitude; + this.address = address; this.phoneNumber = phoneNumber; } - public void updateFrom(CafeRequest request) { + public void update(CafeUpdateRequest request) { this.name = request.getCafeName(); this.address = request.getAddress(); this.latitude = request.getLatitude(); diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeController.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeController.java index 3671867..bd39006 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeController.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeController.java @@ -2,61 +2,66 @@ import com.cagong.receiptpowerserver.domain.cafe.dto.CafeRequest; import com.cagong.receiptpowerserver.domain.cafe.dto.CafeResponse; +import com.cagong.receiptpowerserver.domain.cafe.dto.CafeUpdateRequest; +import com.cagong.receiptpowerserver.domain.cafe.dto.CafeWithChatRoomsResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.net.URI; import java.util.List; @RestController -@RequiredArgsConstructor @RequestMapping("/cafes") +@RequiredArgsConstructor public class CafeController { + private final CafeService cafeService; + /** + * 1. 카페 생성 (POST /api/cafes) + */ + @PostMapping + public ResponseEntity createCafe(@RequestBody CafeRequest request) { + Long cafeId = cafeService.saveCafe(request); + return ResponseEntity.status(HttpStatus.CREATED).body(cafeId); + } + /** + * 2. 카페 전체 조회 (GET /api/cafes/all) + */ @GetMapping("/all") - public ResponseEntity> getAllCafes(){ - List responses = cafeService.getAllCafes(); - return ResponseEntity.ok().body(responses); + public ResponseEntity> getAllCafes() { + List cafes = cafeService.findAllCafes(); + return ResponseEntity.ok(cafes); } + /** + * 3. 카페 ID로 1건 조회 (GET /api/cafes/{cafeId}) + */ @GetMapping("/{cafeId}") - public ResponseEntity getCafeById(@PathVariable Long cafeId){ - try{ - CafeResponse response = cafeService.getCafeById(cafeId); - return ResponseEntity.ok().body(response); - } catch (IllegalArgumentException e){ - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } + public ResponseEntity getCafeById(@PathVariable Long cafeId) { + CafeResponse cafe = cafeService.findCafeById(cafeId); + return ResponseEntity.ok(cafe); } - @PostMapping - public ResponseEntity createCafe(@RequestBody CafeRequest request){ - Long cafeId = cafeService.createCafe(request); - return ResponseEntity.created(URI.create("/cafes/" + cafeId)).build(); + /** + * 4. 카페 삭제 (DELETE /api/cafes/{cafeId}) + */ + @DeleteMapping("/{cafeId}") + public ResponseEntity deleteCafe(@PathVariable Long cafeId) { + cafeService.deleteCafeById(cafeId); + return ResponseEntity.noContent().build(); // 204 No Content } - @PutMapping("/{cafeId}") - public ResponseEntity updateCafe (@PathVariable Long cafeId, @RequestBody CafeRequest request){ - try { - cafeService.updateCafe(cafeId, request); - return ResponseEntity.ok().build(); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - } + @PatchMapping("/{cafeId}") + public ResponseEntity updateCafe( + @PathVariable Long cafeId, + @RequestBody CafeUpdateRequest request) { + + // 서비스의 updateCafe 메서드를 호출 + CafeResponse updatedCafe = cafeService.updateCafe(cafeId, request); - @DeleteMapping("{cafeId}") - public ResponseEntity deleteCafe(@PathVariable Long cafeId){ - try { - cafeService.deleteCafe(cafeId); - return ResponseEntity.noContent().build(); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } + // 수정된 정보(updatedCafe)를 클라이언트에게 200 OK와 함께 반환 + return ResponseEntity.ok(updatedCafe); } } \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeRepository.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeRepository.java index 8e1c550..b1e80ab 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeRepository.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeRepository.java @@ -4,5 +4,4 @@ import java.util.Optional; -public interface CafeRepository extends JpaRepository { -} +public interface CafeRepository extends JpaRepository { } diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeService.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeService.java index d72d280..18074ae 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeService.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/CafeService.java @@ -1,48 +1,29 @@ package com.cagong.receiptpowerserver.domain.cafe; import com.cagong.receiptpowerserver.domain.cafe.dto.CafeRequest; +import com.cagong.receiptpowerserver.domain.cafe.dto.CafeUpdateRequest; // [수정] 1. CafeUpdateRequest import import com.cagong.receiptpowerserver.domain.cafe.dto.CafeResponse; +// [수정] 2. Kakao API 및 ChatRoomService 관련 의존성 *모두 삭제* import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - import java.util.List; import java.util.stream.Collectors; + @Service @RequiredArgsConstructor -@Transactional(readOnly = true) public class CafeService { - private final CafeRepository cafeRepository; - public List getAllCafes(){ - return cafeRepository.findAll().stream() - .map(cafe -> new CafeResponse( - cafe.getId(), - cafe.getName(), - cafe.getAddress(), - cafe.getLatitude(), - cafe.getLongitude(), - cafe.getPhoneNumber() - )) - .collect(Collectors.toList()); - } + private final CafeRepository cafeRepository; - public CafeResponse getCafeById(Long cafeId){ - return cafeRepository.findById(cafeId) - .map(cafe -> new CafeResponse( - cafe.getId(), - cafe.getName(), - cafe.getAddress(), - cafe.getLatitude(), - cafe.getLongitude(), - cafe.getPhoneNumber() - )) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 카페를 찾을 수 없습니다: " + cafeId)); - } + // [수정] 3. ChatRoomService, RestTemplate, kakaoApiKey 의존성 *모두 삭제* + /** + * 1. 카페 생성 (POST /api/cafes) + */ @Transactional - public Long createCafe(CafeRequest request){ + public Long saveCafe(CafeRequest request) { Cafe cafe = Cafe.builder() .name(request.getCafeName()) .address(request.getAddress()) @@ -50,26 +31,57 @@ public Long createCafe(CafeRequest request){ .longitude(request.getLongitude()) .phoneNumber(request.getPhoneNumber()) .build(); - Cafe saved = cafeRepository.save(cafe); - return saved.getId(); - } - @Transactional - public void updateCafe(Long cafeId, CafeRequest request){ - Cafe cafe = cafeRepository.findById(cafeId) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 카페를 찾을 수 없습니다: " + cafeId)); + Cafe savedCafe = cafeRepository.save(cafe); + return savedCafe.getId(); + } - cafe.updateFrom(request); + /** + * 2. 카페 전체 조회 (GET /api/cafes/all) + */ + @Transactional(readOnly = true) + public List findAllCafes() { + return cafeRepository.findAll().stream() + .map(CafeResponse::new) // (CafeResponse(Cafe) 생성자 사용) + .collect(Collectors.toList()); + } - cafeRepository.save(cafe); + /** + * 3. 카페 ID로 1건 조회 (GET /api/cafes/{cafeId}) + */ + @Transactional(readOnly = true) + public CafeResponse findCafeById(Long cafeId) { + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new RuntimeException("해당 ID의 카페를 조회할 수 없습니다:" + cafeId)); + return new CafeResponse(cafe); // (CafeResponse(Cafe) 생성자 사용) } + /** + * 4. 카페 삭제 (DELETE /api/cafes/{cafeId}) + */ @Transactional - public void deleteCafe(Long cafeId){ - if(!cafeRepository.existsById(cafeId)){ - throw new IllegalArgumentException("해당 ID의 카페를 찾을 수 없습니다: " + cafeId); - } - cafeRepository.deleteById(cafeId); + public void deleteCafeById(Long cafeId) { + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new RuntimeException("해당 ID의 카페를 찾을 수 없습니다: " + cafeId)); + cafeRepository.delete(cafe); } -} + /** + * 5. 카페 수정 + * */ + @Transactional + public CafeResponse updateCafe(Long cafeId, CafeUpdateRequest request) { + // [수정] 4. updateCafe 로직을 올바르게 수정 + + // 1. DB에서 카페 Entity를 조회 + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new RuntimeException("해당 ID의 카페를 찾을 수 없습니다: " + cafeId)); + + // 2. Entity의 update 메서드 호출 (JPA 변경 감지) + // (이 코드가 작동하려면 Cafe.java에 update(CafeUpdateRequest request) 메서드가 있어야 합니다) + cafe.update(request); + + // 3. 수정된 Entity를 DTO로 변환하여 반환 + return new CafeResponse(cafe); + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeRequest.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeRequest.java index 3025cb1..782137e 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeRequest.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeRequest.java @@ -6,9 +6,9 @@ @Getter @AllArgsConstructor public class CafeRequest { - private String cafeName; + private String cafeName; // ❗️ 필드명 확인 private String address; private double latitude; private double longitude; private String phoneNumber; -} +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeResponse.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeResponse.java index fb585f6..603dd01 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeResponse.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeResponse.java @@ -1,10 +1,11 @@ package com.cagong.receiptpowerserver.domain.cafe.dto; - +// Cafe import 받는걸로 추가 +import com.cagong.receiptpowerserver.domain.cafe.Cafe; import lombok.AllArgsConstructor; import lombok.Getter; @Getter -@AllArgsConstructor +@AllArgsConstructor // 6개 인자를 받는 생성자 (그대로 둠) public class CafeResponse { private Long cafeId; private String cafeName; @@ -12,4 +13,18 @@ public class CafeResponse { private double latitude; private double longitude; private String phoneNumber; -} + + // [추가] 2. Cafe Entity를 인자로 받는 생성자를 추가합니다. + /** + * Cafe Entity를 CafeResponse DTO로 변환하는 생성자 + * @param cafe DB에서 조회한 Cafe Entity + */ + public CafeResponse(Cafe cafe) { + this.cafeId = cafe.getId(); + this.cafeName = cafe.getName(); + this.address = cafe.getAddress(); + this.latitude = cafe.getLatitude(); + this.longitude = cafe.getLongitude(); + this.phoneNumber = cafe.getPhoneNumber(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeUpdateRequest.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeUpdateRequest.java new file mode 100644 index 0000000..ccf8d1d --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeUpdateRequest.java @@ -0,0 +1,17 @@ +package com.cagong.receiptpowerserver.domain.cafe.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class CafeUpdateRequest { + + private String cafeName; + private String address; + private Double latitude; + private Double longitude; + private String phoneNumber; +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeWithChatRoomsResponse.java b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeWithChatRoomsResponse.java new file mode 100644 index 0000000..1dcfbb1 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/cafe/dto/CafeWithChatRoomsResponse.java @@ -0,0 +1,31 @@ +// domain/cafe/dto/CafeWithChatRoomsResponse.java + +package com.cagong.receiptpowerserver.domain.cafe.dto; + +import com.cagong.receiptpowerserver.domain.cafe.Cafe; +import com.cagong.receiptpowerserver.domain.chat.dto.ChatRoomResponse; +import lombok.Getter; + +import java.util.List; + +@Getter +public class CafeWithChatRoomsResponse { + + // 1. 카페 정보 필드 + private final Long cafeId; + private final String name; + private final String address; + private final String phoneNumber; + + // 2. 해당 카페에 속한 채팅방 목록 필드 + private final List chatRooms; + + // 생성자: Cafe 객체와 ChatRoomResponse 리스트를 받아서 이 DTO를 만듭니다. + public CafeWithChatRoomsResponse(Cafe cafe, List chatRooms) { + this.cafeId = cafe.getId(); + this.name = cafe.getName(); + this.address = cafe.getAddress(); + this.phoneNumber = cafe.getPhoneNumber(); + this.chatRooms = chatRooms; + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatMessageController.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatMessageController.java new file mode 100644 index 0000000..257e520 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatMessageController.java @@ -0,0 +1,38 @@ +// chat/ChatMessageController.java + +package com.cagong.receiptpowerserver.domain.chat; + +import com.cagong.receiptpowerserver.domain.chat.dto.ChatMessageRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Controller; + +@Controller +@RequiredArgsConstructor +public class ChatMessageController { + + private final SimpMessageSendingOperations messagingTemplate; + + @MessageMapping("/chat/message") + public void message(@Payload ChatMessageRequest message, StompHeaderAccessor headerAccessor) { + + // [핵심] STOMP 세션 속성에서 StompHandler가 저장해 둔 사용자 이름을 직접 가져옵니다. + String senderName = (String) headerAccessor.getSessionAttributes().get("username"); + + // 만약 비정상적인 접근으로 senderName이 없다면, 메시지 처리를 중단합니다. + if (senderName == null) { + return; + } + + message.setSender(senderName); + + if (ChatMessageRequest.MessageType.ENTER.equals(message.getType())) { + message.setMessage(senderName + "님이 입장하셨습니다."); + } + + messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message); + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoom.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoom.java new file mode 100644 index 0000000..f32d1e5 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoom.java @@ -0,0 +1,65 @@ +package com.cagong.receiptpowerserver.domain.chat; + +import com.cagong.receiptpowerserver.domain.cafe.Cafe; +import com.cagong.receiptpowerserver.domain.member.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public class ChatRoom { + + // 기본값 설정 + private static final Integer DEFAULT_MAX_PARTICIPANTS = 10; // 기본값 우선 10명 + + //기본 식별자 + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 채팅방 제목 - 필수 입력 + @Column(nullable = false) + private String title; + + // 하나의 카페에 여러 채팅방 가능 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cafe_id") + private Cafe cafe; + + // 생성자 정보 - 다대일 관게 ChatRoom : Member = N:1 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id") + private Member creator; + + // 채팅방 설정 + @Column(nullable = false) + private Integer maxParticipants = DEFAULT_MAX_PARTICIPANTS; // 최대 참여자 수 - 필수 입력, 기본값 : 10명 + + // 상태 관리 - ChatRoomStatus Enum - ACTIVE, INACTIVE, CLOSED + @Enumerated(EnumType.STRING) + private ChatRoomStatus status = ChatRoomStatus.ACTIVE; + + @CreatedDate + private LocalDateTime createdAt; + + @Builder + public ChatRoom(String title, Member creator, Integer maxParticipants) { + this.title = title; + this.creator = creator; + this.maxParticipants = maxParticipants != null ? maxParticipants : 10; + } + + // 상태 변경 메서드 추가 + public void setStatus(ChatRoomStatus status) { + this.status = status; + } +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomController.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomController.java new file mode 100644 index 0000000..6f0f453 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomController.java @@ -0,0 +1,115 @@ +package com.cagong.receiptpowerserver.domain.chat; + +import com.cagong.receiptpowerserver.domain.chat.dto.ChatRoomCreateRequest; +import com.cagong.receiptpowerserver.domain.chat.dto.ChatRoomResponse; +import com.cagong.receiptpowerserver.domain.chat.dto.ChatRoomStatusUpdateRequest; +import com.cagong.receiptpowerserver.global.security.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.lang.reflect.Method; +import java.util.List; + +@RestController +@RequestMapping("/api/chatrooms") +@RequiredArgsConstructor +public class ChatRoomController { + + private final ChatRoomService chatRoomService; + + @PostMapping + public ResponseEntity create( + @Valid @RequestBody ChatRoomCreateRequest request, + Authentication authentication + ) { + Long authenticatedUserId = extractUserId(authentication); + + ChatRoomResponse response = chatRoomService.create(request, authenticatedUserId); + + return ResponseEntity.created( + ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(response.getId()) + .toUri() + ).body(response); + } + + // 모든 활성화된 채팅방 조회 + @GetMapping + public ResponseEntity> getAllActiveRooms() { + return ResponseEntity.ok(chatRoomService.getAllActiveRooms()); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id) { + return ResponseEntity.ok(chatRoomService.getById(id)); + } + + @GetMapping("/me") + public ResponseEntity> getMyRooms(Authentication authentication) { + Long authenticatedUserId = extractUserId(authentication); + return ResponseEntity.ok(chatRoomService.getMyRooms(authenticatedUserId)); + } + + @PatchMapping("/{id}/status") + public ResponseEntity updateStatus( + @PathVariable Long id, + @Valid @RequestBody ChatRoomStatusUpdateRequest request, + Authentication authentication + ) { + Long authenticatedUserId = extractUserId(authentication); + return ResponseEntity.ok( + chatRoomService.updateStatus(id, authenticatedUserId, request.getStatus()) + ); + } + + /** + * 인증 주체에서 Long 타입의 사용자 ID를 추출합니다. + */ + private Long extractUserId(Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + throw new org.springframework.security.authentication.AuthenticationCredentialsNotFoundException("Unauthenticated"); + } + + Object principal = authentication.getPrincipal(); + + // [수정된 부분]: CustomUserDetails 타입인지 확인하고 ID를 직접 추출 + if (principal instanceof CustomUserDetails customUserDetails) { + return customUserDetails.getMember().getId(); + } + + // --- 이하 코드는 기존 코드의 안전 장치로 사용됩니다 --- + + if (principal instanceof UserDetails userDetails) { + try { + return Long.parseLong(userDetails.getUsername()); + } catch (NumberFormatException ignored) { + // fall through + } + } + + if (principal instanceof String s) { + try { + return Long.parseLong(s); + } catch (NumberFormatException ignored) { + // fall through + } + } + + try { + Method getId = principal.getClass().getMethod("getId"); + Object id = getId.invoke(principal); + if (id instanceof Long l) return l; + if (id instanceof Number n) return n.longValue(); + } catch (Exception ignored) { + // fall through + } + + throw new org.springframework.security.authentication.BadCredentialsException("Cannot resolve user id from principal"); + } +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomRepository.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomRepository.java new file mode 100644 index 0000000..eeb568a --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomRepository.java @@ -0,0 +1,15 @@ +package com.cagong.receiptpowerserver.domain.chat; + +import com.cagong.receiptpowerserver.domain.member.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ChatRoomRepository extends JpaRepository { + + // status로 채팅방 목록 조회 + List findByStatus(ChatRoomStatus status); + + // 생성자별 채팅방 조회 + List findByCreatorAndStatus(Member creator, ChatRoomStatus status); +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomService.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomService.java new file mode 100644 index 0000000..32edad0 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomService.java @@ -0,0 +1,86 @@ +// domain/chat/ChatRoomService.java + +package com.cagong.receiptpowerserver.domain.chat; + +import com.cagong.receiptpowerserver.domain.member.Member; +import com.cagong.receiptpowerserver.domain.member.MemberRepository; +import com.cagong.receiptpowerserver.domain.chat.dto.ChatRoomCreateRequest; +import com.cagong.receiptpowerserver.domain.chat.dto.ChatRoomResponse; +import com.cagong.receiptpowerserver.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatRoomService { + private final ChatRoomRepository chatRoomRepository; + private final MemberRepository memberRepository; + + @Transactional + public ChatRoomResponse create(ChatRoomCreateRequest req, Long authenticatedUserId) { + Member creator = memberRepository.findById(authenticatedUserId) + .orElseThrow(() -> new NotFoundException("creator not found: " + authenticatedUserId)); + + String title = req.getTitle().trim(); + if (title.isEmpty()) { + throw new IllegalArgumentException("title must not be blank"); + } + + ChatRoom chatRoom = ChatRoom.builder() + .title(title) + .creator(creator) + .maxParticipants(req.getMaxParticipants()) + .build(); + + ChatRoom saved = chatRoomRepository.save(chatRoom); + + return toResponse(saved); + } + + // 모든 활성화된 채팅방 조회 + public List getAllActiveRooms() { + return chatRoomRepository.findByStatus(ChatRoomStatus.ACTIVE) + .stream().map(this::toResponse).toList(); + } + + public ChatRoomResponse getById(Long id) { + ChatRoom room = chatRoomRepository.findById(id) + .orElseThrow(() -> new NotFoundException("chat room not found: " + id)); + return toResponse(room); + } + + public List getMyRooms(Long authenticatedUserId) { + Member creator = memberRepository.findById(authenticatedUserId) + .orElseThrow(() -> new NotFoundException("creator not found: " + authenticatedUserId)); + return chatRoomRepository.findByCreatorAndStatus(creator, ChatRoomStatus.ACTIVE) + .stream().map(this::toResponse).toList(); + } + + @Transactional + public ChatRoomResponse updateStatus(Long roomId, Long authenticatedUserId, ChatRoomStatus newStatus) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> new NotFoundException("chat room not found: " + roomId)); + + if (room.getCreator() == null || !room.getCreator().getId().equals(authenticatedUserId)) { + throw new IllegalStateException("only creator can change the room status"); + } + + room.setStatus(newStatus); + return toResponse(room); + } + + private ChatRoomResponse toResponse(ChatRoom saved) { + return ChatRoomResponse.builder() + .id(saved.getId()) + .title(saved.getTitle()) + .creatorId(saved.getCreator() != null ? saved.getCreator().getId() : null) + .maxParticipants(saved.getMaxParticipants()) + .status(saved.getStatus().name()) + .createdAt(saved.getCreatedAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomStatus.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomStatus.java new file mode 100644 index 0000000..f15df92 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/ChatRoomStatus.java @@ -0,0 +1,7 @@ +package com.cagong.receiptpowerserver.domain.chat; + +public enum ChatRoomStatus { + ACTIVE, + INACTIVE, + CLOSED +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatMessageRequest.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatMessageRequest.java new file mode 100644 index 0000000..2d9366a --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatMessageRequest.java @@ -0,0 +1,22 @@ +// chat/dto/ChatMessageRequest.java + +package com.cagong.receiptpowerserver.domain.chat.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class ChatMessageRequest { + + public enum MessageType { + ENTER, TALK, QUIT + } + + private MessageType type; // 메시지 타입 (입장, 대화, 퇴장) + private Long roomId; // 채팅방 ID + private String sender; // 채팅 참여자 이름 + private String message; // 메시지 내용 +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomCreateRequest.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomCreateRequest.java new file mode 100644 index 0000000..d25d441 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomCreateRequest.java @@ -0,0 +1,23 @@ +package com.cagong.receiptpowerserver.domain.chat.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ChatRoomCreateRequest { + + @NotBlank + @Size(max = 255) + private String title; + + @Min(2) + @Max(100) // 정책에 맞춰 조정 + private Integer maxParticipants; // 옵션: null 이면 기본값(엔티티에서 10) + +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomResponse.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomResponse.java new file mode 100644 index 0000000..716af17 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomResponse.java @@ -0,0 +1,28 @@ +package com.cagong.receiptpowerserver.domain.chat.dto; + +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ChatRoomResponse { + + private final Long id; + private final String title; + private final Long creatorId; + private final Integer maxParticipants; + private final String status; + private final LocalDateTime createdAt; + + @Builder + public ChatRoomResponse(Long id, String title, Long creatorId, + Integer maxParticipants, + String status, LocalDateTime createdAt) { + this.id = id; + this.title = title; + this.creatorId = creatorId; + this.maxParticipants = maxParticipants; + this.status = status; + this.createdAt = createdAt; + } +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomStatusUpdateRequest.java b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomStatusUpdateRequest.java new file mode 100644 index 0000000..501daf8 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/domain/chat/dto/ChatRoomStatusUpdateRequest.java @@ -0,0 +1,13 @@ +package com.cagong.receiptpowerserver.domain.chat.dto; + +import com.cagong.receiptpowerserver.domain.chat.ChatRoomStatus; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ChatRoomStatusUpdateRequest { + @NotNull + private ChatRoomStatus status; +} diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/member/MemberService.java b/src/main/java/com/cagong/receiptpowerserver/domain/member/MemberService.java index 103e951..09909e6 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/member/MemberService.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/member/MemberService.java @@ -44,6 +44,7 @@ public MemberSignupResponse signup(MemberSignupRequest request) { //로그인 @Transactional(readOnly = true) public MemberLoginResponse login(MemberLoginRequest request) { + // 1. 사용자 찾기 (username 또는 email로) Member member = findMemberByUsernameOrEmail(request.getUsernameOrEmail()); @@ -53,9 +54,9 @@ public MemberLoginResponse login(MemberLoginRequest request) { } // 3. JWT 토큰 생성 - String accessToken = jwtUtil.generateAccessToken(member.getId(), member.getUsername()); + String accessToken = jwtUtil.generateAccessToken(member.getId(), member.getId().toString()); - return new MemberLoginResponse(member, accessToken); + return MemberLoginResponse.of(member, accessToken); } // 검증 diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/member/dto/MemberLoginResponse.java b/src/main/java/com/cagong/receiptpowerserver/domain/member/dto/MemberLoginResponse.java index db1001b..02fb84a 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/member/dto/MemberLoginResponse.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/member/dto/MemberLoginResponse.java @@ -1,24 +1,39 @@ package com.cagong.receiptpowerserver.domain.member.dto; import com.cagong.receiptpowerserver.domain.member.Member; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class MemberLoginResponse { - - private final Long id; - private final String username; - private final String email; - private final String accessToken; - private final String tokenType; - private final String message; - - public MemberLoginResponse(Member member, String accessToken) { - this.id = member.getId(); - this.username = member.getUsername(); - this.email = member.getEmail(); - this.accessToken = accessToken; + + private Long id; + private String email; + private String username; + private String accessToken; + private String tokenType; + private String message; + + private MemberLoginResponse() { this.tokenType = "Bearer"; this.message = "로그인이 성공적으로 완료되었습니다."; } -} + + public static MemberLoginResponse of(Member member, String accessToken) { + return new MemberLoginResponse( + member.getId(), + member.getEmail(), + member.getUsername(), + accessToken, + "Bearer", + "로그인이 성공적으로 완료되었습니다." + ); + } + + // 테스트용 + public static MemberLoginResponse create() { + return new MemberLoginResponse(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/mileage/MileageRepository.java b/src/main/java/com/cagong/receiptpowerserver/domain/mileage/MileageRepository.java index 2d2d5c3..e2a98a2 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/mileage/MileageRepository.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/mileage/MileageRepository.java @@ -6,7 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; - import java.util.List; import java.util.Optional; diff --git a/src/main/java/com/cagong/receiptpowerserver/domain/oauth/OAuthService.java b/src/main/java/com/cagong/receiptpowerserver/domain/oauth/OAuthService.java index e55d2b3..c71025e 100644 --- a/src/main/java/com/cagong/receiptpowerserver/domain/oauth/OAuthService.java +++ b/src/main/java/com/cagong/receiptpowerserver/domain/oauth/OAuthService.java @@ -61,7 +61,7 @@ public MemberLoginResponse login(OAuthLoginRequest request) { // JWT 발급 String token = jwtUtil.generateAccessToken(member.getId(), member.getUsername()); - return new MemberLoginResponse(member, token); + return MemberLoginResponse.of(member, token); } private String generateUsername(String name) { diff --git a/src/main/java/com/cagong/receiptpowerserver/exception/NotFoundException.java b/src/main/java/com/cagong/receiptpowerserver/exception/NotFoundException.java new file mode 100644 index 0000000..696d57f --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/exception/NotFoundException.java @@ -0,0 +1,28 @@ +package com.cagong.receiptpowerserver.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * 리소스를 찾지 못했을 때 던지는 예외. + * 컨트롤러 계층까지 전파되면 404 Not Found로 응답합니다. + */ +@ResponseStatus(HttpStatus.NOT_FOUND) +public class NotFoundException extends RuntimeException { + + public NotFoundException() { + super(); + } + + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public NotFoundException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/cagong/receiptpowerserver/global/config/SecurityConfig.java b/src/main/java/com/cagong/receiptpowerserver/global/config/SecurityConfig.java index 80328c9..493d2dd 100644 --- a/src/main/java/com/cagong/receiptpowerserver/global/config/SecurityConfig.java +++ b/src/main/java/com/cagong/receiptpowerserver/global/config/SecurityConfig.java @@ -1,32 +1,69 @@ package com.cagong.receiptpowerserver.global.config; +// 필요한 import 문들... +import com.cagong.receiptpowerserver.global.jwt.JwtAuthenticationFilter; // 이 부분을 import 해주세요. import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; // Session 정책 import import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; // 이 부분을 import 해주세요. +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.Arrays; @Configuration @EnableWebSecurity -@RequiredArgsConstructor +@RequiredArgsConstructor // final 필드 주입을 위해 추가 public class SecurityConfig { - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) - .formLogin(form -> form.disable()); // login 기본 폼 로그인 비활성화 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() +// +// .requestMatchers( "/","/swagger-ui/**", "/v3/api-docs/**").permitAll() +// .requestMatchers("/chat.html", "/ws-stomp/**").permitAll() +// .requestMatchers(HttpMethod.POST, "/members/signup", "/members/login").permitAll() +// // ------------------ +// .requestMatchers(HttpMethod.GET, "/api/cafes/**").permitAll() // 카페 경로는 /api 유지 (CafeController와 일치 가정) +// .requestMatchers(HttpMethod.GET, "/api/chat-rooms/**").permitAll() // 채팅방 경로는 /api 유지 (ChatRoomController와 일치 가정) +// .anyRequest().authenticated() + ) + .httpBasic(httpBasic -> httpBasic.disable()); + + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } -} + + // CORS 설정 빈 (기존 코드와 거의 동일) + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080")); // 프론트엔드 주소에 맞게 수정 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); // 자격 증명 허용 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/global/config/WebSocketConfig.java b/src/main/java/com/cagong/receiptpowerserver/global/config/WebSocketConfig.java new file mode 100644 index 0000000..01c3e0d --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/global/config/WebSocketConfig.java @@ -0,0 +1,40 @@ +// global/config/WebSocketConfig.java + +package com.cagong.receiptpowerserver.global.config; + +import com.cagong.receiptpowerserver.global.jwt.StompHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +// @RequiredArgsConstructor를 제거합니다. +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompHandler stompHandler; + + // [수정된 부분] 생성자를 직접 작성합니다. + public WebSocketConfig(StompHandler stompHandler) { + this.stompHandler = stompHandler; + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/sub"); + registry.setApplicationDestinationPrefixes("/pub"); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompHandler); + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtAuthenticationFilter.java b/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtAuthenticationFilter.java index 5c97748..dfcafeb 100644 --- a/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtAuthenticationFilter.java @@ -2,7 +2,6 @@ import com.cagong.receiptpowerserver.domain.member.Member; import com.cagong.receiptpowerserver.domain.member.MemberRepository; -import com.cagong.receiptpowerserver.global.security.CustomUserDetails; import com.cagong.receiptpowerserver.global.security.CustomUserDetailsService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtUtil.java b/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtUtil.java index 66421a3..79c2851 100644 --- a/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtUtil.java +++ b/src/main/java/com/cagong/receiptpowerserver/global/jwt/JwtUtil.java @@ -4,32 +4,35 @@ import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; - import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; import java.util.Date; @Component public class JwtUtil { - + private final SecretKey key; private final long accessTokenExpiration; - - public JwtUtil(@Value("${jwt.secret:receiptpowersecretkeythatissecureenoughforjwttoken}") String secret, - @Value("${jwt.access-token-expiration:86400000}") long accessTokenExpiration) { - this.key = Keys.hmacShaKeyFor(secret.getBytes()); - this.accessTokenExpiration = accessTokenExpiration; + + public JwtUtil( + @Value("${jwt.secret}") String secretKey, // yml의 jwt.secret 값을 secretKey 변수에 주입 + @Value("${jwt.expiration}") long expiration // yml의 jwt.expiration 값을 expiration 변수에 주입 + ) { + // [수정] 4. 하드코딩된 변수 대신, yml에서 *주입받은 파라미터*를 사용합니다. + this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + this.accessTokenExpiration = expiration; } - + public String generateAccessToken(Long userId, String username) { return Jwts.builder() .subject(userId.toString()) .claim("username", username) .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + accessTokenExpiration)) + .expiration(new Date(System.currentTimeMillis() + this.accessTokenExpiration)) .signWith(key) .compact(); } - + public boolean validateToken(String token) { try { Jwts.parser() @@ -41,7 +44,7 @@ public boolean validateToken(String token) { return false; } } - + public Long getUserIdFromToken(String token) { Claims claims = Jwts.parser() .verifyWith(key) @@ -50,7 +53,7 @@ public Long getUserIdFromToken(String token) { .getPayload(); return Long.valueOf(claims.getSubject()); } - + public String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .verifyWith(key) diff --git a/src/main/java/com/cagong/receiptpowerserver/global/jwt/StompHandler.java b/src/main/java/com/cagong/receiptpowerserver/global/jwt/StompHandler.java new file mode 100644 index 0000000..c1eb4e3 --- /dev/null +++ b/src/main/java/com/cagong/receiptpowerserver/global/jwt/StompHandler.java @@ -0,0 +1,55 @@ +// global/jwt/StompHandler.java + +package com.cagong.receiptpowerserver.global.jwt; + +import com.cagong.receiptpowerserver.global.security.CustomUserDetailsService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class StompHandler implements ChannelInterceptor { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + + // STOMP 연결 요청일 때만 토큰을 검증하고 사용자 정보를 세션에 저장합니다. + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + try { + String jwt = accessor.getFirstNativeHeader("Authorization"); + + if (jwt != null && jwt.startsWith("Bearer ") && jwtUtil.validateToken(jwt.substring(7))) { + String token = jwt.substring(7); + Long userId = jwtUtil.getUserIdFromToken(token); + UserDetails userDetails = customUserDetailsService.loadUserById(userId); + + // [핵심] STOMP 세션 속성에 사용자 이름을 직접 저장합니다. + Map sessionAttributes = accessor.getSessionAttributes(); + if (sessionAttributes != null) { + sessionAttributes.put("username", userDetails.getUsername()); + log.info("User connected and authenticated: {}", userDetails.getUsername()); + } + } + } catch (Exception e) { + log.error("Authentication error during WebSocket connect", e); + // 여기서 예외를 던지면 클라이언트에게 오류가 전달됩니다. + // throw new MessagingException("Authentication failed"); + } + } + return message; + } +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/global/security/CustomUserDetails.java b/src/main/java/com/cagong/receiptpowerserver/global/security/CustomUserDetails.java index badc71c..8ca37d5 100644 --- a/src/main/java/com/cagong/receiptpowerserver/global/security/CustomUserDetails.java +++ b/src/main/java/com/cagong/receiptpowerserver/global/security/CustomUserDetails.java @@ -7,38 +7,40 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; +import java.util.Collections; // List 대신 Collections 사용 import java.util.List; @Getter public class CustomUserDetails implements UserDetails { - private final Long id; - private final String email; - private final String password; + private final Member member; // [수정] Member 객체를 통째로 저장합니다. public CustomUserDetails(Member member) { - this.id = member.getId(); - this.email = member.getEmail(); - this.password = member.getPassword(); + this.member = member; } + // --- 최종 수정 --- + // JWT 토큰 주체와 loadUserByUsername 파라미터를 "이메일"로 통일 @Override public String getUsername() { - return email; // Security에서 username으로 인식 + return member.getEmail(); } + // --- 동적 권한 --- + // member 객체에서 실제 Role을 꺼내 동적으로 권한 부여 @Override - public String getPassword() { - return password; + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); } @Override - public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority("ROLE_USER")); + public String getPassword() { + return member.getPassword(); } + // ... (나머지 4개 메서드는 true로 동일) ... @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } -} +} \ No newline at end of file diff --git a/src/main/java/com/cagong/receiptpowerserver/global/util/MemberUtil.java b/src/main/java/com/cagong/receiptpowerserver/global/util/MemberUtil.java index b5c6c3f..cfe2cb4 100644 --- a/src/main/java/com/cagong/receiptpowerserver/global/util/MemberUtil.java +++ b/src/main/java/com/cagong/receiptpowerserver/global/util/MemberUtil.java @@ -19,7 +19,7 @@ public static Long getCurrentMember() { } CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); - return userDetails.getId(); + return userDetails.getMember().getId(); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d310ecf..4be120e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,7 +15,7 @@ spring: client: registration: google: - client-id: "${CLIENT_SECRET}" + client-id: "${CLIENT_ID}" client-secret: "${CLIENT_SECRET}" redirect-uri: "{baseUrl}/login/oauth2/code/google" scope: @@ -26,6 +26,12 @@ spring: jwt: secret: "${JWT_SECRET}" + expiration: "${JWT_EXPIRATION}" + +# [추가] 카카오 API 설정 +kakao: + api: + key: "${KAKAO_API_KEY}" # 실제 키 대신 .env 파일의 변수 이름을 사용 mqtt: broker: ssl://75061fd0c9b94d79a3e6f99ea2f4055b.s1.eu.hivemq.cloud:8883 diff --git a/src/main/resources/static/chat.html b/src/main/resources/static/chat.html new file mode 100644 index 0000000..88c8e9d --- /dev/null +++ b/src/main/resources/static/chat.html @@ -0,0 +1,411 @@ + + + + Cagong Chat + + + + + + + + + + + + +
+ + +
+

로그인

+
+
+ + +
+ + +
+

회원가입

+
+ + +
+
+ + +
+
+ + +
+ + +
+ + + + +
+

전체 채팅방 목록

+

참여할 채팅방을 선택하거나, 새 채팅방을 만들어보세요.

+
    + +
+ + +
+ + +
+

새 채팅방 만들기

+

채팅방 이름은 "스타벅스 강남점"처럼 다른 사용자가 찾기 쉽게 지어주세요.

+
+
+ + +
+ + +
+ + +
+

+
+
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/src/test/java/com/cagong/receiptpowerserver/MemberSignupTest.java b/src/test/java/com/cagong/receiptpowerserver/MemberSignupTest.java new file mode 100644 index 0000000..a909f96 --- /dev/null +++ b/src/test/java/com/cagong/receiptpowerserver/MemberSignupTest.java @@ -0,0 +1,73 @@ +package com.cagong.receiptpowerserver; + +import com.cagong.receiptpowerserver.domain.member.Member; +import com.cagong.receiptpowerserver.domain.member.MemberRepository; +import com.cagong.receiptpowerserver.domain.member.MemberService; +import com.cagong.receiptpowerserver.domain.member.dto.MemberSignupRequest; +import com.cagong.receiptpowerserver.domain.member.dto.MemberSignupResponse; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +public class MemberSignupTest { + + @Autowired + private MemberService memberService; + + @Autowired + private MemberRepository memberRepository; + + @Test + void 회원가입_성공() { + // given + MemberSignupRequest request = new MemberSignupRequest( + "testuser", + "test@example.com", + "password123" + ); + + // when + MemberSignupResponse response = memberService.signup(request); + + // then + assertThat(response.getUsername()).isEqualTo("testuser"); + assertThat(response.getEmail()).isEqualTo("test@example.com"); + assertThat(response.getMessage()).contains("성공적으로 완료"); + + // 실제 저장 확인 + Member savedMember = memberRepository.findById(response.getId()).orElse(null); + assertThat(savedMember).isNotNull(); + assertThat(savedMember.getPassword()).isNotEqualTo("password123"); // 암호화 확인 + } + + @Test + void 중복_사용자명으로_회원가입_실패() { + // given + memberService.signup(new MemberSignupRequest("duplicate", "test1@example.com", "password123")); + + // when & then + assertThatThrownBy(() -> { + memberService.signup(new MemberSignupRequest("duplicate", "test2@example.com", "password456")); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이미 사용 중인 사용자명"); + } + + @Test + void 중복_이메일로_회원가입_실패() { + // given + memberService.signup(new MemberSignupRequest("user1", "duplicate@example.com", "password123")); + + // when & then + assertThatThrownBy(() -> { + memberService.signup(new MemberSignupRequest("user2", "duplicate@example.com", "password456")); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이미 사용 중인 이메일"); + } +} diff --git a/src/test/java/com/cagong/receiptpowerserver/cafe/CafeApiTest.java b/src/test/java/com/cagong/receiptpowerserver/cafe/CafeApiTest.java index 776f592..728ff07 100644 --- a/src/test/java/com/cagong/receiptpowerserver/cafe/CafeApiTest.java +++ b/src/test/java/com/cagong/receiptpowerserver/cafe/CafeApiTest.java @@ -16,6 +16,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.ActiveProfiles; +// import javax.management.relation.Role; // ✅ 잘못된 import 삭제 확인 + import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; @@ -23,6 +25,7 @@ @ActiveProfiles("test") public class CafeApiTest { + // ... (Autowired 필드 동일) ... @Autowired MemberRepository memberRepository; @Autowired @@ -30,6 +33,7 @@ public class CafeApiTest { @Autowired PasswordEncoder passwordEncoder; + @LocalServerPort int port; @@ -37,15 +41,15 @@ public class CafeApiTest { private String accessToken; - //TODO: setup메서드, 디비 초기화 테스트 패키지 전체화 하기 @BeforeEach void setup() { RestAssured.port = port; - //테스트db 초기화 + // ... (DB 초기화 동일) ... memberRepository.deleteAll(); cafeRepository.deleteAll(); + //멤버 저장 Member member = Member.builder() .username("테스터1") @@ -65,7 +69,10 @@ void setup() { .contentType(ContentType.JSON) .body(request) .when() + // --- ❗️ [수정 필요] --- + // SecurityConfig가 /members/login을 permitAll 하므로 /api 제거 .post("/members/login") + // -------------------- .then() .statusCode(200) .extract() @@ -73,45 +80,47 @@ void setup() { .getString("accessToken"); authorizationValue = "Bearer " + accessToken; + System.out.println("accessToken = " + accessToken); } + // --- (@Test 메서드들은 /api/cafes 경로 사용 유지 - CafeController와 일치 가정) --- @Test void 전체조회_성공한다() { + // ... (Cafe 생성 및 저장) ... Cafe cafe = Cafe.builder() .name("testCafe") .address("testAddress") - .latitude(37.111111) - .longitude(127.222222) .phoneNumber("12341234") .build(); + cafeRepository.save(cafe); given() .header("Authorization", authorizationValue) .when() - .get("/cafes/all") + .get("/cafes/all") // ✅ CafeController 경로와 일치 .then() + // ... (검증) ... .statusCode(200) .contentType(ContentType.JSON) - .body("size()", greaterThanOrEqualTo(0)); // 카페가 0개 이상 + .body("size()", greaterThanOrEqualTo(1)); } @Test void 카페_아이디로_조회_성공한다() { + // ... (Cafe 생성 및 저장) ... Cafe cafe = Cafe.builder() .name("testCafe") .address("testAddress") - .latitude(37.111111) - .longitude(127.222222) .phoneNumber("12341234") .build(); Cafe saved = cafeRepository.save(cafe); - Long cafeId = saved.getId(); + given() .header("Authorization", authorizationValue) .when() - .get("/cafes/{cafeId}", cafeId) + .get("/cafes/{cafeId}", cafeId) // ✅ CafeController 경로와 일치 .then() .statusCode(200); } @@ -131,29 +140,27 @@ void setup() { .contentType(ContentType.JSON) .body(request) .when() - .post("/cafes") + .post("/cafes") // ✅ CafeController 경로와 일치 .then() .statusCode(201); } @Test void 삭제_성공() { + // ... (Cafe 생성 및 저장) ... Cafe cafe = Cafe.builder() .name("testCafe") .address("testAddress") - .latitude(37.111111) - .longitude(127.222222) .phoneNumber("12341234") .build(); Cafe saved = cafeRepository.save(cafe); - Long cafeId = saved.getId(); given() .header("Authorization", authorizationValue) .when() - .delete("/cafes/{cafeId}", cafeId) + .delete("/cafes/{cafeId}", cafeId) // ✅ CafeController 경로와 일치 .then() .statusCode(204); } -} +} \ No newline at end of file diff --git a/src/test/java/com/cagong/receiptpowerserver/member/MemberTest.java b/src/test/java/com/cagong/receiptpowerserver/member/MemberTest.java index 47b32d0..69434a1 100644 --- a/src/test/java/com/cagong/receiptpowerserver/member/MemberTest.java +++ b/src/test/java/com/cagong/receiptpowerserver/member/MemberTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.password.PasswordEncoder; // ❗️ PasswordEncoder import 확인! import org.springframework.test.context.ActiveProfiles; import java.util.Optional; @@ -19,16 +20,27 @@ public class MemberTest { @Autowired private MemberRepository memberRepository; + @Autowired // ❗️ PasswordEncoder 주입 추가! + private PasswordEncoder passwordEncoder; + @Test void testSomething() { + // --- ⬇️ [이 부분을 다시 확인 & 수정] --- Member member = Member.builder() - .username("테스터") .email("test@gmail.com") - .password("password123") + .username("testuser") + // ❗️ passwordEncoder 사용! + .password(passwordEncoder.encode("password123")) .build(); + // ------------------------------------ + + // save() 메서드가 반환하는 값을 'saved' 변수에 저장 Member saved = memberRepository.save(member); Optional found = memberRepository.findById(saved.getId()); - Assertions.assertThat(found.get().getUsername()).isEqualTo("테스터"); + + // isPresent() 체크 추가 (더 안전함) + Assertions.assertThat(found).isPresent(); + Assertions.assertThat(found.get().getUsername()).isEqualTo(member.getUsername()); } -} +} \ No newline at end of file diff --git a/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberLoginTest.java b/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberLoginTest.java index 49df5d7..2cd8a5a 100644 --- a/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberLoginTest.java +++ b/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberLoginTest.java @@ -1,5 +1,6 @@ package com.cagong.receiptpowerserver.member.auth; +import com.cagong.receiptpowerserver.domain.member.MemberRepository; import com.cagong.receiptpowerserver.domain.member.MemberService; import com.cagong.receiptpowerserver.domain.member.dto.MemberSignupRequest; import com.cagong.receiptpowerserver.domain.member.dto.MemberLoginRequest; @@ -21,8 +22,12 @@ public class MemberLoginTest { @Autowired private MemberService memberService; + @Autowired + private MemberRepository memberRepository; + @BeforeEach void setUp() { + memberRepository.deleteAll(); // 테스트용 회원 생성 MemberSignupRequest signupRequest = new MemberSignupRequest( "testuser", @@ -41,7 +46,8 @@ void setUp() { MemberLoginResponse response = memberService.login(request); // then - assertThat(response.getUsername()).isEqualTo("testuser"); + // [수정됨] username 대신 ID와 email 검증 + assertThat(response.getId()).isNotNull(); assertThat(response.getEmail()).isEqualTo("test@example.com"); assertThat(response.getAccessToken()).isNotNull(); assertThat(response.getTokenType()).isEqualTo("Bearer"); @@ -57,7 +63,8 @@ void setUp() { MemberLoginResponse response = memberService.login(request); // then - assertThat(response.getUsername()).isEqualTo("testuser"); + // [수정됨] username 대신 ID가 null이 아닌지 확인 + assertThat(response.getId()).isNotNull(); assertThat(response.getEmail()).isEqualTo("test@example.com"); assertThat(response.getAccessToken()).isNotNull(); } diff --git a/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberSignupTest.java b/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberSignupTest.java index 829ac36..00348b5 100644 --- a/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberSignupTest.java +++ b/src/test/java/com/cagong/receiptpowerserver/member/auth/MemberSignupTest.java @@ -1,10 +1,11 @@ -package com.cagong.receiptpowerserver.member.auth; +package com.cagong.receiptpowerserver.member.auth; // 패키지 이름은 auth가 아닐 수 있습니다. import com.cagong.receiptpowerserver.domain.member.Member; import com.cagong.receiptpowerserver.domain.member.MemberRepository; import com.cagong.receiptpowerserver.domain.member.MemberService; import com.cagong.receiptpowerserver.domain.member.dto.MemberSignupRequest; import com.cagong.receiptpowerserver.domain.member.dto.MemberSignupResponse; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -21,27 +22,32 @@ public class MemberSignupTest { @Autowired private MemberService memberService; - + @Autowired private MemberRepository memberRepository; + @BeforeEach + void setUp() { + // 각 테스트 메서드 실행 전에 member 테이블 비우기 + memberRepository.deleteAll(); + } + @Test void 회원가입_성공() { // given MemberSignupRequest request = new MemberSignupRequest( - "testuser", - "test@example.com", - "password123" + "testuser", + "test@example.com", + "password123" ); - // when MemberSignupResponse response = memberService.signup(request); // then assertThat(response.getUsername()).isEqualTo("testuser"); assertThat(response.getEmail()).isEqualTo("test@example.com"); - assertThat(response.getMessage()).contains("성공적으로 완료"); - + // assertThat(response.getMessage()).contains("성공적으로 완료"); // MemberSignupResponse에 message 필드가 있다면 주석 해제 + // 실제 저장 확인 Member savedMember = memberRepository.findById(response.getId()).orElse(null); assertThat(savedMember).isNotNull(); @@ -51,13 +57,14 @@ public class MemberSignupTest { @Test void 중복_사용자명으로_회원가입_실패() { // given + // signup 메서드는 @Transactional이므로, 이 데이터는 테스트 종료 후 롤백됨 memberService.signup(new MemberSignupRequest("duplicate", "test1@example.com", "password123")); // when & then assertThatThrownBy(() -> { memberService.signup(new MemberSignupRequest("duplicate", "test2@example.com", "password456")); }).isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("이미 사용 중인 사용자명"); + .hasMessageContaining("이미 사용 중인 사용자명"); } @Test @@ -69,6 +76,6 @@ public class MemberSignupTest { assertThatThrownBy(() -> { memberService.signup(new MemberSignupRequest("user2", "duplicate@example.com", "password456")); }).isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("이미 사용 중인 이메일"); + .hasMessageContaining("이미 사용 중인 이메일"); } -} +} \ No newline at end of file diff --git a/src/test/java/com/cagong/receiptpowerserver/mileage/MileageTest.java b/src/test/java/com/cagong/receiptpowerserver/mileage/MileageTest.java index 1d7a114..824cb9b 100644 --- a/src/test/java/com/cagong/receiptpowerserver/mileage/MileageTest.java +++ b/src/test/java/com/cagong/receiptpowerserver/mileage/MileageTest.java @@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.mockito.Mock; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -38,7 +38,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.annotation.DirtiesContext; - +import org.springframework.transaction.annotation.Transactional; import java.util.List; import static io.restassured.RestAssured.*; @@ -52,10 +52,10 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MileageTest { - @MockBean + @Mock private MqttPublisher mqttPublisher; - @MockBean + @Mock private MqttService mqttService; @Autowired @@ -154,7 +154,7 @@ private Member createTestMember(String username, String email) { Member member = Member.builder() .username(username) .email(email) - .password("password123") + .password(passwordEncoder.encode("password123")) .build(); return memberRepository.save(member); } @@ -172,9 +172,8 @@ private Cafe createTestCafe(String name, String address, String phoneNumber) { @DisplayName("마일리지 저장 및 조회 테스트") void 마일리지_저장_및_조회_테스트() { // Given - 테스트 데이터 준비 - Member savedMember = createTestMember("마일리지테스터", "mileage@test.com"); + // Member savedMember = createTestMember("마일리지테스터", "mileage@test.com"); // ❗️ 이 줄 삭제! (setup에서 로그인한 유저 사용) Cafe savedCafe = createTestCafe("스타벅스 강남점", "서울시 강남구 역삼동", "02-1234-5678"); - // When - 마일리지 저장 /* Mileage mileage = Mileage.builder() @@ -187,8 +186,7 @@ private Cafe createTestCafe(String name, String address, String phoneNumber) { SaveMileageRequest request = new SaveMileageRequest(savedCafe.getId(),3000); mileageService.addMileage(request); - // Then - 검증 - //Optional found = mileageRepository.findById(savedMileage.getId()); + // Then - 검증 (setup에서 로그인한 유저 기준으로 조회됨) CafeMileageResponse response = mileageService.getCafeMileage(savedCafe.getId()); Assertions.assertThat(response.getPoints()).isEqualTo(120); @@ -245,7 +243,7 @@ private Cafe createTestCafe(String name, String address, String phoneNumber) { Member member = Member.builder() .username("연관관계테스터") .email("relation@test.com") - .password("password123") + .password(passwordEncoder.encode("password123")) .build(); Member savedMember = memberRepository.save(member); diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index c49c5e5..15ee44d 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,4 +1,13 @@ spring: + security: + oauth2: + client: + registration: + google: + client-id: 'dummy-google-client-id' + client-secret: 'dummy-google-client-secret' + redirect-uri: 'http://localhost:8080/login/oauth2/code/google' + datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE @@ -15,26 +24,17 @@ spring: console: enabled: true path: /h2-console - security: - oauth2: - client: - registration: - google: - client-id: "test-client-id" - client-secret: "test-client-secret" - redirect-uri: "{baseUrl}/login/oauth2/code/google" - scope: - - email - - profile - authorization-grant-type: authorization_code - client-name: Google - +# spring과 같은 레벨로 들여쓰기 없음 +kakao: + api: + key: 'dummy-kakao-api-key-for-testing' #테스트용 jwt: secret: ThisIsA_VeryLongAndSecureSecretKeyForJwtTEST_YouShouldChange + expiration: 86400000 mqtt: broker: ssl://75061fd0c9b94d79a3e6f99ea2f4055b.s1.eu.hivemq.cloud:8883 username: NodeMCU password: Cc011184 - client-id: spring-mqtt-client \ No newline at end of file + client-id: spring-mqtt-client diff --git a/testdb.mv.db b/testdb.mv.db deleted file mode 100644 index e69de29..0000000