diff --git a/src/main/java/in/koreatech/koin/admin/coopShop/controller/AdminCoopShopApi.java b/src/main/java/in/koreatech/koin/admin/coopShop/controller/AdminCoopShopApi.java new file mode 100644 index 0000000000..91fc2de829 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/coopShop/controller/AdminCoopShopApi.java @@ -0,0 +1,59 @@ +package in.koreatech.koin.admin.coopShop.controller; + +import static in.koreatech.koin.domain.user.model.UserType.ADMIN; +import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; + +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import in.koreatech.koin.admin.coopShop.dto.AdminCoopShopResponse; +import in.koreatech.koin.admin.coopShop.dto.AdminCoopShopsResponse; +import in.koreatech.koin.domain.coopshop.dto.CoopShopResponse; +import in.koreatech.koin.global.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@RequestMapping("/admin/coopshop") +@Tag(name = "(ADMIN) AdminCoopShop : 생협 매장 정보", description = "생협 매장 정보 조회 페이지") +public interface AdminCoopShopApi { + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "모든 생협 매장 정보 조회") + @GetMapping + ResponseEntity getCoopsShops( + @RequestParam(name = "page", defaultValue = "1") Integer page, + @RequestParam(name = "limit", defaultValue = "10", required = false) Integer limit, + @RequestParam(name = "is_deleted", defaultValue = "false") Boolean isDeleted, + @Auth(permit = {ADMIN}) Integer adminId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "특정 생협 매장 정보 조회") + @GetMapping("/{coopShopId}") + ResponseEntity getCoopShop( + @Auth(permit = {ADMIN}) Integer adminId, + @Parameter(in = PATH) @PathVariable Integer coopShopId + ); +} diff --git a/src/main/java/in/koreatech/koin/admin/coopShop/controller/AdminCoopShopController.java b/src/main/java/in/koreatech/koin/admin/coopShop/controller/AdminCoopShopController.java new file mode 100644 index 0000000000..053d57bb04 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/coopShop/controller/AdminCoopShopController.java @@ -0,0 +1,45 @@ +package in.koreatech.koin.admin.coopShop.controller; + +import static in.koreatech.koin.domain.user.model.UserType.ADMIN; + +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import in.koreatech.koin.admin.coopShop.dto.AdminCoopShopResponse; +import in.koreatech.koin.admin.coopShop.dto.AdminCoopShopsResponse; +import in.koreatech.koin.admin.coopShop.service.AdminCoopShopService; +import in.koreatech.koin.global.auth.Auth; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/admin/coopshop") +@RequiredArgsConstructor +public class AdminCoopShopController implements AdminCoopShopApi { + + private final AdminCoopShopService adminCoopShopService; + + @GetMapping + public ResponseEntity getCoopsShops( + @RequestParam(name = "page", defaultValue = "1") Integer page, + @RequestParam(name = "limit", defaultValue = "10", required = false) Integer limit, + @RequestParam(name = "is_deleted", defaultValue = "false") Boolean isDeleted, + @Auth(permit = {ADMIN}) Integer adminId + ) { + AdminCoopShopsResponse coopShops = adminCoopShopService.getCoopsShops(page, limit, isDeleted); + return ResponseEntity.ok(coopShops); + } + + @GetMapping("/{coopShopId}") + public ResponseEntity getCoopShop( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable Integer coopShopId + ) { + AdminCoopShopResponse coopShop = adminCoopShopService.getCoopShop(coopShopId); + return ResponseEntity.ok(coopShop); + } + +} diff --git a/src/main/java/in/koreatech/koin/admin/coopShop/dto/AdminCoopShopResponse.java b/src/main/java/in/koreatech/koin/admin/coopShop/dto/AdminCoopShopResponse.java new file mode 100644 index 0000000000..00f8897297 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/coopShop/dto/AdminCoopShopResponse.java @@ -0,0 +1,73 @@ +package in.koreatech.koin.admin.coopShop.dto; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.coopshop.model.CoopOpen; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record AdminCoopShopResponse( + @Schema(example = "1", description = "생협 매장 고유 id", requiredMode = REQUIRED) + Integer id, + + @Schema(example = "세탁소", description = "생협 매장 이름", requiredMode = REQUIRED) + String name, + + @Schema(description = "요일별 생협 매장 운영시간") + List opens, + + @Schema(example = "041-560-1234", description = "생협 매장 연락처", requiredMode = REQUIRED) + String phone, + + @Schema(example = "학생식당 2층", description = "생협 매장 위치", requiredMode = REQUIRED) + String location, + + @Schema(example = "공휴일 휴무", description = "생협 매장 특이사항") + String remarks +) { + + public static AdminCoopShopResponse from(CoopShop coopShop) { + return new AdminCoopShopResponse( + coopShop.getId(), + coopShop.getName(), + coopShop.getCoopOpens().stream() + .map(InnerCoopOpens::from) + .toList(), + coopShop.getPhone(), + coopShop.getLocation(), + coopShop.getRemarks() + ); + } + + @JsonNaming(value = SnakeCaseStrategy.class) + public record InnerCoopOpens( + + @Schema(example = "평일", description = "생협 매장 운영시간 요일", requiredMode = REQUIRED) + String dayOfWeek, + + @Schema(example = "아침", description = "생협 매장 운영시간 타입", requiredMode = REQUIRED) + String type, + + @Schema(example = "09:00", description = "생협 매장 오픈 시간", requiredMode = REQUIRED) + String openTime, + + @Schema(example = "18:00", description = "생협 매장 마감 시간", requiredMode = REQUIRED) + String closeTime + ) { + + public static InnerCoopOpens from(CoopOpen coopOpen) { + return new InnerCoopOpens( + coopOpen.getDayOfWeek(), + coopOpen.getType(), + coopOpen.getOpenTime(), + coopOpen.getCloseTime() + ); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/coopShop/dto/AdminCoopShopsResponse.java b/src/main/java/in/koreatech/koin/admin/coopShop/dto/AdminCoopShopsResponse.java new file mode 100644 index 0000000000..8acfdf8f67 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/coopShop/dto/AdminCoopShopsResponse.java @@ -0,0 +1,42 @@ +package in.koreatech.koin.admin.coopShop.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import org.springframework.data.domain.Page; + +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import in.koreatech.koin.global.model.Criteria; +import io.swagger.v3.oas.annotations.media.Schema; + +public record AdminCoopShopsResponse( + + @Schema(description = "조건에 해당하는 총 생협 매장의 수", example = "20", requiredMode = REQUIRED) + Long totalCount, + + @Schema(description = "조건에 해당하는 생협 매장 중 현재 페이지에서 조회된 수", example = "10", requiredMode = REQUIRED) + Integer currentCount, + + @Schema(description = "조건에 해당하는 생협 매장을 조회할 수 있는 최대 페이지", example = "6", requiredMode = REQUIRED) + Integer totalPage, + + @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) + Integer currentPage, + + @Schema(description = "생협 매장 정보 리스트", requiredMode = REQUIRED) + List coopShops +) { + public static AdminCoopShopsResponse of(Page pagedResult, Criteria criteria) { + return new AdminCoopShopsResponse( + pagedResult.getTotalElements(), + pagedResult.getContent().size(), + pagedResult.getTotalPages(), + criteria.getPage() + 1, + pagedResult.getContent() + .stream() + .map(AdminCoopShopResponse::from) + .toList() + ); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/coopShop/repository/AdminCoopShopRepository.java b/src/main/java/in/koreatech/koin/admin/coopShop/repository/AdminCoopShopRepository.java new file mode 100644 index 0000000000..3ba09f9640 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/coopShop/repository/AdminCoopShopRepository.java @@ -0,0 +1,36 @@ +package in.koreatech.koin.admin.coopShop.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.domain.coopshop.exception.CoopShopNotFoundException; +import in.koreatech.koin.domain.coopshop.model.CoopShop; + +public interface AdminCoopShopRepository extends Repository { + + Page findAllByIsDeleted(boolean isDeleted, Pageable pageable); + + Integer countAllByIsDeleted(boolean isDeleted); + + List findAll(); + + CoopShop save(CoopShop coopShop); + + Optional findByName(String name); + + default CoopShop getByName(String name) { + return findByName(name) + .orElseThrow(() -> CoopShopNotFoundException.withDetail("coop_name : " + name)); + } + + Optional findById(Integer id); + + default CoopShop getById(Integer id) { + return findById(id) + .orElseThrow(() -> CoopShopNotFoundException.withDetail("coop_id : " + id)); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/coopShop/service/AdminCoopShopService.java b/src/main/java/in/koreatech/koin/admin/coopShop/service/AdminCoopShopService.java new file mode 100644 index 0000000000..e4b173881d --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/coopShop/service/AdminCoopShopService.java @@ -0,0 +1,38 @@ +package in.koreatech.koin.admin.coopShop.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import in.koreatech.koin.admin.coopShop.dto.AdminCoopShopResponse; +import in.koreatech.koin.admin.coopShop.dto.AdminCoopShopsResponse; +import in.koreatech.koin.admin.coopShop.repository.AdminCoopShopRepository; +import in.koreatech.koin.domain.coopshop.dto.CoopShopResponse; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import in.koreatech.koin.global.model.Criteria; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AdminCoopShopService { + + private final AdminCoopShopRepository adminCoopShopRepository; + + public AdminCoopShopsResponse getCoopsShops(Integer page, Integer limit, Boolean isDeleted) { + Integer total = adminCoopShopRepository.countAllByIsDeleted(isDeleted); + + Criteria criteria = Criteria.of(page, limit, total); + PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), + Sort.by(Sort.Direction.ASC, "id")); + + Page result = adminCoopShopRepository.findAllByIsDeleted(isDeleted, pageRequest); + + return AdminCoopShopsResponse.of(result, criteria); + } + + public AdminCoopShopResponse getCoopShop(Integer id) { + CoopShop coopShop = adminCoopShopRepository.getById(id); + return AdminCoopShopResponse.from(coopShop); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java index a5425a7c58..0adaf55f77 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java +++ b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java @@ -22,7 +22,7 @@ import jakarta.validation.Valid; @RequestMapping("/coop") -@Tag(name = "(OWNER) Coop Dining : 영양사 식단", description = "영양사 식단 페이지") +@Tag(name = "(COOP) Coop Dining : 영양사 식단", description = "영양사 식단 페이지") public interface CoopApi { @ApiResponses( diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/controller/CoopShopApi.java b/src/main/java/in/koreatech/koin/domain/coopshop/controller/CoopShopApi.java new file mode 100644 index 0000000000..c9739a39e0 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/controller/CoopShopApi.java @@ -0,0 +1,51 @@ +package in.koreatech.koin.domain.coopshop.controller; + +import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; + +import java.util.List; + +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.RequestMapping; + +import in.koreatech.koin.domain.coopshop.dto.CoopShopResponse; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@RequestMapping("/coopshop") +@Tag(name = "(NORMAL) CoopShop : 생협 매장 정보", description = "생협 매장 정보 조회 페이지") +public interface CoopShopApi { + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "모든 생협 매장 정보 조회") + @GetMapping + ResponseEntity> getCoopsShops(); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "특정 생협 매장 정보 조회") + @GetMapping("/{coopShopId}") + ResponseEntity getCoopShop( + @Parameter(in = PATH) @PathVariable Integer coopShopId + ); +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/controller/CoopShopController.java b/src/main/java/in/koreatech/koin/domain/coopshop/controller/CoopShopController.java new file mode 100644 index 0000000000..9f43b9bbe1 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/controller/CoopShopController.java @@ -0,0 +1,35 @@ +package in.koreatech.koin.domain.coopshop.controller; + +import java.util.List; + +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import in.koreatech.koin.domain.coopshop.dto.CoopShopResponse; +import in.koreatech.koin.domain.coopshop.service.CoopShopService; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/coopshop") +@RequiredArgsConstructor +public class CoopShopController implements CoopShopApi { + + private final CoopShopService coopShopService; + + @GetMapping + public ResponseEntity> getCoopsShops() { + List coopShops = coopShopService.getCoopShops(); + return ResponseEntity.ok(coopShops); + } + + @GetMapping("/{coopShopId}") + public ResponseEntity getCoopShop( + @PathVariable Integer coopShopId + ) { + CoopShopResponse coopShop = coopShopService.getCoopShop(coopShopId); + return ResponseEntity.ok(coopShop); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/dto/CoopShopResponse.java b/src/main/java/in/koreatech/koin/domain/coopshop/dto/CoopShopResponse.java new file mode 100644 index 0000000000..17eb468802 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/dto/CoopShopResponse.java @@ -0,0 +1,73 @@ +package in.koreatech.koin.domain.coopshop.dto; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.coopshop.model.CoopOpen; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record CoopShopResponse( + @Schema(example = "1", description = "생협 매장 고유 id", requiredMode = REQUIRED) + Integer id, + + @Schema(example = "세탁소", description = "생협 매장 이름", requiredMode = REQUIRED) + String name, + + @Schema(description = "요일별 생협 매장 운영시간") + List opens, + + @Schema(example = "041-560-1234", description = "생협 매장 연락처", requiredMode = REQUIRED) + String phone, + + @Schema(example = "학생식당 2층", description = "생협 매장 위치", requiredMode = REQUIRED) + String location, + + @Schema(example = "공휴일 휴무", description = "생협 매장 특이사항") + String remarks +) { + + public static CoopShopResponse from(CoopShop coopShop) { + return new CoopShopResponse( + coopShop.getId(), + coopShop.getName(), + coopShop.getCoopOpens().stream() + .map(InnerCoopOpens::from) + .toList(), + coopShop.getPhone(), + coopShop.getLocation(), + coopShop.getRemarks() + ); + } + + @JsonNaming(value = SnakeCaseStrategy.class) + public record InnerCoopOpens( + + @Schema(example = "평일", description = "생협 매장 운영시간 요일", requiredMode = REQUIRED) + String dayOfWeek, + + @Schema(example = "아침", description = "생협 매장 운영시간 타입", requiredMode = REQUIRED) + String type, + + @Schema(example = "09:00", description = "생협 매장 오픈 시간", requiredMode = REQUIRED) + String openTime, + + @Schema(example = "18:00", description = "생협 매장 마감 시간", requiredMode = REQUIRED) + String closeTime + ) { + + public static InnerCoopOpens from(CoopOpen coopOpen) { + return new InnerCoopOpens( + coopOpen.getDayOfWeek(), + coopOpen.getType(), + coopOpen.getOpenTime(), + coopOpen.getCloseTime() + ); + } + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/exception/CoopShopNotFoundException.java b/src/main/java/in/koreatech/koin/domain/coopshop/exception/CoopShopNotFoundException.java new file mode 100644 index 0000000000..d8b5a0bea8 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/exception/CoopShopNotFoundException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.domain.coopshop.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class CoopShopNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "해당 생협 매장이 존재하지 않습니다."; + + public CoopShopNotFoundException(String message) { + super(message); + } + + public CoopShopNotFoundException(String message, String detail) { + super(message, detail); + } + + public static CoopShopNotFoundException withDetail(String detail) { + return new CoopShopNotFoundException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopOpen.java b/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopOpen.java new file mode 100644 index 0000000000..32fc188f7b --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopOpen.java @@ -0,0 +1,65 @@ +package in.koreatech.koin.domain.coopshop.model; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "coop_opens") +@NoArgsConstructor(access = PROTECTED) +public class CoopOpen { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "coop_id", referencedColumnName = "id", nullable = false) + private CoopShop coopShop; + + @Column(name = "type") + private String type; + + @NotNull + @Column(name = "day_of_week", nullable = false) + private String dayOfWeek; + + @Column(name = "open_time") + private String openTime; + + @Column(name = "close_time") + private String closeTime; + + @NotNull + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted = false; + + @Builder + private CoopOpen( + CoopShop coopShop, + String type, + String dayOfWeek, + String openTime, + String closeTime + ) { + this.coopShop = coopShop; + this.type = type; + this.dayOfWeek = dayOfWeek; + this.openTime = openTime; + this.closeTime = closeTime; + } + +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopShop.java b/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopShop.java new file mode 100644 index 0000000000..95b4284592 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/model/CoopShop.java @@ -0,0 +1,66 @@ +package in.koreatech.koin.domain.coopshop.model; + +import static jakarta.persistence.CascadeType.*; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import java.util.ArrayList; +import java.util.List; + +import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "coop_shop") +@NoArgsConstructor(access = PROTECTED) +public class CoopShop extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Integer id; + + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @NotNull + @Column(name = "phone", nullable = false) + private String phone; + + @NotNull + @Column(name = "location", nullable = false) + private String location; + + @Column(name = "remarks") + private String remarks; + + @NotNull + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted = false; + + @OneToMany(mappedBy = "coopShop", orphanRemoval = true, cascade = {PERSIST, REFRESH, MERGE, REMOVE}) + private List coopOpens = new ArrayList<>(); + + @Builder + private CoopShop( + String name, + String phone, + String location, + String remarks + ) { + this.name = name; + this.phone = phone; + this.location = location; + this.remarks = remarks; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/repository/CoopShopRepository.java b/src/main/java/in/koreatech/koin/domain/coopshop/repository/CoopShopRepository.java new file mode 100644 index 0000000000..5ece7cff78 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/repository/CoopShopRepository.java @@ -0,0 +1,25 @@ +package in.koreatech.koin.domain.coopshop.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.domain.coopshop.exception.CoopShopNotFoundException; +import in.koreatech.koin.domain.coopshop.model.CoopShop; + +public interface CoopShopRepository extends Repository { + + CoopShop save(CoopShop coopShop); + + List findAll(); + + List findAllByIsDeletedFalse(); + + Optional findById(Integer id); + + default CoopShop getById(Integer id) { + return findById(id) + .orElseThrow(() -> CoopShopNotFoundException.withDetail("coop_id : " + id)); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java b/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java new file mode 100644 index 0000000000..ecaf3d7946 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java @@ -0,0 +1,28 @@ +package in.koreatech.koin.domain.coopshop.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import in.koreatech.koin.domain.coopshop.dto.CoopShopResponse; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CoopShopService { + + private final CoopShopRepository coopShopRepository; + + public List getCoopShops() { + return coopShopRepository.findAllByIsDeletedFalse().stream() + .map(CoopShopResponse::from) + .toList(); + } + + public CoopShopResponse getCoopShop(Integer id) { + CoopShop coopShop = coopShopRepository.getById(id); + return CoopShopResponse.from(coopShop); + } +} diff --git a/src/main/resources/db/migration/V29__add_coop_shop_table.sql b/src/main/resources/db/migration/V29__add_coop_shop_table.sql new file mode 100644 index 0000000000..751d235012 --- /dev/null +++ b/src/main/resources/db/migration/V29__add_coop_shop_table.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS coop_shop ( + id INT UNSIGNED NOT NULL COMMENT 'coop 고유 id' PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '생협 매장 이름', + phone VARCHAR(50) COMMENT '생협 매장 연락처', + location VARCHAR(50) NOT NULL COMMENT '생협 매장 위치', + remarks TEXT COMMENT '특이사항', + is_deleted TINYINT(1) NOT NULL COMMENT '삭제 여부' DEFAULT 0, + created_at TIMESTAMP NOT NULL COMMENT '생성 일자' DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL COMMENT '업데이트 일자' DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS coop_opens ( + id INT UNSIGNED NOT NULL COMMENT 'coop_open 고유 id' PRIMARY KEY AUTO_INCREMENT, + coop_id INT UNSIGNED NOT NULL COMMENT '생협 매장 id', + type VARCHAR(10) COMMENT '기타 타입(아침, 점심, 저녁)', + day_of_week VARCHAR(10) NOT NULL COMMENT '요일', + open_time VARCHAR(10) COMMENT '오픈 시간', + close_time VARCHAR(10) COMMENT '마감 시간', + is_deleted TINYINT(1) NOT NULL COMMENT '삭제 여부' DEFAULT 0, + created_at TIMESTAMP NOT NULL COMMENT '생성 일자' DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL COMMENT '업데이트 일자' DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_COOP_ID FOREIGN KEY (coop_id) REFERENCES coop_shop(id) ON DELETE CASCADE +); diff --git a/src/test/java/in/koreatech/koin/acceptance/CoopShopTest.java b/src/test/java/in/koreatech/koin/acceptance/CoopShopTest.java new file mode 100644 index 0000000000..10d76cdf5e --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/CoopShopTest.java @@ -0,0 +1,157 @@ +package in.koreatech.koin.acceptance; + +import static io.restassured.RestAssured.given; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import in.koreatech.koin.AcceptanceTest; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; +import in.koreatech.koin.fixture.CoopShopFixture; +import in.koreatech.koin.support.JsonAssertions; + +@SuppressWarnings("NonAsciiCharacters") +class CoopShopTest extends AcceptanceTest { + + @Autowired + private CoopShopRepository coopShopRepository; + + @Autowired + private CoopShopFixture coopShopFixture; + + private CoopShop 학생식당; + private CoopShop 세탁소; + + @BeforeEach + void setUp() { + 학생식당 = coopShopFixture.학생식당(); + 세탁소 = coopShopFixture.세탁소(); + } + + @Test + public void getCoopShops() { + var response = given() + .when() + .get("/coopshop") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + JsonAssertions.assertThat(response.asPrettyString()) + .isEqualTo( + """ + [ + { + "id": 1, + "name": "학생식당", + "opens": [ + { + "day_of_week": "평일", + "type": "아침", + "open_time": "08:00", + "close_time": "09:00" + }, + { + "day_of_week": "평일", + "type": "점심", + "open_time": "11:30", + "close_time": "13:30" + }, + { + "day_of_week": "평일", + "type": "저녁", + "open_time": "17:30", + "close_time": "18:30" + }, + { + "day_of_week": "주말", + "type": "점심", + "open_time": "11:30", + "close_time": "13:00" + } + ], + "phone": "041-000-0000", + "location": "학생회관 1층", + "remarks": "공휴일 휴무" + }, + { + "id": 2, + "name": "세탁소", + "opens": [ + { + "day_of_week": "평일", + "type": null, + "open_time": "09:00", + "close_time": "18:00" + }, + { + "day_of_week": "주말", + "type": null, + "open_time": "미운영", + "close_time": "미운영" + } + ], + "phone": "041-000-0000", + "location": "학생회관 2층", + "remarks": "연중무휴" + } + ] + """ + ); + } + + @Test + public void getCoopShop() { + var response = given() + .when() + .get("/coopshop/1") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + JsonAssertions.assertThat(response.asPrettyString()) + .isEqualTo( + """ + { + "id": 1, + "name": "학생식당", + "opens": [ + { + "day_of_week": "평일", + "type": "아침", + "open_time": "08:00", + "close_time": "09:00" + }, + { + "day_of_week": "평일", + "type": "점심", + "open_time": "11:30", + "close_time": "13:30" + }, + { + "day_of_week": "평일", + "type": "저녁", + "open_time": "17:30", + "close_time": "18:30" + }, + { + "day_of_week": "주말", + "type": "점심", + "open_time": "11:30", + "close_time": "13:00" + } + ], + "phone": "041-000-0000", + "location": "학생회관 1층", + "remarks": "공휴일 휴무" + } + """ + ); + + } +} diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminCoopShopTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminCoopShopTest.java new file mode 100644 index 0000000000..b15d760460 --- /dev/null +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminCoopShopTest.java @@ -0,0 +1,165 @@ +package in.koreatech.koin.admin.acceptance; + +import static io.restassured.RestAssured.given; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import in.koreatech.koin.AcceptanceTest; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.fixture.CoopShopFixture; +import in.koreatech.koin.fixture.UserFixture; +import in.koreatech.koin.support.JsonAssertions; +import io.restassured.RestAssured; + +@SuppressWarnings("NonAsciiCharacters") +class AdminCoopShopTest extends AcceptanceTest { + + @Autowired + private CoopShopRepository coopShopRepository; + + @Autowired + private CoopShopFixture coopShopFixture; + + @Autowired + private UserFixture userFixture; + + private CoopShop 학생식당; + private CoopShop 세탁소; + private User admin; + private String token_admin; + + @BeforeEach + void setUp() { + 학생식당 = coopShopFixture.학생식당(); + 세탁소 = coopShopFixture.세탁소(); + admin = userFixture.코인_운영자(); + token_admin = userFixture.getToken(admin); + } + + @Test + public void getCoopShops() { + var response = RestAssured + .given() + .header("Authorization", "Bearer " + token_admin) + .when() + .param("page", 1) + .param("is_deleted", false) + .get("/admin/coopshop") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + JsonAssertions.assertThat(response.asPrettyString()) + .isEqualTo( + """ + { + "totalCount": 2, + "currentCount": 2, + "totalPage": 1, + "currentPage": 1, + "coopShops": [ + { + "id": 1, + "name": "학생식당", + "opens": [ + { + "day_of_week": "평일", + "type": "아침", + "open_time": "08:00", + "close_time": "09:00" + }, + { + "day_of_week": "평일", + "type": "점심", + "open_time": "11:30", + "close_time": "13:30" + }, + { + "day_of_week": "평일", + "type": "저녁", + "open_time": "17:30", + "close_time": "18:30" + }, + { + "day_of_week": "주말", + "type": "점심", + "open_time": "11:30", + "close_time": "13:00" + } + ], + "phone": "041-000-0000", + "location": "학생회관 1층", + "remarks": "공휴일 휴무" + }, + { + "id": 2, + "name": "세탁소", + "opens": [ + { + "day_of_week": "평일", + "type": null, + "open_time": "09:00", + "close_time": "18:00" + }, + { + "day_of_week": "주말", + "type": null, + "open_time": "미운영", + "close_time": "미운영" + } + ], + "phone": "041-000-0000", + "location": "학생회관 2층", + "remarks": "연중무휴" + } + ] + } + """ + ); + } + + @Test + public void getCoopShop() { + var response = given() + .header("Authorization", "Bearer " + token_admin) + .when() + .get("/coopshop/2") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + JsonAssertions.assertThat(response.asPrettyString()) + .isEqualTo( + """ + { + "id": 2, + "name": "세탁소", + "opens": [ + { + "day_of_week": "평일", + "type": null, + "open_time": "09:00", + "close_time": "18:00" + }, + { + "day_of_week": "주말", + "type": null, + "open_time": "미운영", + "close_time": "미운영" + } + ], + "phone": "041-000-0000", + "location": "학생회관 2층", + "remarks": "연중무휴" + } + """ + ); + } +} diff --git a/src/test/java/in/koreatech/koin/fixture/CoopShopFixture.java b/src/test/java/in/koreatech/koin/fixture/CoopShopFixture.java new file mode 100644 index 0000000000..a55d828a29 --- /dev/null +++ b/src/test/java/in/koreatech/koin/fixture/CoopShopFixture.java @@ -0,0 +1,92 @@ +package in.koreatech.koin.fixture; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import in.koreatech.koin.domain.coopshop.model.CoopOpen; +import in.koreatech.koin.domain.coopshop.model.CoopShop; +import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; + +@Component +@SuppressWarnings("NonAsciiCharacters") +public class CoopShopFixture { + + private final CoopShopRepository coopShopRepository; + + public CoopShopFixture(CoopShopRepository coopShopRepository) { + this.coopShopRepository = coopShopRepository; + } + + public CoopShop 학생식당() { + var coopShop = coopShopRepository.save( + CoopShop.builder() + .name("학생식당") + .location("학생회관 1층") + .phone("041-000-0000") + .remarks("공휴일 휴무") + .build() + ); + coopShop.getCoopOpens().addAll( + List.of( + CoopOpen.builder() + .openTime("08:00") + .closeTime("09:00") + .coopShop(coopShop) + .dayOfWeek("평일") + .type("아침") + .build(), + CoopOpen.builder() + .openTime("11:30") + .closeTime("13:30") + .coopShop(coopShop) + .dayOfWeek("평일") + .type("점심") + .build(), + CoopOpen.builder() + .openTime("17:30") + .closeTime("18:30") + .coopShop(coopShop) + .dayOfWeek("평일") + .type("저녁") + .build(), + CoopOpen.builder() + .openTime("11:30") + .closeTime("13:00") + .coopShop(coopShop) + .dayOfWeek("주말") + .type("점심") + .build() + ) + ); + return coopShopRepository.save(coopShop); + } + + public CoopShop 세탁소() { + var coopShop = coopShopRepository.save( + CoopShop.builder() + .name("세탁소") + .location("학생회관 2층") + .phone("041-000-0000") + .remarks("연중무휴") + .build() + ); + coopShop.getCoopOpens().addAll( + List.of( + CoopOpen.builder() + .openTime("09:00") + .closeTime("18:00") + .coopShop(coopShop) + .dayOfWeek("평일") + .build(), + CoopOpen.builder() + .openTime("미운영") + .closeTime("미운영") + .coopShop(coopShop) + .dayOfWeek("주말") + .build() + ) + ); + return coopShopRepository.save(coopShop); + } +}