diff --git a/.env b/.env index 5e6d4ad..3c536f1 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ SPRING_PROFILES_ACTIVE=local # prod에서 local로 변경 -LOCAL_DB_URL=jdbc:mysql://localhost:3306/cokerthon-local?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&createDatabaseIfNotExist=true +LOCAL_DB_URL=jdbc:mysql://localhost:3306/cokerthon LOCAL_DB_USERNAME=root -LOCAL_DB_PASSWORD=dlgodnjs! +LOCAL_DB_PASSWORD=1234 PROD_DB_URL=jdbc:mysql://cokerthon-db.clgeccwgurgu.ap-northeast-2.rds.amazonaws.com:3306/cokathon?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 PROD_DB_USERNAME=admin PROD_DB_PASSWORD=cokerthonpassword \ No newline at end of file diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/controller/EmotionController.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/controller/EmotionController.java new file mode 100644 index 0000000..667aeb4 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/controller/EmotionController.java @@ -0,0 +1,194 @@ +package com.cokerthon.Team3_Backend.domain.emotion.controller; + +import com.cokerthon.Team3_Backend.domain.emotion.converter.EmotionConverter; +import com.cokerthon.Team3_Backend.domain.emotion.dto.request.EmotionRequestDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionResponseDto; +import com.cokerthon.Team3_Backend.domain.emotion.entity.EmotionRecord; +import com.cokerthon.Team3_Backend.domain.emotion.exception.code.EmotionSuccessCode; +import com.cokerthon.Team3_Backend.domain.emotion.service.command.EmotionCommandService; +import com.cokerthon.Team3_Backend.domain.user.entity.User; +import com.cokerthon.Team3_Backend.domain.user.repository.UserRepository; +import com.cokerthon.Team3_Backend.global.apiPayload.ApiResponse; +import com.cokerthon.Team3_Backend.global.apiPayload.code.GeneralErrorCode; +import com.cokerthon.Team3_Backend.global.exception.GeneralException; +import com.cokerthon.Team3_Backend.global.security.CurrentUser; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Tag( + name = "Emotion", + description = "감정 기록 생성·수정·삭제 API" +) +@RestController +@RequestMapping("/api/emotions") +@RequiredArgsConstructor +public class EmotionController { + + private final EmotionCommandService emotionCommandService; + private final UserRepository userRepository; + + /** + * 감정 생성 API + * @param requestDto EmotionCreateRequestDto + * @param user 로그인 된 사용자 엔티티 + * @return 감정 생성 완료 응답 + */ + @Operation( + summary = "감정 기록 생성 by 임준서(개발 완료)", + description = """ + 현재 로그인한 사용자가 감정 기록을 생성합니다. + + - 감정 점수는 1~5 사이 값만 허용됩니다. + - visibility = PUBLIC 인 경우 다른 사용자에게 공개됩니다. + """ + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "감정 기록 생성 성공" + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "잘못된 요청 (감정 점수 범위 오류 등)", + content = @Content + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "로그인되지 않은 사용자", + content = @Content + ) + }) + @PostMapping + public ResponseEntity> createEmotion( + @RequestBody EmotionRequestDto.EmotionCreateRequestDto requestDto, + @CurrentUser User user + ) { + EmotionRecord emotion = + emotionCommandService.createEmotion(user, requestDto); + + return ResponseEntity.ok( + ApiResponse.onSuccess( + EmotionSuccessCode.EMOTION_CREATED, + EmotionConverter.toCreateResponse(emotion) + ) + ); + } + + /** + * 감정 수정 API + * @param emotionId 감정 기록 ID + * @param requestDto EmotionUpdateRequestDto + * @param user 로그인 된 사용자 엔티티 + * @return 감정 수정 완료 응답 + */ + @Operation( + summary = "감정 기록 수정 by 임준서(개발 완료)", + description = """ + 기존에 작성한 감정 기록을 수정합니다. + + - 작성자 본인만 수정할 수 있습니다. + - 감정 점수는 1~5 사이 값만 허용됩니다. + """ + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "감정 기록 수정 성공" + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "403", + description = "작성자가 아닌 경우", + content = @Content + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "감정 기록을 찾을 수 없음", + content = @Content + ) + }) + @PutMapping("/{emotionId}") + public ResponseEntity> updateEmotion( + @PathVariable Long emotionId, + @RequestBody EmotionRequestDto.EmotionUpdateRequestDto requestDto, + @CurrentUser User user + ) { + emotionCommandService.updateEmotion( + emotionId, + user, + requestDto + ); + + return ResponseEntity.ok( + ApiResponse.onSuccess( + EmotionSuccessCode.EMOTION_UPDATED, + null + ) + ); + } + + /** + * 감정 삭제 API + * @param emotionId 감정 기록 ID + * @param user 로그인 된 사용자 엔티티 + * @return 감정 삭제 완료 응답 + */ + @Operation( + summary = "감정 기록 삭제 by 임준서(개발 완료)", + description = """ + 감정 기록을 삭제합니다. + + - 작성자 본인만 삭제할 수 있습니다. + - 삭제된 감정 기록은 복구할 수 없습니다. + """ + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "감정 기록 삭제 성공" + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "403", + description = "작성자가 아닌 경우", + content = @Content + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "감정 기록을 찾을 수 없음", + content = @Content + ) + }) + @DeleteMapping("/{emotionId}") + public ResponseEntity> deleteEmotion( + @PathVariable Long emotionId, + @CurrentUser User user + ) { + + emotionCommandService.deleteEmotion(emotionId, user); + + return ResponseEntity.ok( + ApiResponse.onSuccess( + EmotionSuccessCode.EMOTION_DELETED, + null + ) + ); + } + + /* ===== private ===== */ + + private User getLoginUser(Long userId) { + if (userId == null) { + throw new GeneralException(GeneralErrorCode.UNAUTHORIZED); + } + + return userRepository.findById(userId) + .orElseThrow(() -> + new GeneralException(GeneralErrorCode.UNAUTHORIZED) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/converter/EmotionConverter.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/converter/EmotionConverter.java new file mode 100644 index 0000000..bbd8bbb --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/converter/EmotionConverter.java @@ -0,0 +1,50 @@ +package com.cokerthon.Team3_Backend.domain.emotion.converter; + +import com.cokerthon.Team3_Backend.domain.emotion.dto.request.EmotionRequestDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionResponseDto; +import com.cokerthon.Team3_Backend.domain.emotion.entity.EmotionRecord; +import com.cokerthon.Team3_Backend.domain.user.entity.User; + +public class EmotionConverter { + + private EmotionConverter() { + } + + /** 생성 요청 DTO → Entity */ + public static EmotionRecord toEntity( + User user, + EmotionRequestDto.EmotionCreateRequestDto dto + ) { + return EmotionRecord.builder() + .user(user) + .emotionScore(dto.emotionScore()) + .content(dto.content()) + .visibility(dto.visibility()) // 생성 시에만 설정 + .build(); + } + + /** 수정 요청 DTO → Entity 반영 */ + public static void updateEntity( + EmotionRecord emotion, + EmotionRequestDto.EmotionUpdateRequestDto dto + ) { + emotion.update( + dto.emotionScore(), + dto.content() + ); + } + + /** Entity → 생성 응답 DTO */ + public static EmotionResponseDto.EmotionCreateResponseDto toCreateResponse( + EmotionRecord emotion + ) { + return new EmotionResponseDto.EmotionCreateResponseDto(emotion.getId()); + } + + /** Entity → 수정 응답 DTO */ + public static EmotionResponseDto.EmotionUpdateResponseDto toUpdateResponse( + EmotionRecord emotion + ) { + return new EmotionResponseDto.EmotionUpdateResponseDto(emotion.getId()); + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/request/EmotionRequestDto.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/request/EmotionRequestDto.java new file mode 100644 index 0000000..07e044d --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/request/EmotionRequestDto.java @@ -0,0 +1,28 @@ +package com.cokerthon.Team3_Backend.domain.emotion.dto.request; + +import com.cokerthon.Team3_Backend.domain.emotion.enums.Visibility; +import io.swagger.v3.oas.annotations.media.Schema; + +public class EmotionRequestDto { + + public record EmotionCreateRequestDto( + @Schema(description = "감정 점수 (1~5)", example = "4") + int emotionScore, + + @Schema(description = "감정 내용", example = "오늘은 기분이 좋다.") + String content, + + @Schema(description = "공개 여부", example = "PUBLIC") + Visibility visibility + ) { + } + + public record EmotionUpdateRequestDto( + @Schema(description = "감정 점수 (1~5)", example = "4") + int emotionScore, + + @Schema(description = "감정 내용", example = "오늘은 기분이 좋다.") + String content + ) { + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionResponseDto.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionResponseDto.java new file mode 100644 index 0000000..696daff --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionResponseDto.java @@ -0,0 +1,14 @@ +package com.cokerthon.Team3_Backend.domain.emotion.dto.response; + +public class EmotionResponseDto { + + public record EmotionCreateResponseDto( + Long emotionId + ) { + } + + public record EmotionUpdateResponseDto( + Long emotionId + ) { + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/entity/EmotionRecord.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/entity/EmotionRecord.java index 91e58e5..d9d3d27 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/entity/EmotionRecord.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/entity/EmotionRecord.java @@ -1,6 +1,6 @@ package com.cokerthon.Team3_Backend.domain.emotion.entity; -import com.cokerthon.Team3_Backend.domain.emotion.enums.Visiblity; +import com.cokerthon.Team3_Backend.domain.emotion.enums.Visibility; import com.cokerthon.Team3_Backend.domain.user.entity.User; import com.cokerthon.Team3_Backend.global.entity.BaseEntity; import jakarta.persistence.*; @@ -44,5 +44,21 @@ public class EmotionRecord extends BaseEntity { /** 공개 여부 */ @Enumerated(EnumType.STRING) @Column(nullable = false) - private Visiblity visibility; + private Visibility visibility; + + + // == 비즈니스 로직 == // + + /** 감정 수정 */ + public void update(int emotionScore, String content) { + this.emotionScore = emotionScore; + this.content = content; + } + + /** 작성자 검증 */ + public void validateOwner(User user) { + if (!this.user.getId().equals(user.getId())) { + throw new IllegalStateException("작성자만 수정/삭제할 수 있습니다."); + } + } } diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/enums/Visiblity.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/enums/Visibility.java similarity index 77% rename from src/main/java/com/cokerthon/Team3_Backend/domain/emotion/enums/Visiblity.java rename to src/main/java/com/cokerthon/Team3_Backend/domain/emotion/enums/Visibility.java index b53c1db..e781711 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/enums/Visiblity.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/enums/Visibility.java @@ -1,6 +1,6 @@ package com.cokerthon.Team3_Backend.domain.emotion.enums; -public enum Visiblity { +public enum Visibility { PUBLIC, PRIVATE } diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/EmotionException.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/EmotionException.java new file mode 100644 index 0000000..7391ccc --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/EmotionException.java @@ -0,0 +1,10 @@ +package com.cokerthon.Team3_Backend.domain.emotion.exception; + +import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseErrorCode; +import com.cokerthon.Team3_Backend.global.exception.GeneralException; + +public class EmotionException extends GeneralException { + public EmotionException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionErrorCode.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionErrorCode.java new file mode 100644 index 0000000..c1275ea --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionErrorCode.java @@ -0,0 +1,23 @@ +package com.cokerthon.Team3_Backend.domain.emotion.exception.code; + +import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum EmotionErrorCode implements BaseErrorCode { + + INVALID_EMOTION_SCORE(HttpStatus.BAD_REQUEST, "EMOTION_400_1", "감정 점수는 1~5 사이여야 합니다."), + + EMOTION_FORBIDDEN(HttpStatus.FORBIDDEN, "EMOTION_403_1", "작성자만 수정/삭제할 수 있습니다."), + + EMOTION_NOT_FOUND(HttpStatus.NOT_FOUND, "EMOTION_404_1", "감정 기록이 존재하지 않습니다."), + + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionSuccessCode.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionSuccessCode.java new file mode 100644 index 0000000..9bfd11a --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionSuccessCode.java @@ -0,0 +1,22 @@ +package com.cokerthon.Team3_Backend.domain.emotion.exception.code; + +import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum EmotionSuccessCode implements BaseSuccessCode { + + EMOTION_CREATED(HttpStatus.OK, "EMOTION_200_1", "감정 기록 생성 성공"), + + EMOTION_UPDATED(HttpStatus.OK, "EMOTION_200_2", "감정 기록 수정 성공"), + + EMOTION_DELETED(HttpStatus.OK, "EMOTION_200_3", "감정 기록 삭제 성공"), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/repository/EmotionRepository.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/repository/EmotionRepository.java new file mode 100644 index 0000000..bcca1b5 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/repository/EmotionRepository.java @@ -0,0 +1,8 @@ +package com.cokerthon.Team3_Backend.domain.emotion.repository; + +import com.cokerthon.Team3_Backend.domain.emotion.entity.EmotionRecord; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EmotionRepository extends JpaRepository { + +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/command/EmotionCommandService.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/command/EmotionCommandService.java new file mode 100644 index 0000000..1a0f08f --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/command/EmotionCommandService.java @@ -0,0 +1,103 @@ +package com.cokerthon.Team3_Backend.domain.emotion.service.command; + +import com.cokerthon.Team3_Backend.domain.emotion.converter.EmotionConverter; +import com.cokerthon.Team3_Backend.domain.emotion.dto.request.EmotionRequestDto; +import com.cokerthon.Team3_Backend.domain.emotion.entity.EmotionRecord; +import com.cokerthon.Team3_Backend.domain.emotion.exception.EmotionException; +import com.cokerthon.Team3_Backend.domain.emotion.exception.code.EmotionErrorCode; +import com.cokerthon.Team3_Backend.domain.emotion.repository.EmotionRepository; +import com.cokerthon.Team3_Backend.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class EmotionCommandService { + + private final EmotionRepository emotionRepository; + + /** + * 감정 기록 생성 + * + * @param user 사용자 엔티티 + * @param requestDto 감정 기록 생성 요청 DTO + * @return 생성된 감정 기록 엔티티 + */ + public EmotionRecord createEmotion( + User user, + EmotionRequestDto.EmotionCreateRequestDto requestDto + ) { + validateScore(requestDto.emotionScore()); + + EmotionRecord emotion = + EmotionConverter.toEntity(user, requestDto); + + return emotionRepository.save(emotion); + } + + + /** + * 감정 기록 수정 + * + * @param emotionId 감정 기록 ID + * @param user 사용자 엔티티 + * @param requestDto 감정 기록 수정 요청 DTO + */ + public void updateEmotion( + Long emotionId, + User user, + EmotionRequestDto.EmotionUpdateRequestDto requestDto + ) { + EmotionRecord emotion = emotionRepository.findById(emotionId) + .orElseThrow(() -> + new EmotionException(EmotionErrorCode.EMOTION_NOT_FOUND) + ); + + validateOwner(emotion, user); + validateScore(requestDto.emotionScore()); + + emotion.update( + requestDto.emotionScore(), + requestDto.content() + ); + } + + + /** + * 감정 기록 삭제 + * + * @param emotionId 감정 기록 ID + * @param user 사용자 엔티티 + */ + public void deleteEmotion( + Long emotionId, + User user + ) { + EmotionRecord emotion = emotionRepository.findById(emotionId) + .orElseThrow(() -> + new EmotionException(EmotionErrorCode.EMOTION_NOT_FOUND) + ); + + validateOwner(emotion, user); + emotionRepository.delete(emotion); + } + + + // ===== private ===== // + + /** 감정 점수 유효성 검사 */ + private void validateScore(int score) { + if (score < 1 || score > 5) { + throw new EmotionException(EmotionErrorCode.INVALID_EMOTION_SCORE); + } + } + + /** 작성자 검증 */ + private void validateOwner(EmotionRecord emotion, User user) { + if (!emotion.getUser().getId().equals(user.getId())) { + throw new EmotionException(EmotionErrorCode.EMOTION_FORBIDDEN); + } + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/query/EmotionQueryService.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/query/EmotionQueryService.java new file mode 100644 index 0000000..0c12fb4 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/query/EmotionQueryService.java @@ -0,0 +1,4 @@ +package com.cokerthon.Team3_Backend.domain.emotion.service.query; + +public class EmotionQueryService { +}