diff --git a/.env.prod b/.env.prod deleted file mode 100644 index a371f54..0000000 --- a/.env.prod +++ /dev/null @@ -1,19 +0,0 @@ -SPRING_PROFILES_ACTIVE=prod - -# 운영 RDS -DB_URL=jdbc:mysql://your-rds-endpoint:3306/itda_prod?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul -DB_USERNAME= -DB_PASSWORD= - -# 운영 Redis -REDIS_HOST= -REDIS_PORT=6379 -REDIS_PASSWORD= - -SOLAPI_API_KEY= -SOLAPI_API_SECRET= -SOLAPI_DEFAULT_FROM= - - -JWT_ACCESS_SECRET_BASE64= -JWT_REFRESH_SECRET_BASE64= \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/cotato/itda/domain/chattopic/controller/ChatTopicController.java b/src/main/java/com/cotato/itda/domain/chattopic/controller/ChatTopicController.java new file mode 100644 index 0000000..0b02840 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/controller/ChatTopicController.java @@ -0,0 +1,25 @@ +package com.cotato.itda.domain.chattopic.controller; + +import com.cotato.itda.domain.chattopic.dto.res.ChatTopicResDTO; +import com.cotato.itda.domain.chattopic.service.query.ChatTopicQueryService; +import com.cotato.itda.global.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/chat-topics") +@Tag(name = "ChatTopic", description = "대화 주제") +public class ChatTopicController implements ChatTopicControllerDocs { + + private final ChatTopicQueryService chatTopicQueryService; + + @GetMapping + @Override + public ApiResponse getChatTopics() { + return ApiResponse.success(chatTopicQueryService.getChatTopics()); + } +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/controller/ChatTopicControllerDocs.java b/src/main/java/com/cotato/itda/domain/chattopic/controller/ChatTopicControllerDocs.java new file mode 100644 index 0000000..9180ee1 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/controller/ChatTopicControllerDocs.java @@ -0,0 +1,20 @@ +package com.cotato.itda.domain.chattopic.controller; + +import com.cotato.itda.domain.chattopic.dto.res.ChatTopicResDTO; +import com.cotato.itda.global.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.web.bind.annotation.GetMapping; + +public interface ChatTopicControllerDocs { + + @Operation( + summary = "대화 주제 조회 API By 정원", + description = "활성화된 대화 주제 목록만 조회합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공") + }) + @GetMapping + ApiResponse getChatTopics(); +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/converter/ChatTopicConverter.java b/src/main/java/com/cotato/itda/domain/chattopic/converter/ChatTopicConverter.java new file mode 100644 index 0000000..39c1d08 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/converter/ChatTopicConverter.java @@ -0,0 +1,26 @@ +package com.cotato.itda.domain.chattopic.converter; + +import com.cotato.itda.domain.chattopic.dto.res.ChatTopicResDTO; +import com.cotato.itda.domain.chattopic.entity.ChatTopic; + +import java.util.List; + +public class ChatTopicConverter { + + // Entity -> DTO + public static ChatTopicResDTO.ChatTopicDTO toChatTopicDTO(ChatTopic chatTopic) { + return ChatTopicResDTO.ChatTopicDTO.builder() + .name(chatTopic.getName()) + .code(chatTopic.getCode()) + .build(); + } + + public static ChatTopicResDTO.ChatTopicListDTO toChatTopicListDTO(List chatTopics) { + return ChatTopicResDTO.ChatTopicListDTO.builder() + .count(chatTopics.size()) + .chatTopicList(chatTopics.stream() + .map(ChatTopicConverter::toChatTopicDTO) + .toList()) + .build(); + } +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/dto/res/ChatTopicResDTO.java b/src/main/java/com/cotato/itda/domain/chattopic/dto/res/ChatTopicResDTO.java new file mode 100644 index 0000000..ed71178 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/dto/res/ChatTopicResDTO.java @@ -0,0 +1,26 @@ +package com.cotato.itda.domain.chattopic.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.List; + +public class ChatTopicResDTO { + + @Builder + public record ChatTopicListDTO( + @Schema(description = "채팅 주제 개수", example = "10") + Integer count, + @Schema(description = "채팅 주제 목록") + List chatTopicList + ) {} + + @Builder + public record ChatTopicDTO( + @Schema(description = "주제 이름", example = "일상") + String name, + @Schema(description = "주제 코드", example = "DAILY_LIFE") + String code + ) { + } +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/entity/ChatTopic.java b/src/main/java/com/cotato/itda/domain/chattopic/entity/ChatTopic.java new file mode 100644 index 0000000..11b6cb3 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/entity/ChatTopic.java @@ -0,0 +1,26 @@ +package com.cotato.itda.domain.chattopic.entity; + +import com.cotato.itda.global.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.*; + +@Entity +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Table(name = "chat_topic") +public class ChatTopic extends BaseEntity { + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "code", nullable = false, unique = true) + private String code; + + @Column(name = "is_active", nullable = false) + @Builder.Default + private Boolean isActive = true; +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/exception/ChatTopicException.java b/src/main/java/com/cotato/itda/domain/chattopic/exception/ChatTopicException.java new file mode 100644 index 0000000..caa6f5f --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/exception/ChatTopicException.java @@ -0,0 +1,16 @@ +package com.cotato.itda.domain.chattopic.exception; + +import com.cotato.itda.domain.chattopic.exception.code.ChatTopicErrorCode; +import com.cotato.itda.global.error.exception.BusinessException; + +import java.util.Map; + +public class ChatTopicException extends BusinessException { + public ChatTopicException(ChatTopicErrorCode errorCode) { + super(errorCode); + } + + public ChatTopicException(ChatTopicErrorCode errorCode, Map reasons) { + super(errorCode, reasons); + } +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/exception/code/ChatTopicErrorCode.java b/src/main/java/com/cotato/itda/domain/chattopic/exception/code/ChatTopicErrorCode.java new file mode 100644 index 0000000..32eb460 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/exception/code/ChatTopicErrorCode.java @@ -0,0 +1,28 @@ +package com.cotato.itda.domain.chattopic.exception.code; + +import com.cotato.itda.global.error.constant.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ChatTopicErrorCode implements ErrorCode { + + NOT_FOUND( + HttpStatus.NOT_FOUND, + "CHAT_TOPIC_ERROR_404_NOT_FOUND", + "대화 주제를 찾을 수 없습니다." + ), + + BAD_REQUEST( + HttpStatus.BAD_REQUEST, + "CHAT_TOPIC_ERROR_400_BAD_REQUEST", + "유효하지 않은 요청입니다." + ) + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/repository/ChatTopicRepository.java b/src/main/java/com/cotato/itda/domain/chattopic/repository/ChatTopicRepository.java new file mode 100644 index 0000000..05f3972 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/repository/ChatTopicRepository.java @@ -0,0 +1,12 @@ +package com.cotato.itda.domain.chattopic.repository; + +import com.cotato.itda.domain.chattopic.entity.ChatTopic; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ChatTopicRepository extends JpaRepository { + List findAllByCodeIn(List codes); + + List findAllByIsActiveTrue(); +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/service/query/ChatTopicQueryService.java b/src/main/java/com/cotato/itda/domain/chattopic/service/query/ChatTopicQueryService.java new file mode 100644 index 0000000..f6f8e39 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/service/query/ChatTopicQueryService.java @@ -0,0 +1,7 @@ +package com.cotato.itda.domain.chattopic.service.query; + +import com.cotato.itda.domain.chattopic.dto.res.ChatTopicResDTO; + +public interface ChatTopicQueryService { + ChatTopicResDTO.ChatTopicListDTO getChatTopics(); +} diff --git a/src/main/java/com/cotato/itda/domain/chattopic/service/query/ChatTopicQueryServiceImpl.java b/src/main/java/com/cotato/itda/domain/chattopic/service/query/ChatTopicQueryServiceImpl.java new file mode 100644 index 0000000..2eb545e --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chattopic/service/query/ChatTopicQueryServiceImpl.java @@ -0,0 +1,26 @@ +package com.cotato.itda.domain.chattopic.service.query; + +import com.cotato.itda.domain.chattopic.converter.ChatTopicConverter; +import com.cotato.itda.domain.chattopic.dto.res.ChatTopicResDTO; +import com.cotato.itda.domain.chattopic.entity.ChatTopic; +import com.cotato.itda.domain.chattopic.repository.ChatTopicRepository; +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 ChatTopicQueryServiceImpl implements ChatTopicQueryService { + + private final ChatTopicRepository chatTopicRepository; + + @Override + public ChatTopicResDTO.ChatTopicListDTO getChatTopics() { + List chatTopics = chatTopicRepository.findAllByIsActiveTrue(); + + return ChatTopicConverter.toChatTopicListDTO(chatTopics); + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/controller/FriendshipController.java b/src/main/java/com/cotato/itda/domain/friendship/controller/FriendshipController.java new file mode 100644 index 0000000..aeda5d0 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/controller/FriendshipController.java @@ -0,0 +1,93 @@ +package com.cotato.itda.domain.friendship.controller; + +import com.cotato.itda.domain.friendship.dto.req.FriendshipReqDTO; +import com.cotato.itda.domain.friendship.dto.res.FriendshipResDTO; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import com.cotato.itda.domain.friendship.service.command.FriendshipCommandService; +import com.cotato.itda.domain.friendship.service.query.FriendshipQueryService; +import com.cotato.itda.global.common.response.ApiResponse; +import com.cotato.itda.global.security.jwt.principal.JwtPrincipal; +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.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/friendships") +@Tag(name = "Friendship", description = "친구 관계 API") +public class FriendshipController implements FriendshipControllerDocs { + + private final FriendshipCommandService friendshipCommandService; + private final FriendshipQueryService friendshipQueryService; + + // TODO: JWT 사용 시 memberId 추출 로직으로 변경 + @SecurityRequirement(name = "AccessToken") + @PostMapping("/{friendId}") + @Override + public ApiResponse createFriendship( + @AuthenticationPrincipal JwtPrincipal jwtPrincipal, + @PathVariable Long friendId + ) { + Long memberId = jwtPrincipal.memberId(); + return ApiResponse.success(friendshipCommandService.createFriendship(friendId, memberId)); + } + + // TODO: JWT 사용 시 memberId 추출 로직으로 변경 + @SecurityRequirement(name = "AccessToken") + @PatchMapping("/{friendshipId}") + @Override + public ApiResponse updateFriendship( + @PathVariable Long friendshipId, + @AuthenticationPrincipal JwtPrincipal jwtPrincipal, + @Valid @RequestBody FriendshipReqDTO.UpdateDTO dto + ) { + Long memberId = jwtPrincipal.memberId(); + return ApiResponse.success(friendshipCommandService.updateFriendship(dto, friendshipId, memberId)); + } + + // TODO: JWT 사용 시 memberId 추출 로직으로 변경 + @SecurityRequirement(name = "AccessToken") + @GetMapping + @Override + public ApiResponse getFriendshipList( + @AuthenticationPrincipal JwtPrincipal jwtPrincipal, + @RequestParam(required = false) List status, + @SortDefault(sort = "lastInteractedAt", direction = Sort.Direction.DESC) Sort sort + ) { + Long memberId = jwtPrincipal.memberId(); + return ApiResponse.success(friendshipQueryService.getFriendshipList(memberId, status, sort)); + } + + // TODO: JWT 사용 시 memberId 추출 로직으로 변경 + @SecurityRequirement(name = "AccessToken") + @DeleteMapping("/{friendshipId}") + @Override + public ApiResponse deleteFriendship( + @PathVariable Long friendshipId, + @AuthenticationPrincipal JwtPrincipal jwtPrincipal + ) { + Long memberId = jwtPrincipal.memberId(); + friendshipCommandService.deleteFriendship(friendshipId, memberId); + return ApiResponse.success(null); + } + + // TODO: JWT 사용 시 memberId 추출 로직으로 변경 + @SecurityRequirement(name = "AccessToken") + @GetMapping("/settings/{friendshipId}") + @Override + public ApiResponse getFriendshipSettings( + @PathVariable Long friendshipId, + @AuthenticationPrincipal JwtPrincipal jwtPrincipal + ) { + Long memberId = jwtPrincipal.memberId(); + return ApiResponse.success(friendshipQueryService.getFriendshipSettings(friendshipId, memberId)); + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/controller/FriendshipControllerDocs.java b/src/main/java/com/cotato/itda/domain/friendship/controller/FriendshipControllerDocs.java new file mode 100644 index 0000000..7d2670f --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/controller/FriendshipControllerDocs.java @@ -0,0 +1,117 @@ +package com.cotato.itda.domain.friendship.controller; + +import com.cotato.itda.domain.friendship.dto.req.FriendshipReqDTO; +import com.cotato.itda.domain.friendship.dto.res.FriendshipResDTO; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import com.cotato.itda.global.common.response.ApiResponse; +import com.cotato.itda.global.security.jwt.principal.JwtPrincipal; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import org.springframework.data.domain.Sort; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +public interface FriendshipControllerDocs { + + @Operation( + summary = "친구 추가 API By 정원", + description = "friendId를 Path Variable로 전달받아 친구 관계를 추가합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "자기 자신을 친구로 추가하거나 이미 존재하는 친구 관계"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음") + }) + @PostMapping("/{friendId}") + ApiResponse createFriendship( + @Parameter(hidden = true) JwtPrincipal jwtPrincipal, + + @Parameter(description = "친구로 추가할 사용자 ID", required = true) + @PathVariable Long friendId + ); + + @Operation( + summary = "친구 관계 설정 API By 정원", + description = "친구 별명, 대화 말투, 대화 목표, 대화 주제를 설정합니다. 설정 완료 시 상태가 ACTIVE로 변경됩니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "유효하지 않은 입력값 또는 존재하지 않는 대화 주제"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음 (본인의 친구 관계가 아님)"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "친구 관계를 찾을 수 없음") + }) + @PatchMapping("/{friendshipId}") + ApiResponse updateFriendship( + @Parameter(description = "수정할 친구 관계 ID", required = true) + @PathVariable Long friendshipId, + + @Parameter(hidden = true) JwtPrincipal jwtPrincipal, + + @Valid @RequestBody FriendshipReqDTO.UpdateDTO dto + ); + + @Operation( + summary = "친구 목록 조회 API By 정원", + description = "전체 친구 목록을 조회합니다. status 필터링 및 정렬이 가능합니다. status를 지정하지 않으면 ACTIVE 상태의 친구만 조회됩니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음") + }) + @GetMapping + ApiResponse getFriendshipList( + @Parameter(hidden = true) JwtPrincipal jwtPrincipal, + + @Parameter( + description = "친구 설정 상태. 친구를 설정했다면 ACTIVE로, 설정하지 않았다면 PENDING으로 조회됩니다. 미입력 시 ACTIVE만 조회됩니다.", + example = "ACTIVE", + array = @ArraySchema(schema = @Schema(implementation = FriendshipStatus.class)) + ) + @RequestParam(required = false) List status, + + @Parameter( + description = "정렬 기준. 예: lastInteractedAt,desc 또는 createdAt,asc", + example = "lastInteractedAt,desc" + ) + Sort sort + ); + + @Operation( + summary = "친구 관계 삭제 API By 정원", + description = "친구 관계를 삭제합니다. DB에서 완전히 삭제되며 복구할 수 없습니다. 연관된 대화 주제도 함께 삭제됩니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음 (본인의 친구 관계가 아님)"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "친구 관계를 찾을 수 없음") + }) + @DeleteMapping("/{friendshipId}") + ApiResponse deleteFriendship( + @Parameter(description = "삭제할 친구 관계 ID", required = true) + @PathVariable Long friendshipId, + + @Parameter(hidden = true) JwtPrincipal jwtPrincipal + ); + + @Operation( + summary = "친구 관계 설정 조회 API By 정원", + description = "친구 관계의 상세 설정 정보를 조회합니다. 별명, 대화 말투, 대화 목표, 대화 주제를 포함합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음 (본인의 친구 관계가 아님)"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "친구 관계를 찾을 수 없음") + }) + @GetMapping("/settings/{friendshipId}") + ApiResponse getFriendshipSettings( + @Parameter(description = "조회할 친구 관계 ID", required = true) + @PathVariable Long friendshipId, + + @Parameter(hidden = true) JwtPrincipal jwtPrincipal + ); +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/converter/FriendshipConverter.java b/src/main/java/com/cotato/itda/domain/friendship/converter/FriendshipConverter.java new file mode 100644 index 0000000..4916871 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/converter/FriendshipConverter.java @@ -0,0 +1,80 @@ +package com.cotato.itda.domain.friendship.converter; + +import com.cotato.itda.domain.friendship.dto.req.FriendshipReqDTO; +import com.cotato.itda.domain.friendship.dto.res.FriendshipResDTO; +import com.cotato.itda.domain.friendship.entity.Friendship; +import com.cotato.itda.domain.member.entity.Member; +import java.util.List; + +public class FriendshipConverter { + + // Entity -> DTO + public static FriendshipResDTO.CreateDTO toCreateDTO(Friendship friendship) { + + FriendshipResDTO.FriendInfoDTO friendInfo = FriendshipResDTO.FriendInfoDTO.builder() + .id(friendship.getFriend().getId()) + .name(friendship.getFriend().getName()) + .profileImageUrl(friendship.getFriend().getProfileImageUrl()) + .build(); + + return FriendshipResDTO.CreateDTO.builder() + .friendshipId(friendship.getId()) + .friendInfo(friendInfo) + .status(friendship.getStatus()) + .createdAt(friendship.getCreatedAt()) + .build(); + } + + public static Friendship toFriendship(Member member, Member friend) { + return Friendship.builder() + .member(member) + .friend(friend) + .build(); + } + + // Entity -> DTO + public static FriendshipResDTO.UpdateDTO toUpdateDTO(Friendship friendship) { + return FriendshipResDTO.UpdateDTO.builder() + .friendshipId(friendship.getId()) + .status(friendship.getStatus()) + .updatedAt(friendship.getUpdatedAt()) + .build(); + } + + public static FriendshipResDTO.FriendshipItemDTO toFriendshipItemDTO(Friendship friendship) { + return FriendshipResDTO.FriendshipItemDTO.builder() + .friendshipId(friendship.getId()) + .name(friendship.getFriend().getName()) + .nickname(friendship.getNickname()) + .profileImageUrl(friendship.getFriend().getProfileImageUrl()) + .status(friendship.getStatus()) + .lastInteractedAt(friendship.getLastInteractedAt()) + .build(); + } + + public static FriendshipResDTO.FriendshipListDTO toFriendshipListDTO(List friendships) { + List items = friendships.stream() + .map(FriendshipConverter::toFriendshipItemDTO) + .toList(); + + return FriendshipResDTO.FriendshipListDTO.builder() + .count(friendships.size()) + .friendshipList(items) + .build(); + } + + public static FriendshipResDTO.FriendshipSettingsDTO toFriendshipSettingsDTO(Friendship friendship) { + + List topicCodes = friendship.getFriendshipTopics().stream() + .map(ft -> ft.getChatTopic().getCode()) + .toList(); + + return FriendshipResDTO.FriendshipSettingsDTO.builder() + .friendshipId(friendship.getId()) + .nickname(friendship.getNickname()) + .speechStyle(friendship.getSpeechStyle()) + .chatGoal(friendship.getChatGoal()) + .topicCodes(topicCodes) + .build(); + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/dto/req/FriendshipReqDTO.java b/src/main/java/com/cotato/itda/domain/friendship/dto/req/FriendshipReqDTO.java new file mode 100644 index 0000000..bceeeab --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/dto/req/FriendshipReqDTO.java @@ -0,0 +1,27 @@ +package com.cotato.itda.domain.friendship.dto.req; + +import com.cotato.itda.domain.friendship.enums.SpeechStyle; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public class FriendshipReqDTO { + + public record UpdateDTO( + @Schema(description = "친구 별명 (선택)", example = "어마마마") + String nickname, + @Schema(description = "대화 말투 (필수)", example = "존댓말", allowableValues = {"존댓말", "반말"}) + @NotNull(message = "대화 말투는 필수입니다.") + SpeechStyle speechStyle, + @Schema(description = "대화 목표 (필수)", example = "주 1-2회") + @NotBlank(message = "대화 목표는 필수입니다.") + String chatGoal, + @Schema(description = "대화 주제 코드 리스트 (필수)", example = "[\"DAILY_LIFE\", \"WEATHER\"]") + @NotEmpty(message = "대화 주제는 필수입니다.") + List topicCodes + ) { + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/dto/res/FriendshipResDTO.java b/src/main/java/com/cotato/itda/domain/friendship/dto/res/FriendshipResDTO.java new file mode 100644 index 0000000..eff48c7 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/dto/res/FriendshipResDTO.java @@ -0,0 +1,86 @@ +package com.cotato.itda.domain.friendship.dto.res; + +import com.cotato.itda.domain.chattopic.entity.ChatTopic; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import com.cotato.itda.domain.friendship.enums.SpeechStyle; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +public class FriendshipResDTO { + + @Builder + public record CreateDTO( + @Schema(description = "친구 관계 ID", example = "1") + Long friendshipId, + @Schema(description = "친구 정보") + FriendInfoDTO friendInfo, + @Schema(description = "친구 관계 상태", example = "PENDING") + FriendshipStatus status, + @Schema(description = "생성 일시", example = "2025-12-30T00:22:05.36169") + LocalDateTime createdAt + ) { + } + + @Builder + public record FriendInfoDTO( + @Schema(description = "친구 회원 ID", example = "2") + Long id, + @Schema(description = "친구 이름", example = "홍길동") + String name, + @Schema(description = "친구 프로필 이미지 URL", example = "https://example.com/profile.jpg") + String profileImageUrl + ) { + } + + @Builder + public record UpdateDTO( + @Schema(description = "친구 관계 ID", example = "1") + Long friendshipId, + @Schema(description = "친구 관계 상태", example = "ACTIVE") + FriendshipStatus status, + @Schema(description = "수정 일시", example = "2025-12-30T00:22:05.36169") + LocalDateTime updatedAt + ) { + } + + @Builder + public record FriendshipItemDTO( + @Schema(description = "친구 관계 ID", example = "1") + Long friendshipId, + @Schema(description = "친구 본명", example = "홍길동") + String name, + @Schema(description = "친구 별명", example = "길동이") + String nickname, + @Schema(description = "친구 프로필 이미지 URL", example = "https://example.com/profile.jpg") + String profileImageUrl, + @Schema(description = "친구 관계 상태", example = "ACTIVE") + FriendshipStatus status, + @Schema(description = "마지막 대화 일시", example = "2025-12-30T00:22:05.36169") + LocalDateTime lastInteractedAt + ) {} + + @Builder + public record FriendshipListDTO( + @Schema(description = "친구 관계 개수", example = "5") + Integer count, + @Schema(description = "친구 관계 목록") + List friendshipList + ) {} + + @Builder + public record FriendshipSettingsDTO( + @Schema(description = "친구 관계 ID", example = "1") + Long friendshipId, + @Schema(description = "친구 별명", example = "길동이") + String nickname, + @Schema(description = "대화 말투", example = "존댓말") + SpeechStyle speechStyle, + @Schema(description = "대화 목표", example = "주 1-2회") + String chatGoal, + @Schema(description = "대화 주제 코드 리스트", example = "[\"DAILY_LIFE\", \"WEATHER\"]") + List topicCodes + ) {} +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/entity/Friendship.java b/src/main/java/com/cotato/itda/domain/friendship/entity/Friendship.java new file mode 100644 index 0000000..a5364ab --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/entity/Friendship.java @@ -0,0 +1,63 @@ +package com.cotato.itda.domain.friendship.entity; + +import com.cotato.itda.domain.friendship.entity.mapping.FriendshipTopic; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import com.cotato.itda.domain.friendship.enums.SpeechStyle; +import com.cotato.itda.domain.member.entity.Member; +import com.cotato.itda.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Table(name = "friendship") +public class Friendship extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "friend_id", nullable = false) + private Member friend; + + @Column(name = "nickname") + private String nickname; + + @Column(name = "speech_style") + @Enumerated(EnumType.STRING) + private SpeechStyle speechStyle; + + @Column(name = "chat_goal") + private String chatGoal; + + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + @Builder.Default + private FriendshipStatus status = FriendshipStatus.PENDING; + + @Column(name = "is_favorite", nullable = false) + @Builder.Default + private Boolean isFavorite = false; + + @Column(name = "last_interacted_at", nullable = false) + @Builder.Default + private LocalDateTime lastInteractedAt = LocalDateTime.now(); + + @OneToMany(mappedBy = "friendship", cascade = CascadeType.ALL, orphanRemoval = true) + private List friendshipTopics = new ArrayList<>(); + + public void update(String nickname, SpeechStyle speechStyle, String chatGoal, FriendshipStatus status) { + this.nickname = nickname; + if (speechStyle != null) this.speechStyle = speechStyle; + if (chatGoal != null) this.chatGoal = chatGoal; + if (status != null) this.status = status; + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/entity/mapping/FriendshipTopic.java b/src/main/java/com/cotato/itda/domain/friendship/entity/mapping/FriendshipTopic.java new file mode 100644 index 0000000..d11b7a2 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/entity/mapping/FriendshipTopic.java @@ -0,0 +1,24 @@ +package com.cotato.itda.domain.friendship.entity.mapping; + +import com.cotato.itda.domain.chattopic.entity.ChatTopic; +import com.cotato.itda.domain.friendship.entity.Friendship; +import com.cotato.itda.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Table(name = "friendship_topic") +public class FriendshipTopic extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "friendship_id", nullable = false) + private Friendship friendship; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "chat_topic_id", nullable = false) + private ChatTopic chatTopic; +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/enums/FriendshipStatus.java b/src/main/java/com/cotato/itda/domain/friendship/enums/FriendshipStatus.java new file mode 100644 index 0000000..9aa8fd3 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/enums/FriendshipStatus.java @@ -0,0 +1,14 @@ +package com.cotato.itda.domain.friendship.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FriendshipStatus { + + PENDING("친구 설정 이전"), + ACTIVE("친구 설정 완료"), + ; + private final String description; +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/enums/SpeechStyle.java b/src/main/java/com/cotato/itda/domain/friendship/enums/SpeechStyle.java new file mode 100644 index 0000000..aa462a2 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/enums/SpeechStyle.java @@ -0,0 +1,37 @@ +package com.cotato.itda.domain.friendship.enums; + +import com.cotato.itda.domain.friendship.exception.FriendshipException; +import com.cotato.itda.domain.friendship.exception.code.FriendshipErrorCode; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public enum SpeechStyle { + + FORMAL("존댓말"), + INFORMAL("반말"), + ; + + private final String korean; + + @JsonCreator + public static SpeechStyle from(String value) { + for (SpeechStyle style : SpeechStyle.values()) { + if (style.korean.equals(value)) { + return style; + } + } + + throw new FriendshipException(FriendshipErrorCode.INVALID_SPEECH_STYLE, Map.of("speechStyle", value)); + } + + @JsonValue + public String toKorean() { + return this.korean; + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/exception/FriendshipException.java b/src/main/java/com/cotato/itda/domain/friendship/exception/FriendshipException.java new file mode 100644 index 0000000..34f23ff --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/exception/FriendshipException.java @@ -0,0 +1,16 @@ +package com.cotato.itda.domain.friendship.exception; + +import com.cotato.itda.domain.friendship.exception.code.FriendshipErrorCode; +import com.cotato.itda.global.error.exception.BusinessException; + +import java.util.Map; + +public class FriendshipException extends BusinessException { + public FriendshipException(FriendshipErrorCode errorCode) { + super(errorCode); + } + + public FriendshipException(FriendshipErrorCode errorCode, Map reasons) { + super(errorCode, reasons); + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/exception/code/FriendshipErrorCode.java b/src/main/java/com/cotato/itda/domain/friendship/exception/code/FriendshipErrorCode.java new file mode 100644 index 0000000..1ff1eca --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/exception/code/FriendshipErrorCode.java @@ -0,0 +1,45 @@ +package com.cotato.itda.domain.friendship.exception.code; + +import com.cotato.itda.global.error.constant.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum FriendshipErrorCode implements ErrorCode { + + NOT_FOUND( + HttpStatus.NOT_FOUND, + "FRIENDSHIP_ERROR_404_NOT_FOUND", + "친구 관계를 찾을 수 없습니다."), + + INVALID_SPEECH_STYLE( + HttpStatus.BAD_REQUEST, + "FRIENDSHIP_ERROR_400_INVALID_SPEECH_STYLE", + "유효하지 않은 대화 말투입니다." + ), + + CANNOT_ADD_SELF( + HttpStatus.BAD_REQUEST, + "FRIENDSHIP_ERROR_400_CANNOT_ADD_SELF", + "자기 자신을 친구로 추가할 수 없습니다." + ), + + FRIENDSHIP_ALREADY_EXISTS( + HttpStatus.BAD_REQUEST, + "FRIENDSHIP_ERROR_400_FRIENDSHIP_ALREADY_EXISTS", + "이미 존재하는 친구 관계입니다." + ), + + FORBIDDEN( + HttpStatus.FORBIDDEN, + "FRIENDSHIP_ERROR_403_FORBIDDEN", + "권한이 없는 친구 관계입니다." + ), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/repository/FriendshipRepository.java b/src/main/java/com/cotato/itda/domain/friendship/repository/FriendshipRepository.java new file mode 100644 index 0000000..0b875a0 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/repository/FriendshipRepository.java @@ -0,0 +1,15 @@ +package com.cotato.itda.domain.friendship.repository; + +import com.cotato.itda.domain.friendship.entity.Friendship; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import jakarta.validation.constraints.NotNull; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FriendshipRepository extends JpaRepository { + List findAllByMemberIdAndStatusIn(Long memberId, List statuses, Sort sort); + + boolean existsByMemberIdAndFriendId(Long memberId, Long friendId); +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/repository/FriendshipTopicRepository.java b/src/main/java/com/cotato/itda/domain/friendship/repository/FriendshipTopicRepository.java new file mode 100644 index 0000000..3556816 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/repository/FriendshipTopicRepository.java @@ -0,0 +1,10 @@ +package com.cotato.itda.domain.friendship.repository; + +import com.cotato.itda.domain.friendship.entity.mapping.FriendshipTopic; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FriendshipTopicRepository extends JpaRepository { + List findByFriendshipId(Long id); +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/service/command/FriendshipCommandService.java b/src/main/java/com/cotato/itda/domain/friendship/service/command/FriendshipCommandService.java new file mode 100644 index 0000000..1d3f675 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/service/command/FriendshipCommandService.java @@ -0,0 +1,13 @@ +package com.cotato.itda.domain.friendship.service.command; + +import com.cotato.itda.domain.friendship.dto.req.FriendshipReqDTO; +import com.cotato.itda.domain.friendship.dto.res.FriendshipResDTO; +import org.springframework.transaction.annotation.Transactional; + +public interface FriendshipCommandService { + FriendshipResDTO.CreateDTO createFriendship(Long friendId, Long memberId); + + FriendshipResDTO.UpdateDTO updateFriendship(FriendshipReqDTO.UpdateDTO dto, Long friendshipId, Long memberId); + + void deleteFriendship(Long friendshipId, Long memberId); +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/service/command/FriendshipCommandServiceImpl.java b/src/main/java/com/cotato/itda/domain/friendship/service/command/FriendshipCommandServiceImpl.java new file mode 100644 index 0000000..211f019 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/service/command/FriendshipCommandServiceImpl.java @@ -0,0 +1,169 @@ +package com.cotato.itda.domain.friendship.service.command; + +import com.cotato.itda.domain.friendship.converter.FriendshipConverter; +import com.cotato.itda.domain.friendship.dto.req.FriendshipReqDTO; +import com.cotato.itda.domain.friendship.dto.res.FriendshipResDTO; +import com.cotato.itda.domain.chattopic.entity.ChatTopic; +import com.cotato.itda.domain.chattopic.exception.ChatTopicException; +import com.cotato.itda.domain.chattopic.exception.code.ChatTopicErrorCode; +import com.cotato.itda.domain.chattopic.repository.ChatTopicRepository; +import com.cotato.itda.domain.friendship.entity.Friendship; +import com.cotato.itda.domain.friendship.entity.mapping.FriendshipTopic; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import com.cotato.itda.domain.friendship.exception.FriendshipException; +import com.cotato.itda.domain.friendship.exception.code.FriendshipErrorCode; +import com.cotato.itda.domain.friendship.repository.FriendshipRepository; +import com.cotato.itda.domain.friendship.repository.FriendshipTopicRepository; +import com.cotato.itda.domain.member.entity.Member; +import com.cotato.itda.domain.member.repository.MemberRepository; +import com.cotato.itda.global.error.constant.UserErrorCode; +import com.cotato.itda.global.error.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FriendshipCommandServiceImpl implements FriendshipCommandService { + + private final FriendshipRepository friendshipRepository; + private final MemberRepository memberRepository; + private final ChatTopicRepository chatTopicRepository; + private final FriendshipTopicRepository friendshipTopicRepository; + + @Transactional + @Override + public FriendshipResDTO.CreateDTO createFriendship(Long friendId, Long memberId) { + + if (memberId.equals(friendId)) { + throw new FriendshipException( + FriendshipErrorCode.CANNOT_ADD_SELF + ); + } + + if (friendshipRepository.existsByMemberIdAndFriendId(memberId, friendId)) { + throw new FriendshipException( + FriendshipErrorCode.FRIENDSHIP_ALREADY_EXISTS + ); + } + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_FOUND, Map.of("userId", memberId))); + + Member friend = memberRepository.findById(friendId) + .orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_FOUND, Map.of("friendId", friendId))); + + Friendship friendship = FriendshipConverter.toFriendship(member, friend); + + Friendship savedFriendship = friendshipRepository.save(friendship); + + return FriendshipConverter.toCreateDTO(savedFriendship); + } + + @Transactional + @Override + public FriendshipResDTO.UpdateDTO updateFriendship(FriendshipReqDTO.UpdateDTO dto, Long friendshipId, Long memberId) { + + Friendship friendship = friendshipRepository.findById(friendshipId) + .orElseThrow(() -> new FriendshipException(FriendshipErrorCode.NOT_FOUND, Map.of("friendshipId", friendshipId))); + + // 본인의 친구 관계인지 확인 + if (!friendship.getMember().getId().equals(memberId)) { + throw new FriendshipException(FriendshipErrorCode.FORBIDDEN, Map.of("friendshipId", friendshipId, "memberId", memberId)); + } + + friendship.update(dto.nickname(), dto.speechStyle(), dto.chatGoal(), FriendshipStatus.ACTIVE); + + updateFriendshipTopics(friendship, dto.topicCodes()); + + return FriendshipConverter.toUpdateDTO(friendship); + } + + private void updateFriendshipTopics(Friendship friendship, List topicCodes) { + // 1. 요청한 주제 검증 및 조회 + List requestedTopics = validateAndFetchTopics(topicCodes); + + // 2. 기존 주제 조회 + List existingTopics = friendship.getFriendshipTopics(); + + // 3. 변경분 계산 및 처리 + updateTopicChanges(friendship, requestedTopics, existingTopics); + } + + private List validateAndFetchTopics(List topicCodes) { + List topics = chatTopicRepository.findAllByCodeIn(topicCodes); + + Set foundCodes = topics.stream() + .map(ChatTopic::getCode) + .collect(Collectors.toSet()); + + List notFoundCodes = topicCodes.stream() + .filter(code -> !foundCodes.contains(code)) + .toList(); + + if (!notFoundCodes.isEmpty()) { + throw new ChatTopicException( + ChatTopicErrorCode.BAD_REQUEST, + Map.of("notFoundCodes", notFoundCodes, "requestedCodes", topicCodes) + ); + } + + return topics; + } + + private void updateTopicChanges( + Friendship friendship, + List requestedTopics, + List existingTopics + ) { + Set requestedCodes = requestedTopics.stream() + .map(ChatTopic::getCode) + .collect(Collectors.toSet()); + + Set existingCodes = existingTopics.stream() + .map(ft -> ft.getChatTopic().getCode()) + .collect(Collectors.toSet()); + + // 삭제할 항목 (기존에 있었는데 요청에 없는 것) + List topicsToDelete = existingTopics.stream() + .filter(ft -> !requestedCodes.contains(ft.getChatTopic().getCode())) + .toList(); + + // 추가할 항목 (요청에 있는데 기존에 없는 것) + List topicsToAdd = requestedTopics.stream() + .filter(topic -> !existingCodes.contains(topic.getCode())) + .map(topic -> FriendshipTopic.builder() + .friendship(friendship) + .chatTopic(topic) + .build()) + .toList(); + + // 저장 + if (!topicsToDelete.isEmpty()) { + friendshipTopicRepository.deleteAll(topicsToDelete); + } + if (!topicsToAdd.isEmpty()) { + friendshipTopicRepository.saveAll(topicsToAdd); + } + } + + @Override + @Transactional + public void deleteFriendship(Long friendshipId, Long memberId) { + Friendship friendship = friendshipRepository.findById(friendshipId) + .orElseThrow(() -> new FriendshipException(FriendshipErrorCode.NOT_FOUND, Map.of("friendshipId", friendshipId))); + + // 본인의 친구 관계인지 확인 + if (!friendship.getMember().getId().equals(memberId)) { + throw new FriendshipException(FriendshipErrorCode.FORBIDDEN, Map.of("friendshipId", friendshipId, "memberId", memberId)); + } + + friendshipRepository.delete(friendship); + } +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/service/query/FriendshipQueryService.java b/src/main/java/com/cotato/itda/domain/friendship/service/query/FriendshipQueryService.java new file mode 100644 index 0000000..4395ab4 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/service/query/FriendshipQueryService.java @@ -0,0 +1,13 @@ +package com.cotato.itda.domain.friendship.service.query; + +import com.cotato.itda.domain.friendship.dto.res.FriendshipResDTO; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import org.springframework.data.domain.Sort; + +import java.util.List; + +public interface FriendshipQueryService { + FriendshipResDTO.FriendshipListDTO getFriendshipList(Long memberId, List statuses, Sort sort); + + FriendshipResDTO.FriendshipSettingsDTO getFriendshipSettings(Long friendshipId, Long memberId); +} diff --git a/src/main/java/com/cotato/itda/domain/friendship/service/query/FriendshipQueryServiceImpl.java b/src/main/java/com/cotato/itda/domain/friendship/service/query/FriendshipQueryServiceImpl.java new file mode 100644 index 0000000..c60bab4 --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/friendship/service/query/FriendshipQueryServiceImpl.java @@ -0,0 +1,62 @@ +package com.cotato.itda.domain.friendship.service.query; + +import com.cotato.itda.domain.friendship.converter.FriendshipConverter; +import com.cotato.itda.domain.friendship.dto.res.FriendshipResDTO; +import com.cotato.itda.domain.friendship.entity.Friendship; +import com.cotato.itda.domain.friendship.enums.FriendshipStatus; +import com.cotato.itda.domain.friendship.exception.FriendshipException; +import com.cotato.itda.domain.friendship.exception.code.FriendshipErrorCode; +import com.cotato.itda.domain.friendship.repository.FriendshipRepository; +import com.cotato.itda.domain.member.repository.MemberRepository; +import com.cotato.itda.global.error.constant.UserErrorCode; +import com.cotato.itda.global.error.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FriendshipQueryServiceImpl implements FriendshipQueryService { + + private final FriendshipRepository friendshipRepository; + private final MemberRepository memberRepository; + + @Override + public FriendshipResDTO.FriendshipListDTO getFriendshipList( + Long memberId, + List statuses, + Sort sort + ) { + if (!memberRepository.existsById(memberId)) { + throw new BusinessException(UserErrorCode.USER_NOT_FOUND, + Map.of("memberId", memberId)); + } + + if (statuses == null || statuses.isEmpty()) { + statuses = List.of(FriendshipStatus.ACTIVE); + } + + List friendships = friendshipRepository + .findAllByMemberIdAndStatusIn(memberId, statuses, sort); + + return FriendshipConverter.toFriendshipListDTO(friendships); + } + + @Override + public FriendshipResDTO.FriendshipSettingsDTO getFriendshipSettings(Long friendshipId, Long memberId) { + Friendship friendship = friendshipRepository.findById(friendshipId) + .orElseThrow(() -> new FriendshipException(FriendshipErrorCode.NOT_FOUND, Map.of("friendshipId", friendshipId))); + + // 본인의 친구 관계인지 확인 + if (!friendship.getMember().getId().equals(memberId)) { + throw new FriendshipException(FriendshipErrorCode.FORBIDDEN, Map.of("friendshipId", friendshipId, "memberId", memberId)); + } + + return FriendshipConverter.toFriendshipSettingsDTO(friendship); + } +} diff --git a/src/main/java/com/cotato/itda/global/config/SwaggerConfig.java b/src/main/java/com/cotato/itda/global/config/SwaggerConfig.java index a660d3a..b457c65 100644 --- a/src/main/java/com/cotato/itda/global/config/SwaggerConfig.java +++ b/src/main/java/com/cotato/itda/global/config/SwaggerConfig.java @@ -84,6 +84,16 @@ public GroupedOpenApi MemberApi() { .build(); } + @Bean + public GroupedOpenApi FriendshipApi() { + return GroupedOpenApi.builder() + .group("Friendship") + .displayName("Friendship API") + .packagesToScan("com.cotato.itda.domain.friendship.controller") + .pathsToMatch("/api/friendships/**") + .build(); + } + @Bean public GroupedOpenApi ProfileApi() { return GroupedOpenApi.builder() @@ -94,6 +104,16 @@ public GroupedOpenApi ProfileApi() { .build(); } + @Bean + public GroupedOpenApi ChatTopicApi() { + return GroupedOpenApi.builder() + .group("ChatTopic") + .displayName("ChatTopic API") + .packagesToScan("com.cotato.itda.domain.chattopic.controller") + .pathsToMatch("/api/chat-topics/**") + .build(); + } + @Bean public GroupedOpenApi ImageApi() { return GroupedOpenApi.builder()