Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.closetnangam.be.domain.outfit.controller;

import com.closetnangam.be.domain.outfit.dto.request.OutfitCreateRequest;
import com.closetnangam.be.domain.outfit.dto.response.OutfitBookResponse;
import com.closetnangam.be.domain.outfit.dto.response.OutfitResponse;
import com.closetnangam.be.domain.outfit.service.OutfitService;
import com.closetnangam.be.global.auth.util.SecurityUtils;
import com.closetnangam.be.global.common.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Outfit", description = "코디북 API")
@RestController
@RequestMapping("/api/v1/outfit-books")
@RequiredArgsConstructor
public class OutfitController {

private final OutfitService outfitService;

@Operation(summary = "코디북 생성", description = "현재 로그인한 사용자의 코디북을 생성합니다.")
@PostMapping
public ResponseEntity<ApiResponse<OutfitBookResponse>> createBook() {
Long userId = SecurityUtils.getCurrentUserId();
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.ok(outfitService.createBook(userId)));
}

@Operation(summary = "코디북 조회", description = "현재 로그인한 사용자의 코디북과 코디 목록을 조회합니다.")
@GetMapping
public ResponseEntity<ApiResponse<OutfitBookResponse>> getMyBook() {
Long userId = SecurityUtils.getCurrentUserId();
return ResponseEntity.ok(ApiResponse.ok(outfitService.getBookByUserId(userId)));
}

@Operation(summary = "코디북 상세 조회", description = "코디북 ID로 상세 정보를 조회합니다.")
@GetMapping("/{bookId}")
public ResponseEntity<ApiResponse<OutfitBookResponse>> getBookById(@PathVariable Long bookId) {
Long userId = SecurityUtils.getCurrentUserId();
return ResponseEntity.ok(ApiResponse.ok(outfitService.getBookById(bookId, userId)));
}

@Operation(summary = "코디 등록", description = "코디북에 새로운 코디를 추가합니다.")
@PostMapping("/{bookId}/outfits")
public ResponseEntity<ApiResponse<OutfitResponse>> addOutfit(
@PathVariable Long bookId,
@Valid @RequestBody OutfitCreateRequest request
) {
Long userId = SecurityUtils.getCurrentUserId();
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.ok(outfitService.createOutfit(bookId, userId, request)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.closetnangam.be.domain.outfit.dto.request;

import com.closetnangam.be.domain.outfit.entity.Outfit;
import com.closetnangam.be.domain.outfit.entity.OutfitBook;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; // 이걸 추가해줘
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class OutfitCreateRequest {

@NotBlank
@Size(max = 100)
private String title;

@NotBlank
// description은 엔티티 제한이 따로 없으면 그대로 둬도 돼!
private String description;

@NotBlank
@Size(max = 500)
// @URL(message = "올바른 URL 형식이 아닙니다.") // 필요하다면 @URL 추가 가능!
private String thumbnailUrl;

@NotBlank
@Size(max = 50)
private String situation;

@NotBlank
@Size(max = 50)
private String season;

private Boolean favorite = Boolean.FALSE;

public Outfit toEntity(OutfitBook outfitBook) {
return Outfit.builder()
.outfitBook(outfitBook)
.title(this.title)
.description(this.description)
.thumbnailUrl(this.thumbnailUrl)
.situation(this.situation)
.season(this.season)
.favorite(Boolean.TRUE.equals(this.favorite))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.closetnangam.be.domain.outfit.dto.response;

import com.closetnangam.be.domain.outfit.entity.Outfit;
import com.closetnangam.be.domain.outfit.entity.OutfitBook;

import java.time.LocalDateTime;
import java.util.List;

public record OutfitBookResponse(
Long outfitBookId,
Long userId,
int outfitCount,
List<OutfitResponse> outfits,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {

public static OutfitBookResponse from(OutfitBook outfitBook, Long userId, List<Outfit> outfits) {
return new OutfitBookResponse(
outfitBook.getId(),
userId,
outfits.size(),
outfits.stream()
.map(OutfitResponse::from)
.toList(),
outfitBook.getCreatedAt(),
outfitBook.getUpdatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.closetnangam.be.domain.outfit.dto.response;

import com.closetnangam.be.domain.outfit.entity.Outfit;

import java.time.LocalDateTime;

public record OutfitResponse(
Long outfitId,
Long outfitBookId,
String title,
String description,
String thumbnailUrl,
String situation,
String season,
boolean favorite,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {

public static OutfitResponse from(Outfit outfit) {
return new OutfitResponse(
outfit.getOutfitId(),
outfit.getOutfitBook().getId(),
outfit.getTitle(),
outfit.getDescription(),
outfit.getThumbnailUrl(),
outfit.getSituation(),
outfit.getSeason(),
outfit.isFavorite(),
outfit.getCreatedAt(),
outfit.getUpdatedAt()
);
}
}
73 changes: 73 additions & 0 deletions src/main/java/com/closetnangam/be/domain/outfit/entity/Outfit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.closetnangam.be.domain.outfit.entity;

import com.closetnangam.be.global.common.entity.BaseEntity;
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 jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "outfits")
public class Outfit extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "outfit_id")
private Long outfitId;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "outfit_book_id", nullable = false)
private OutfitBook outfitBook;

@Column(nullable = false, length = 100)
private String title;

@Column(nullable = false, columnDefinition = "TEXT")
private String description;

@Column(name = "thumbnail_url", nullable = false, length = 500)
private String thumbnailUrl;

@Column(nullable = false, length = 50)
private String situation;

@Column(nullable = false, length = 50)
private String season;

@Column(nullable = false)
private boolean favorite;

@Builder
private Outfit(
OutfitBook outfitBook,
String title,
String description,
String thumbnailUrl,
String situation,
String season,
boolean favorite
) {
this.outfitBook = outfitBook;
this.title = title;
this.description = description;
this.thumbnailUrl = thumbnailUrl;
this.situation = situation;
this.season = season;
this.favorite = favorite;
}

public void markFavorite(boolean favorite) {
this.favorite = favorite;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.closetnangam.be.domain.outfit.entity;

import com.closetnangam.be.domain.user.entity.User;
import com.closetnangam.be.global.common.entity.BaseEntity;
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.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(
name = "outfit_books",
uniqueConstraints = @UniqueConstraint(name = "uk_outfit_books_user_id", columnNames = "user_id")
)
public class OutfitBook extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "outfit_book_id")
private Long id;

@OneToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false, unique = true)
private User user;

@Builder
private OutfitBook(User user) {
this.user = user;
}

public static OutfitBook create(User user) {
return OutfitBook.builder()
.user(user)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.closetnangam.be.domain.outfit.repository;

import com.closetnangam.be.domain.outfit.entity.OutfitBook;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface OutfitBookRepository extends JpaRepository<OutfitBook, Long> {

Optional<OutfitBook> findByUser_Id(Long userId);

@Query("""
select ob
from OutfitBook ob
where ob.id = :bookId
and ob.user.id = :userId
""")
Optional<OutfitBook> findByIdAndUserId(
@Param("bookId") Long bookId,
@Param("userId") Long userId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.closetnangam.be.domain.outfit.repository;

import com.closetnangam.be.domain.outfit.entity.Outfit;
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;

public interface OutfitRepository extends JpaRepository<Outfit, Long> {

@Query("""
select o
from Outfit o
join fetch o.outfitBook ob
where ob.id = :bookId
order by o.createdAt desc
""")
List<Outfit> findAllByOutfitBookId(@Param("bookId") Long bookId);
}
Loading
Loading