diff --git a/src/main/java/com/likelion/trendithon/domain/card/controller/ExperienceController.java b/src/main/java/com/likelion/trendithon/domain/card/controller/ExperienceController.java new file mode 100644 index 0000000..780d696 --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/controller/ExperienceController.java @@ -0,0 +1,60 @@ +package com.likelion.trendithon.domain.card.controller; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.trendithon.domain.card.dto.request.CreateExperienceRequest; +import com.likelion.trendithon.domain.card.dto.request.UpdateExperienceRequest; +import com.likelion.trendithon.domain.card.service.ExperienceService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/cards") +@Tag(name = "Card", description = "Card 관리 API") +public class ExperienceController { + + private ExperienceService experienceService; + + @Operation(summary = "[ 토큰 O | 카드 경험 ]", description = "다른 사용자가 생성한 카드 경험 등록") + @PostMapping("/experience") + public ResponseEntity createExperience( + @Parameter(description = "경험할 카드 ID") Long cardId, + @Parameter(description = "경험 내용") @RequestBody + CreateExperienceRequest createExperienceRequest, + HttpServletRequest httpServletRequest) { + return experienceService.createExperience(cardId, createExperienceRequest, httpServletRequest); + } + + @Operation(summary = "[ 토큰 O | 사용자 경험 조회 ]", description = "사용자가 현재 도전 중인 경험 조회") + @GetMapping("/experience") + public ResponseEntity getEnableExperience(HttpServletRequest httpServletRequest) { + return experienceService.getEnableExperience(httpServletRequest); + } + + @Operation(summary = "[ 토큰 O | 사용자 경험 수정 ]", description = "사용자가 현재 도전 중인 경험 종료일 변경") + @PutMapping("/experience") + public ResponseEntity updateExperience( + @Parameter(description = "경험 종료일") @RequestBody + UpdateExperienceRequest updateExperienceRequest, + HttpServletRequest httpServletRequest) { + return experienceService.updateExperience(updateExperienceRequest, httpServletRequest); + } + + @Operation(summary = "[ 토큰 O | 사용자 경험 포기 ]", description = "사용자가 현재 도전 중인 경험 포기") + @PutMapping("/experience/quit") + public ResponseEntity quitExperience(HttpServletRequest httpServletRequest) { + return experienceService.quitExperience(httpServletRequest); + } +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/dto/request/CreateExperienceRequest.java b/src/main/java/com/likelion/trendithon/domain/card/dto/request/CreateExperienceRequest.java new file mode 100644 index 0000000..77a4a8c --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/dto/request/CreateExperienceRequest.java @@ -0,0 +1,19 @@ +package com.likelion.trendithon.domain.card.dto.request; + +import java.time.LocalDate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class CreateExperienceRequest { + + @Schema(description = "카드 표지", example = "#000000") + private String cover; + + @Schema(description = "시작 날짜", example = "2025.01.01") + private LocalDate startDate; + + @Schema(description = "종료 날짜", example = "2025.01.01") + private LocalDate endDate; +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/dto/request/UpdateExperienceRequest.java b/src/main/java/com/likelion/trendithon/domain/card/dto/request/UpdateExperienceRequest.java new file mode 100644 index 0000000..f6f7b4e --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/dto/request/UpdateExperienceRequest.java @@ -0,0 +1,13 @@ +package com.likelion.trendithon.domain.card.dto.request; + +import java.time.LocalDate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class UpdateExperienceRequest { + + @Schema(description = "종료 날짜", example = "2025.01.01") + private LocalDate endDate; +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/dto/response/CardResponse.java b/src/main/java/com/likelion/trendithon/domain/card/dto/response/CardResponse.java index 2164e1f..e0ab60e 100644 --- a/src/main/java/com/likelion/trendithon/domain/card/dto/response/CardResponse.java +++ b/src/main/java/com/likelion/trendithon/domain/card/dto/response/CardResponse.java @@ -16,6 +16,6 @@ public class CardResponse { @Schema(description = "응답 메세지", example = "카드 조회에 성공하였습니다.") private String message; - @Schema(description = "카드") - private Card card; + @Schema(description = "조회한 카드 ID", example = "1") + private Long cardId; } diff --git a/src/main/java/com/likelion/trendithon/domain/card/dto/response/CreateExperienceResponse.java b/src/main/java/com/likelion/trendithon/domain/card/dto/response/CreateExperienceResponse.java new file mode 100644 index 0000000..67fa50a --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/dto/response/CreateExperienceResponse.java @@ -0,0 +1,22 @@ +package com.likelion.trendithon.domain.card.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class CreateExperienceResponse { + + @Schema(description = "카드 생성 결과", example = "true") + private boolean success; + + @Schema(description = "응답 메세지", example = "카드 생성에 성공하였습니다.") + private String message; + + @Schema(description = "카드 ID", example = "1") + private Long cardId; + + @Schema(description = "생성된 경험 ID", example = "1") + private Long experienceId; +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/dto/response/ExperienceResponse.java b/src/main/java/com/likelion/trendithon/domain/card/dto/response/ExperienceResponse.java new file mode 100644 index 0000000..f1f8cc3 --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/dto/response/ExperienceResponse.java @@ -0,0 +1,19 @@ +package com.likelion.trendithon.domain.card.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ExperienceResponse { + + @Schema(description = "경험 조회 결과", example = "true") + private boolean success; + + @Schema(description = "응답 메세지", example = "경험 조회에 성공하였습니다.") + private String message; + + @Schema(description = "조회한 경험 ID", example = "1") + private Long experienceId; +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/dto/response/UpdateExperienceResponse.java b/src/main/java/com/likelion/trendithon/domain/card/dto/response/UpdateExperienceResponse.java new file mode 100644 index 0000000..df6508b --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/dto/response/UpdateExperienceResponse.java @@ -0,0 +1,19 @@ +package com.likelion.trendithon.domain.card.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class UpdateExperienceResponse { + + @Schema(description = "경험 수정 결과", example = "true") + private boolean success; + + @Schema(description = "응답 메세지", example = "경험 수정에 성공하였습니다.") + private String message; + + @Schema(description = "수정된 경험 ID", example = "1") + private Long experienceId; +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/entity/Card.java b/src/main/java/com/likelion/trendithon/domain/card/entity/Card.java index 61a5105..03538c0 100644 --- a/src/main/java/com/likelion/trendithon/domain/card/entity/Card.java +++ b/src/main/java/com/likelion/trendithon/domain/card/entity/Card.java @@ -44,5 +44,5 @@ public class Card { private String cover; @OneToMany(mappedBy = "card", cascade = CascadeType.ALL, orphanRemoval = true) - private List userCardList = new ArrayList<>(); + private List experienceList = new ArrayList<>(); } diff --git a/src/main/java/com/likelion/trendithon/domain/card/entity/Experience.java b/src/main/java/com/likelion/trendithon/domain/card/entity/Experience.java new file mode 100644 index 0000000..481c966 --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/entity/Experience.java @@ -0,0 +1,58 @@ +package com.likelion.trendithon.domain.card.entity; + +import java.time.LocalDate; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import com.likelion.trendithon.domain.user.entity.User; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class Experience { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long experienceId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "login_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "card_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Card card; + + @Column(name = "state", nullable = false) + private boolean state; + + @Column(name = "cover", nullable = false) + private String cover; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date", nullable = false) + private LocalDate endDate; +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/repository/ExperienceRepository.java b/src/main/java/com/likelion/trendithon/domain/card/repository/ExperienceRepository.java new file mode 100644 index 0000000..05f328a --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/repository/ExperienceRepository.java @@ -0,0 +1,11 @@ +package com.likelion.trendithon.domain.card.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.likelion.trendithon.domain.card.entity.Experience; + +public interface ExperienceRepository extends JpaRepository { + Optional findByStateAndUserLoginId(boolean state, String loginId); +} diff --git a/src/main/java/com/likelion/trendithon/domain/card/service/CardService.java b/src/main/java/com/likelion/trendithon/domain/card/service/CardService.java index 3363a1e..a5f5938 100644 --- a/src/main/java/com/likelion/trendithon/domain/card/service/CardService.java +++ b/src/main/java/com/likelion/trendithon/domain/card/service/CardService.java @@ -31,9 +31,9 @@ @Slf4j public class CardService { - private CardRepository cardRepository; - private UserRepository userRepository; - private JwtUtil jwtUtil; + private final CardRepository cardRepository; + private final UserRepository userRepository; + private final JwtUtil jwtUtil; // 카드 생성 @Transactional @@ -79,6 +79,33 @@ public ResponseEntity createCard( } } + // 카드 한 장 조회 + @Transactional + public ResponseEntity getCardById(Long id) { + + try { + Card card = + cardRepository + .findById(id) + .orElseThrow(() -> new IllegalArgumentException("카드를 찾을 수 없습니다.")); + log.info("[GET /api/cards/{}] 특정 카드 조회 성공 - 조회한 카드 ID: {}", id, id); + return ResponseEntity.ok( + CardResponse.builder() + .success(true) + .message("카드 조회에 성공하였습니다.") + .cardId(card.getCardId()) + .build()); + } catch (IllegalArgumentException e) { + log.error("[GET /api/cards/{}] 특정 카드 조회 실패", id); + return ResponseEntity.ok( + CardResponse.builder().success(false).message(e.getMessage()).build()); + } catch (Exception e) { + log.error("[GET /api/cards/{}] 특정 카드 조회 실패 - 에러: {}", id, e.getMessage()); + return ResponseEntity.ok( + CardResponse.builder().success(false).message("카드 조회 중 오류가 발생하였습니다.").build()); + } + } + // 랜덤 카드 세 장 조회 @Transactional public ResponseEntity getRandomCards() { @@ -115,31 +142,7 @@ public ResponseEntity getRandomCards() { .build()); } } - - // 카드 한 장 조회 - @Transactional - public ResponseEntity getCardById(Long id) { - - try { - Card card = - cardRepository - .findById(id) - .orElseThrow(() -> new IllegalArgumentException("카드를 찾을 수 없습니다.")); - log.info("[GET /api/cards/{}] 특정 카드 조회 성공 - 조회한 카드 ID: {}", id, id); - return ResponseEntity.ok( - CardResponse.builder().success(true).message("카드 조회에 성공하였습니다.").card(card).build()); - } catch (IllegalArgumentException e) { - log.error("[GET /api/cards/{}] 특정 카드 조회 실패", id); - return ResponseEntity.ok( - CardResponse.builder().success(false).message(e.getMessage()).build()); - } catch (Exception e) { - log.error("[GET /api/cards/{}] 특정 카드 조회 실패 - 에러: {}", id, e.getMessage()); - return ResponseEntity.ok( - CardResponse.builder().success(false).message("카드 조회 중 오류가 발생하였습니다.").build()); - } - } - - // 모든 카드 조회 + @Transactional public ResponseEntity getAllCards() { try { diff --git a/src/main/java/com/likelion/trendithon/domain/card/service/ExperienceService.java b/src/main/java/com/likelion/trendithon/domain/card/service/ExperienceService.java new file mode 100644 index 0000000..8fd2a2b --- /dev/null +++ b/src/main/java/com/likelion/trendithon/domain/card/service/ExperienceService.java @@ -0,0 +1,194 @@ +package com.likelion.trendithon.domain.card.service; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.likelion.trendithon.domain.card.dto.request.CreateExperienceRequest; +import com.likelion.trendithon.domain.card.dto.request.UpdateExperienceRequest; +import com.likelion.trendithon.domain.card.dto.response.CreateExperienceResponse; +import com.likelion.trendithon.domain.card.dto.response.ExperienceResponse; +import com.likelion.trendithon.domain.card.entity.Card; +import com.likelion.trendithon.domain.card.entity.Experience; +import com.likelion.trendithon.domain.card.repository.CardRepository; +import com.likelion.trendithon.domain.card.repository.ExperienceRepository; +import com.likelion.trendithon.domain.user.entity.User; +import com.likelion.trendithon.domain.user.repository.UserRepository; +import com.likelion.trendithon.global.auth.JwtUtil; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@AllArgsConstructor +@Slf4j +public class ExperienceService { + + private final ExperienceRepository experienceRepository; + private final CardRepository cardRepository; + private final UserRepository userRepository; + private final JwtUtil jwtUtil; + + // 경험 생성 + @Transactional + public ResponseEntity createExperience( + Long cardId, + CreateExperienceRequest createExperienceRequest, + HttpServletRequest httpServletRequest) { + + try { + String loginId = + jwtUtil.extractLoginId(httpServletRequest.getHeader("Authorization").substring(7)); + User user = + userRepository + .findByLoginId(loginId) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + Card card = + cardRepository + .findById(cardId) + .orElseThrow(() -> new IllegalArgumentException("카드를 찾을 수 없습니다.")); + + Experience experience = + Experience.builder() + .user(user) + .card(card) + .state(true) + .cover(createExperienceRequest.getCover()) + .startDate(createExperienceRequest.getStartDate()) + .endDate(createExperienceRequest.getEndDate()) + .build(); + + experienceRepository.save(experience); + + user.setState(true); + userRepository.save(user); + + log.info( + "[POST /api/cards/] 경험 생성 성공 - 생성한 사용자 ID: {}, 카드 ID: {}, 경험 ID: {}", + user.getLoginId(), + card.getCardId(), + experience.getExperienceId()); + return ResponseEntity.ok( + CreateExperienceResponse.builder() + .success(true) + .message("경험 생성에 성공하였습니다.") + .cardId(card.getCardId()) + .experienceId(experience.getExperienceId()) + .build()); + } catch (Exception e) { + log.error("[POST /api/cards/create] 경험 생성 실패 - 에러: {}", e.getMessage()); + return ResponseEntity.ok( + CreateExperienceResponse.builder().success(false).message("경험 생성에 실패하였습니다.").build()); + } + } + + // 사용자가 도전 중인 경험 조회 + @Transactional + public ResponseEntity getEnableExperience( + HttpServletRequest httpServletRequest) { + + try { + String loginId = + jwtUtil.extractLoginId(httpServletRequest.getHeader("Authorization").substring(7)); + User user = + userRepository + .findByLoginId(loginId) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + Experience experience = + experienceRepository + .findByStateAndUserLoginId(true, user.getLoginId()) + .orElseThrow(() -> new IllegalArgumentException("사용자 경험이 존재하지 않습니다.")); + + return ResponseEntity.ok( + ExperienceResponse.builder() + .success(true) + .message("경험 조회에 성공하였습니다.") + .experienceId(experience.getExperienceId()) + .build()); + } catch (IllegalArgumentException e) { + log.error("[GET /api/cards/experience] 특정 경험 조회 실패"); + return ResponseEntity.ok( + ExperienceResponse.builder().success(false).message(e.getMessage()).build()); + } catch (Exception e) { + log.error("[GET /api/cards/experience] 특정 경험 조회 실패 - 에러: {}", e.getMessage()); + return ResponseEntity.ok( + ExperienceResponse.builder().success(false).message("경험 조회 중 오류가 발생하였습니다.").build()); + } + } + + // 사용자가 도전 중인 경험 수정 + @Transactional + public ResponseEntity updateExperience( + UpdateExperienceRequest updateExperienceRequest, HttpServletRequest httpServletRequest) { + + try { + String loginId = + jwtUtil.extractLoginId(httpServletRequest.getHeader("Authorization").substring(7)); + User user = + userRepository + .findByLoginId(loginId) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + Experience experience = + experienceRepository + .findByStateAndUserLoginId(true, user.getLoginId()) + .orElseThrow(() -> new IllegalArgumentException("사용자 경험이 존재하지 않습니다.")); + + experience.setEndDate(updateExperienceRequest.getEndDate()); + experienceRepository.save(experience); + + return ResponseEntity.ok( + ExperienceResponse.builder() + .success(true) + .message("경험 수정에 성공하였습니다.") + .experienceId(experience.getExperienceId()) + .build()); + } catch (IllegalArgumentException e) { + log.error("[PUT /api/cards/experience] 특정 경험 수정 실패"); + return ResponseEntity.ok( + ExperienceResponse.builder().success(false).message(e.getMessage()).build()); + } catch (Exception e) { + log.error("[PUT /api/cards/experience] 특정 경험 수정 실패 - 에러: {}", e.getMessage()); + return ResponseEntity.ok( + ExperienceResponse.builder().success(false).message("경험 수정 중 오류가 발생하였습니다.").build()); + } + } + + // 사용자가 도전 중인 경험 포기 + @Transactional + public ResponseEntity quitExperience(HttpServletRequest httpServletRequest) { + + try { + String loginId = + jwtUtil.extractLoginId(httpServletRequest.getHeader("Authorization").substring(7)); + User user = + userRepository + .findByLoginId(loginId) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + Experience experience = + experienceRepository + .findByStateAndUserLoginId(true, user.getLoginId()) + .orElseThrow(() -> new IllegalArgumentException("사용자 경험이 존재하지 않습니다.")); + + experience.setState(false); + experienceRepository.save(experience); + + return ResponseEntity.ok( + ExperienceResponse.builder() + .success(true) + .message("경험 수정에 성공하였습니다.") + .experienceId(experience.getExperienceId()) + .build()); + } catch (IllegalArgumentException e) { + log.error("[PUT /api/cards/experience] 특정 경험 수정 실패"); + return ResponseEntity.ok( + ExperienceResponse.builder().success(false).message(e.getMessage()).build()); + } catch (Exception e) { + log.error("[PUT /api/cards/experience] 특정 경험 수정 실패 - 에러: {}", e.getMessage()); + return ResponseEntity.ok( + ExperienceResponse.builder().success(false).message("경험 수정 중 오류가 발생하였습니다.").build()); + } + } +} diff --git a/src/main/java/com/likelion/trendithon/domain/user/entity/User.java b/src/main/java/com/likelion/trendithon/domain/user/entity/User.java index 18b295b..5246639 100644 --- a/src/main/java/com/likelion/trendithon/domain/user/entity/User.java +++ b/src/main/java/com/likelion/trendithon/domain/user/entity/User.java @@ -10,7 +10,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; -import com.likelion.trendithon.domain.card.entity.UserCard; +import com.likelion.trendithon.domain.card.entity.Experience; import com.likelion.trendithon.global.common.BaseTimeEntity; import lombok.AllArgsConstructor; @@ -47,5 +47,5 @@ public class User extends BaseTimeEntity { private String userRole; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - private List userCardList = new ArrayList<>(); + private List experienceList = new ArrayList<>(); }