add : init project

This commit is contained in:
yuhalog
2025-06-11 09:28:32 +09:00
commit b68c7c5fa1
105 changed files with 6022 additions and 0 deletions
@@ -0,0 +1,20 @@
package com.won.smarketing.store;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* 매장 서비스 메인 애플리케이션 클래스
* Spring Boot 애플리케이션의 진입점
*/
@SpringBootApplication(scanBasePackages = {"com.won.smarketing.store", "com.won.smarketing.common"})
@EntityScan(basePackages = {"com.won.smarketing.store.entity"})
@EnableJpaRepositories(basePackages = {"com.won.smarketing.store.repository"})
public class StoreServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StoreServiceApplication.class, args);
}
}
@@ -0,0 +1,89 @@
package com.won.smarketing.store.controller;
import com.won.smarketing.common.dto.ApiResponse;
import com.won.smarketing.store.dto.MenuCreateRequest;
import com.won.smarketing.store.dto.MenuResponse;
import com.won.smarketing.store.dto.MenuUpdateRequest;
import com.won.smarketing.store.service.MenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
/**
* 메뉴 관리를 위한 REST API 컨트롤러
* 메뉴 등록, 조회, 수정, 삭제 기능 제공
*/
@Tag(name = "메뉴 관리", description = "메뉴 정보 관리 API")
@RestController
@RequestMapping("/api/menu")
@RequiredArgsConstructor
public class MenuController {
private final MenuService menuService;
/**
* 메뉴 정보 등록
*
* @param request 메뉴 등록 요청 정보
* @return 등록된 메뉴 정보
*/
@Operation(summary = "메뉴 등록", description = "새로운 메뉴를 등록합니다.")
@PostMapping("/register")
public ResponseEntity<ApiResponse<MenuResponse>> register(@Valid @RequestBody MenuCreateRequest request) {
MenuResponse response = menuService.register(request);
return ResponseEntity.ok(ApiResponse.success(response, "메뉴가 성공적으로 등록되었습니다."));
}
/**
* 메뉴 목록 조회
*
* @param category 메뉴 카테고리 (선택사항)
* @return 메뉴 목록
*/
@Operation(summary = "메뉴 목록 조회", description = "메뉴 목록을 조회합니다. 카테고리별 필터링 가능합니다.")
@GetMapping
public ResponseEntity<ApiResponse<List<MenuResponse>>> getMenus(
@Parameter(description = "메뉴 카테고리")
@RequestParam(required = false) String category) {
List<MenuResponse> response = menuService.getMenus(category);
return ResponseEntity.ok(ApiResponse.success(response));
}
/**
* 메뉴 정보 수정
*
* @param menuId 수정할 메뉴 ID
* @param request 메뉴 수정 요청 정보
* @return 수정된 메뉴 정보
*/
@Operation(summary = "메뉴 수정", description = "메뉴 정보를 수정합니다.")
@PutMapping("/{menuId}")
public ResponseEntity<ApiResponse<MenuResponse>> updateMenu(
@Parameter(description = "메뉴 ID", required = true)
@PathVariable Long menuId,
@Valid @RequestBody MenuUpdateRequest request) {
MenuResponse response = menuService.updateMenu(menuId, request);
return ResponseEntity.ok(ApiResponse.success(response, "메뉴가 성공적으로 수정되었습니다."));
}
/**
* 메뉴 삭제
*
* @param menuId 삭제할 메뉴 ID
* @return 삭제 성공 응답
*/
@Operation(summary = "메뉴 삭제", description = "메뉴를 삭제합니다.")
@DeleteMapping("/{menuId}")
public ResponseEntity<ApiResponse<Void>> deleteMenu(
@Parameter(description = "메뉴 ID", required = true)
@PathVariable Long menuId) {
menuService.deleteMenu(menuId);
return ResponseEntity.ok(ApiResponse.success(null, "메뉴가 성공적으로 삭제되었습니다."));
}
}
@@ -0,0 +1,37 @@
package com.won.smarketing.store.controller;
import com.won.smarketing.common.dto.ApiResponse;
import com.won.smarketing.store.dto.SalesResponse;
import com.won.smarketing.store.service.SalesService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 매출 정보를 위한 REST API 컨트롤러
* 매출 조회 기능 제공
*/
@Tag(name = "매출 관리", description = "매출 정보 조회 API")
@RestController
@RequestMapping("/api/sales")
@RequiredArgsConstructor
public class SalesController {
private final SalesService salesService;
/**
* 매출 정보 조회
*
* @return 매출 정보 (오늘, 월간, 전일 대비)
*/
@Operation(summary = "매출 조회", description = "오늘 매출, 월간 매출, 전일 대비 매출 정보를 조회합니다.")
@GetMapping
public ResponseEntity<ApiResponse<SalesResponse>> getSales() {
SalesResponse response = salesService.getSales();
return ResponseEntity.ok(ApiResponse.success(response));
}
}
@@ -0,0 +1,73 @@
package com.won.smarketing.store.controller;
import com.won.smarketing.common.dto.ApiResponse;
import com.won.smarketing.store.dto.StoreCreateRequest;
import com.won.smarketing.store.dto.StoreResponse;
import com.won.smarketing.store.dto.StoreUpdateRequest;
import com.won.smarketing.store.service.StoreService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 매장 관리를 위한 REST API 컨트롤러
* 매장 등록, 조회, 수정 기능 제공
*/
@Tag(name = "매장 관리", description = "매장 정보 관리 API")
@RestController
@RequestMapping("/api/store")
@RequiredArgsConstructor
public class StoreController {
private final StoreService storeService;
/**
* 매장 정보 등록
*
* @param request 매장 등록 요청 정보
* @return 등록된 매장 정보
*/
@Operation(summary = "매장 등록", description = "새로운 매장 정보를 등록합니다.")
@PostMapping("/register")
public ResponseEntity<ApiResponse<StoreResponse>> register(@Valid @RequestBody StoreCreateRequest request) {
StoreResponse response = storeService.register(request);
return ResponseEntity.ok(ApiResponse.success(response, "매장이 성공적으로 등록되었습니다."));
}
/**
* 매장 정보 조회
*
* @param storeId 조회할 매장 ID
* @return 매장 정보
*/
@Operation(summary = "매장 조회", description = "매장 ID로 매장 정보를 조회합니다.")
@GetMapping
public ResponseEntity<ApiResponse<StoreResponse>> getStore(
@Parameter(description = "매장 ID", required = true)
@RequestParam String storeId) {
StoreResponse response = storeService.getStore(storeId);
return ResponseEntity.ok(ApiResponse.success(response));
}
/**
* 매장 정보 수정
*
* @param storeId 수정할 매장 ID
* @param request 매장 수정 요청 정보
* @return 수정된 매장 정보
*/
@Operation(summary = "매장 수정", description = "매장 정보를 수정합니다.")
@PutMapping("/{storeId}")
public ResponseEntity<ApiResponse<StoreResponse>> updateStore(
@Parameter(description = "매장 ID", required = true)
@PathVariable Long storeId,
@Valid @RequestBody StoreUpdateRequest request) {
StoreResponse response = storeService.updateStore(storeId, request);
return ResponseEntity.ok(ApiResponse.success(response, "매장 정보가 성공적으로 수정되었습니다."));
}
}
@@ -0,0 +1,49 @@
package com.won.smarketing.store.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 메뉴 등록 요청 DTO
* 메뉴 등록 시 필요한 정보를 담는 데이터 전송 객체
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "메뉴 등록 요청 정보")
public class MenuCreateRequest {
@Schema(description = "매장 ID", example = "1", required = true)
@NotNull(message = "매장 ID는 필수입니다.")
private Long storeId;
@Schema(description = "메뉴명", example = "아메리카노", required = true)
@NotBlank(message = "메뉴명은 필수입니다.")
@Size(max = 200, message = "메뉴명은 200자 이하여야 합니다.")
private String menuName;
@Schema(description = "메뉴 카테고리", example = "커피", required = true)
@NotBlank(message = "카테고리는 필수입니다.")
@Size(max = 100, message = "카테고리는 100자 이하여야 합니다.")
private String category;
@Schema(description = "가격", example = "4500", required = true)
@NotNull(message = "가격은 필수입니다.")
@Min(value = 0, message = "가격은 0 이상이어야 합니다.")
private Integer price;
@Schema(description = "메뉴 설명", example = "진한 원두의 깊은 맛")
private String description;
@Schema(description = "메뉴 이미지 URL", example = "https://example.com/americano.jpg")
private String image;
}
@@ -0,0 +1,45 @@
package com.won.smarketing.store.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 메뉴 정보 응답 DTO
* 메뉴 정보 조회/등록/수정 시 반환되는 데이터
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "메뉴 정보 응답")
public class MenuResponse {
@Schema(description = "메뉴 ID", example = "1")
private Long menuId;
@Schema(description = "메뉴명", example = "아메리카노")
private String menuName;
@Schema(description = "메뉴 카테고리", example = "커피")
private String category;
@Schema(description = "가격", example = "4500")
private Integer price;
@Schema(description = "메뉴 설명", example = "진한 원두의 깊은 맛")
private String description;
@Schema(description = "메뉴 이미지 URL", example = "https://example.com/americano.jpg")
private String image;
@Schema(description = "등록 시각")
private LocalDateTime createdAt;
@Schema(description = "수정 시각")
private LocalDateTime updatedAt;
}
@@ -0,0 +1,40 @@
package com.won.smarketing.store.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
/**
* 메뉴 수정 요청 DTO
* 메뉴 정보 수정 시 필요한 정보를 담는 데이터 전송 객체
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "메뉴 수정 요청 정보")
public class MenuUpdateRequest {
@Schema(description = "메뉴명", example = "아메리카노")
@Size(max = 200, message = "메뉴명은 200자 이하여야 합니다.")
private String menuName;
@Schema(description = "메뉴 카테고리", example = "커피")
@Size(max = 100, message = "카테고리는 100자 이하여야 합니다.")
private String category;
@Schema(description = "가격", example = "4500")
@Min(value = 0, message = "가격은 0 이상이어야 합니다.")
private Integer price;
@Schema(description = "메뉴 설명", example = "진한 원두의 깊은 맛")
private String description;
@Schema(description = "메뉴 이미지 URL", example = "https://example.com/americano.jpg")
private String image;
}
@@ -0,0 +1,30 @@
package com.won.smarketing.store.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 매출 정보 응답 DTO
* 오늘 매출, 월간 매출, 전일 대비 매출 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "매출 정보 응답")
public class SalesResponse {
@Schema(description = "오늘 매출", example = "150000")
private BigDecimal todaySales;
@Schema(description = "이번 달 매출", example = "3200000")
private BigDecimal monthSales;
@Schema(description = "전일 대비 매출 변화량", example = "25000")
private BigDecimal previousDayComparison;
}
@@ -0,0 +1,79 @@
package com.won.smarketing.store.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
/**
* 매장 등록 요청 DTO
* 매장 등록 시 필요한 정보를 담는 데이터 전송 객체
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "매장 등록 요청 정보")
public class StoreCreateRequest {
@Schema(description = "매장 소유자 사용자 ID", example = "testuser", required = true)
@NotBlank(message = "사용자 ID는 필수입니다.")
private String userId;
@Schema(description = "매장명", example = "맛있는 카페", required = true)
@NotBlank(message = "매장명은 필수입니다.")
@Size(max = 200, message = "매장명은 200자 이하여야 합니다.")
private String storeName;
@Schema(description = "매장 이미지 URL", example = "https://example.com/store.jpg")
private String storeImage;
@Schema(description = "업종", example = "카페", required = true)
@NotBlank(message = "업종은 필수입니다.")
@Size(max = 100, message = "업종은 100자 이하여야 합니다.")
private String businessType;
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123", required = true)
@NotBlank(message = "주소는 필수입니다.")
@Size(max = 500, message = "주소는 500자 이하여야 합니다.")
private String address;
@Schema(description = "매장 전화번호", example = "02-1234-5678", required = true)
@NotBlank(message = "전화번호는 필수입니다.")
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "올바른 전화번호 형식이 아닙니다.")
private String phoneNumber;
@Schema(description = "사업자 번호", example = "123-45-67890", required = true)
@NotBlank(message = "사업자 번호는 필수입니다.")
@Pattern(regexp = "^\\d{3}-\\d{2}-\\d{5}$", message = "사업자 번호 형식이 올바르지 않습니다.")
private String businessNumber;
@Schema(description = "인스타그램 계정", example = "@mycafe")
@Size(max = 100, message = "인스타그램 계정은 100자 이하여야 합니다.")
private String instaAccount;
@Schema(description = "네이버 블로그 계정", example = "mycafe_blog")
@Size(max = 100, message = "네이버 블로그 계정은 100자 이하여야 합니다.")
private String naverBlogAccount;
@Schema(description = "오픈 시간", example = "09:00")
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
private String openTime;
@Schema(description = "마감 시간", example = "22:00")
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
private String closeTime;
@Schema(description = "휴무일", example = "매주 월요일")
@Size(max = 100, message = "휴무일은 100자 이하여야 합니다.")
private String closedDays;
@Schema(description = "좌석 수", example = "20")
private Integer seatCount;
}
@@ -0,0 +1,66 @@
package com.won.smarketing.store.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 매장 정보 응답 DTO
* 매장 정보 조회/등록/수정 시 반환되는 데이터
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "매장 정보 응답")
public class StoreResponse {
@Schema(description = "매장 ID", example = "1")
private Long storeId;
@Schema(description = "매장명", example = "맛있는 카페")
private String storeName;
@Schema(description = "매장 이미지 URL", example = "https://example.com/store.jpg")
private String storeImage;
@Schema(description = "업종", example = "카페")
private String businessType;
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123")
private String address;
@Schema(description = "매장 전화번호", example = "02-1234-5678")
private String phoneNumber;
@Schema(description = "사업자 번호", example = "123-45-67890")
private String businessNumber;
@Schema(description = "인스타그램 계정", example = "@mycafe")
private String instaAccount;
@Schema(description = "네이버 블로그 계정", example = "mycafe_blog")
private String naverBlogAccount;
@Schema(description = "오픈 시간", example = "09:00")
private String openTime;
@Schema(description = "마감 시간", example = "22:00")
private String closeTime;
@Schema(description = "휴무일", example = "매주 월요일")
private String closedDays;
@Schema(description = "좌석 수", example = "20")
private Integer seatCount;
@Schema(description = "등록 시각")
private LocalDateTime createdAt;
@Schema(description = "수정 시각")
private LocalDateTime updatedAt;
}
@@ -0,0 +1,60 @@
package com.won.smarketing.store.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
/**
* 매장 수정 요청 DTO
* 매장 정보 수정 시 필요한 정보를 담는 데이터 전송 객체
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "매장 수정 요청 정보")
public class StoreUpdateRequest {
@Schema(description = "매장명", example = "맛있는 카페")
@Size(max = 200, message = "매장명은 200자 이하여야 합니다.")
private String storeName;
@Schema(description = "매장 이미지 URL", example = "https://example.com/store.jpg")
private String storeImage;
@Schema(description = "매장 주소", example = "서울시 강남구 테헤란로 123")
@Size(max = 500, message = "주소는 500자 이하여야 합니다.")
private String address;
@Schema(description = "매장 전화번호", example = "02-1234-5678")
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "올바른 전화번호 형식이 아닙니다.")
private String phoneNumber;
@Schema(description = "인스타그램 계정", example = "@mycafe")
@Size(max = 100, message = "인스타그램 계정은 100자 이하여야 합니다.")
private String instaAccount;
@Schema(description = "네이버 블로그 계정", example = "mycafe_blog")
@Size(max = 100, message = "네이버 블로그 계정은 100자 이하여야 합니다.")
private String naverBlogAccount;
@Schema(description = "오픈 시간", example = "09:00")
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
private String openTime;
@Schema(description = "마감 시간", example = "22:00")
@Pattern(regexp = "^\\d{2}:\\d{2}$", message = "올바른 시간 형식이 아닙니다. (HH:MM)")
private String closeTime;
@Schema(description = "휴무일", example = "매주 월요일")
@Size(max = 100, message = "휴무일은 100자 이하여야 합니다.")
private String closedDays;
@Schema(description = "좌석 수", example = "20")
private Integer seatCount;
}
@@ -0,0 +1,110 @@
package com.won.smarketing.store.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
/**
* 메뉴 정보를 나타내는 엔티티
* 메뉴명, 카테고리, 가격, 설명, 이미지 정보 저장
*/
@Entity
@Table(name = "menus")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Menu {
/**
* 메뉴 고유 식별자
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 매장 ID
*/
@Column(name = "store_id", nullable = false)
private Long storeId;
/**
* 메뉴명
*/
@Column(name = "menu_name", nullable = false, length = 200)
private String menuName;
/**
* 메뉴 카테고리
*/
@Column(name = "category", nullable = false, length = 100)
private String category;
/**
* 가격
*/
@Column(name = "price", nullable = false)
private Integer price;
/**
* 메뉴 설명
*/
@Column(name = "description", columnDefinition = "TEXT")
private String description;
/**
* 메뉴 이미지 URL
*/
@Column(name = "image", length = 500)
private String image;
/**
* 메뉴 등록 시각
*/
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 메뉴 정보 수정 시각
*/
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
/**
* 엔티티 저장 전 실행되는 메서드
* 생성 시각과 수정 시각을 현재 시각으로 설정
*/
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
/**
* 엔티티 업데이트 전 실행되는 메서드
* 수정 시각을 현재 시각으로 갱신
*/
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
/**
* 메뉴 정보 업데이트 메서드
*
* @param menuName 메뉴명
* @param category 카테고리
* @param price 가격
* @param description 설명
* @param image 이미지 URL
*/
public void updateMenuInfo(String menuName, String category, Integer price, String description, String image) {
this.menuName = menuName;
this.category = category;
this.price = price;
this.description = description;
this.image = image;
}
}
@@ -0,0 +1,61 @@
package com.won.smarketing.store.entity;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 매출 정보를 나타내는 엔티티
* 일별 매출 데이터 저장
*/
@Entity
@Table(name = "sales")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Sales {
/**
* 매출 고유 식별자
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 매장 ID
*/
@Column(name = "store_id", nullable = false)
private Long storeId;
/**
* 매출 날짜
*/
@Column(name = "sales_date", nullable = false)
private LocalDate salesDate;
/**
* 매출 금액
*/
@Column(name = "sales_amount", nullable = false, precision = 15, scale = 2)
private BigDecimal salesAmount;
/**
* 매출 등록 시각
*/
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 엔티티 저장 전 실행되는 메서드
* 생성 시각을 현재 시각으로 설정
*/
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
@@ -0,0 +1,164 @@
package com.won.smarketing.store.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
/**
* 매장 정보를 나타내는 엔티티
* 매장의 기본 정보, 운영 정보, SNS 계정 정보 저장
*/
@Entity
@Table(name = "stores")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Store {
/**
* 매장 고유 식별자
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 매장 소유자 사용자 ID
*/
@Column(name = "user_id", unique = true, nullable = false, length = 50)
private String userId;
/**
* 매장명
*/
@Column(name = "store_name", nullable = false, length = 200)
private String storeName;
/**
* 매장 이미지 URL
*/
@Column(name = "store_image", length = 500)
private String storeImage;
/**
* 업종
*/
@Column(name = "business_type", nullable = false, length = 100)
private String businessType;
/**
* 매장 주소
*/
@Column(name = "address", nullable = false, length = 500)
private String address;
/**
* 매장 전화번호
*/
@Column(name = "phone_number", nullable = false, length = 20)
private String phoneNumber;
/**
* 사업자 번호
*/
@Column(name = "business_number", nullable = false, length = 20)
private String businessNumber;
/**
* 인스타그램 계정
*/
@Column(name = "insta_account", length = 100)
private String instaAccount;
/**
* 네이버 블로그 계정
*/
@Column(name = "naver_blog_account", length = 100)
private String naverBlogAccount;
/**
* 오픈 시간
*/
@Column(name = "open_time", length = 10)
private String openTime;
/**
* 마감 시간
*/
@Column(name = "close_time", length = 10)
private String closeTime;
/**
* 휴무일
*/
@Column(name = "closed_days", length = 100)
private String closedDays;
/**
* 좌석 수
*/
@Column(name = "seat_count")
private Integer seatCount;
/**
* 매장 등록 시각
*/
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 매장 정보 수정 시각
*/
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
/**
* 엔티티 저장 전 실행되는 메서드
* 생성 시각과 수정 시각을 현재 시각으로 설정
*/
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
/**
* 엔티티 업데이트 전 실행되는 메서드
* 수정 시각을 현재 시각으로 갱신
*/
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
/**
* 매장 정보 업데이트 메서드
*
* @param storeName 매장명
* @param storeImage 매장 이미지
* @param address 주소
* @param phoneNumber 전화번호
* @param instaAccount 인스타그램 계정
* @param naverBlogAccount 네이버 블로그 계정
* @param openTime 오픈 시간
* @param closeTime 마감 시간
* @param closedDays 휴무일
* @param seatCount 좌석 수
*/
public void updateStoreInfo(String storeName, String storeImage, String address, String phoneNumber,
String instaAccount, String naverBlogAccount, String openTime, String closeTime,
String closedDays, Integer seatCount) {
this.storeName = storeName;
this.storeImage = storeImage;
this.address = address;
this.phoneNumber = phoneNumber;
this.instaAccount = instaAccount;
this.naverBlogAccount = naverBlogAccount;
this.openTime = openTime;
this.closeTime = closeTime;
this.closedDays = closedDays;
this.seatCount = seatCount;
}
}
@@ -0,0 +1,38 @@
package com.won.smarketing.store.repository;
import com.won.smarketing.store.entity.Menu;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 메뉴 정보 데이터 접근을 위한 Repository
* JPA를 사용한 메뉴 CRUD 작업 처리
*/
@Repository
public interface MenuRepository extends JpaRepository<Menu, Long> {
/**
* 카테고리별 메뉴 조회 (메뉴명 오름차순)
*
* @param category 메뉴 카테고리
* @return 메뉴 목록
*/
List<Menu> findByCategoryOrderByMenuNameAsc(String category);
/**
* 전체 메뉴 조회 (메뉴명 오름차순)
*
* @return 메뉴 목록
*/
List<Menu> findAllByOrderByMenuNameAsc();
/**
* 매장별 메뉴 조회
*
* @param storeId 매장 ID
* @return 메뉴 목록
*/
List<Menu> findByStoreId(Long storeId);
}
@@ -0,0 +1,44 @@
package com.won.smarketing.store.repository;
import com.won.smarketing.store.entity.Sales;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
/**
* 매출 정보 데이터 접근을 위한 Repository
* JPA를 사용한 매출 조회 작업 처리
*/
@Repository
public interface SalesRepository extends JpaRepository<Sales, Long> {
/**
* 매장의 오늘 매출 조회
*
* @param storeId 매장 ID
* @return 오늘 매출
*/
@Query("SELECT COALESCE(SUM(s.salesAmount), 0) FROM Sales s WHERE s.storeId = :storeId AND s.salesDate = CURRENT_DATE")
BigDecimal findTodaySalesByStoreId(@Param("storeId") Long storeId);
/**
* 매장의 이번 달 매출 조회
*
* @param storeId 매장 ID
* @return 이번 달 매출
*/
@Query("SELECT COALESCE(SUM(s.salesAmount), 0) FROM Sales s WHERE s.storeId = :storeId AND YEAR(s.salesDate) = YEAR(CURRENT_DATE) AND MONTH(s.salesDate) = MONTH(CURRENT_DATE)")
BigDecimal findMonthSalesByStoreId(@Param("storeId") Long storeId);
/**
* 매장의 전일 대비 매출 변화량 조회
*
* @param storeId 매장 ID
* @return 전일 대비 매출 변화량
*/
@Query("SELECT COALESCE((SELECT SUM(s1.salesAmount) FROM Sales s1 WHERE s1.storeId = :storeId AND s1.salesDate = CURRENT_DATE) - (SELECT SUM(s2.salesAmount) FROM Sales s2 WHERE s2.storeId = :storeId AND s2.salesDate = CURRENT_DATE - 1), 0)")
BigDecimal findPreviousDayComparisonByStoreId(@Param("storeId") Long storeId);
}
@@ -0,0 +1,23 @@
package com.won.smarketing.store.repository;
import com.won.smarketing.store.entity.Store;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 매장 정보 데이터 접근을 위한 Repository
* JPA를 사용한 매장 CRUD 작업 처리
*/
@Repository
public interface StoreRepository extends JpaRepository<Store, Long> {
/**
* 사용자 ID로 매장 조회
*
* @param userId 사용자 ID
* @return 매장 정보
*/
Optional<Store> findByUserId(String userId);
}
@@ -0,0 +1,46 @@
package com.won.smarketing.store.service;
import com.won.smarketing.store.dto.MenuCreateRequest;
import com.won.smarketing.store.dto.MenuResponse;
import com.won.smarketing.store.dto.MenuUpdateRequest;
import java.util.List;
/**
* 메뉴 관리 서비스 인터페이스
* 메뉴 등록, 조회, 수정, 삭제 기능 정의
*/
public interface MenuService {
/**
* 메뉴 정보 등록
*
* @param request 메뉴 등록 요청 정보
* @return 등록된 메뉴 정보
*/
MenuResponse register(MenuCreateRequest request);
/**
* 메뉴 목록 조회
*
* @param category 메뉴 카테고리 (선택사항)
* @return 메뉴 목록
*/
List<MenuResponse> getMenus(String category);
/**
* 메뉴 정보 수정
*
* @param menuId 수정할 메뉴 ID
* @param request 메뉴 수정 요청 정보
* @return 수정된 메뉴 정보
*/
MenuResponse updateMenu(Long menuId, MenuUpdateRequest request);
/**
* 메뉴 삭제
*
* @param menuId 삭제할 메뉴 ID
*/
void deleteMenu(Long menuId);
}
@@ -0,0 +1,130 @@
package com.won.smarketing.store.service;
import com.won.smarketing.common.exception.BusinessException;
import com.won.smarketing.common.exception.ErrorCode;
import com.won.smarketing.store.dto.MenuCreateRequest;
import com.won.smarketing.store.dto.MenuResponse;
import com.won.smarketing.store.dto.MenuUpdateRequest;
import com.won.smarketing.store.entity.Menu;
import com.won.smarketing.store.repository.MenuRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 메뉴 관리 서비스 구현체
* 메뉴 등록, 조회, 수정, 삭제 기능 구현
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MenuServiceImpl implements MenuService {
private final MenuRepository menuRepository;
/**
* 메뉴 정보 등록
*
* @param request 메뉴 등록 요청 정보
* @return 등록된 메뉴 정보
*/
@Override
@Transactional
public MenuResponse register(MenuCreateRequest request) {
// 메뉴 엔티티 생성 및 저장
Menu menu = Menu.builder()
.storeId(request.getStoreId())
.menuName(request.getMenuName())
.category(request.getCategory())
.price(request.getPrice())
.description(request.getDescription())
.image(request.getImage())
.build();
Menu savedMenu = menuRepository.save(menu);
return toMenuResponse(savedMenu);
}
/**
* 메뉴 목록 조회
*
* @param category 메뉴 카테고리 (선택사항)
* @return 메뉴 목록
*/
@Override
public List<MenuResponse> getMenus(String category) {
List<Menu> menus;
if (category != null && !category.trim().isEmpty()) {
menus = menuRepository.findByCategoryOrderByMenuNameAsc(category);
} else {
menus = menuRepository.findAllByOrderByMenuNameAsc();
}
return menus.stream()
.map(this::toMenuResponse)
.collect(Collectors.toList());
}
/**
* 메뉴 정보 수정
*
* @param menuId 수정할 메뉴 ID
* @param request 메뉴 수정 요청 정보
* @return 수정된 메뉴 정보
*/
@Override
@Transactional
public MenuResponse updateMenu(Long menuId, MenuUpdateRequest request) {
Menu menu = menuRepository.findById(menuId)
.orElseThrow(() -> new BusinessException(ErrorCode.MENU_NOT_FOUND));
// 메뉴 정보 업데이트
menu.updateMenuInfo(
request.getMenuName(),
request.getCategory(),
request.getPrice(),
request.getDescription(),
request.getImage()
);
Menu updatedMenu = menuRepository.save(menu);
return toMenuResponse(updatedMenu);
}
/**
* 메뉴 삭제
*
* @param menuId 삭제할 메뉴 ID
*/
@Override
@Transactional
public void deleteMenu(Long menuId) {
Menu menu = menuRepository.findById(menuId)
.orElseThrow(() -> new BusinessException(ErrorCode.MENU_NOT_FOUND));
menuRepository.delete(menu);
}
/**
* Menu 엔티티를 MenuResponse DTO로 변환
*
* @param menu Menu 엔티티
* @return MenuResponse DTO
*/
private MenuResponse toMenuResponse(Menu menu) {
return MenuResponse.builder()
.menuId(menu.getId())
.menuName(menu.getMenuName())
.category(menu.getCategory())
.price(menu.getPrice())
.description(menu.getDescription())
.image(menu.getImage())
.createdAt(menu.getCreatedAt())
.updatedAt(menu.getUpdatedAt())
.build();
}
}
@@ -0,0 +1,17 @@
package com.won.smarketing.store.service;
import com.won.smarketing.store.dto.SalesResponse;
/**
* 매출 관리 서비스 인터페이스
* 매출 조회 기능 정의
*/
public interface SalesService {
/**
* 매출 정보 조회
*
* @return 매출 정보 (오늘, 월간, 전일 대비)
*/
SalesResponse getSales();
}
@@ -0,0 +1,42 @@
package com.won.smarketing.store.service;
import com.won.smarketing.store.dto.SalesResponse;
import com.won.smarketing.store.repository.SalesRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
* 매출 관리 서비스 구현체
* 매출 조회 기능 구현
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SalesServiceImpl implements SalesService {
private final SalesRepository salesRepository;
/**
* 매출 정보 조회
*
* @return 매출 정보 (오늘, 월간, 전일 대비)
*/
@Override
public SalesResponse getSales() {
// TODO: 현재는 더미 데이터 반환, 실제로는 현재 로그인한 사용자의 매장 ID를 사용해야 함
Long storeId = 1L; // 임시로 설정
BigDecimal todaySales = salesRepository.findTodaySalesByStoreId(storeId);
BigDecimal monthSales = salesRepository.findMonthSalesByStoreId(storeId);
BigDecimal previousDayComparison = salesRepository.findPreviousDayComparisonByStoreId(storeId);
return SalesResponse.builder()
.todaySales(todaySales != null ? todaySales : BigDecimal.ZERO)
.monthSales(monthSales != null ? monthSales : BigDecimal.ZERO)
.previousDayComparison(previousDayComparison != null ? previousDayComparison : BigDecimal.ZERO)
.build();
}
}
@@ -0,0 +1,37 @@
package com.won.smarketing.store.service;
import com.won.smarketing.store.dto.StoreCreateRequest;
import com.won.smarketing.store.dto.StoreResponse;
import com.won.smarketing.store.dto.StoreUpdateRequest;
/**
* 매장 관리 서비스 인터페이스
* 매장 등록, 조회, 수정 기능 정의
*/
public interface StoreService {
/**
* 매장 정보 등록
*
* @param request 매장 등록 요청 정보
* @return 등록된 매장 정보
*/
StoreResponse register(StoreCreateRequest request);
/**
* 매장 정보 조회
*
* @param storeId 조회할 매장 ID
* @return 매장 정보
*/
StoreResponse getStore(String storeId);
/**
* 매장 정보 수정
*
* @param storeId 수정할 매장 ID
* @param request 매장 수정 요청 정보
* @return 수정된 매장 정보
*/
StoreResponse updateStore(Long storeId, StoreUpdateRequest request);
}
@@ -0,0 +1,130 @@
package com.won.smarketing.store.service;
import com.won.smarketing.common.exception.BusinessException;
import com.won.smarketing.common.exception.ErrorCode;
import com.won.smarketing.store.dto.StoreCreateRequest;
import com.won.smarketing.store.dto.StoreResponse;
import com.won.smarketing.store.dto.StoreUpdateRequest;
import com.won.smarketing.store.entity.Store;
import com.won.smarketing.store.repository.StoreRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 매장 관리 서비스 구현체
* 매장 등록, 조회, 수정 기능 구현
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class StoreServiceImpl implements StoreService {
private final StoreRepository storeRepository;
/**
* 매장 정보 등록
*
* @param request 매장 등록 요청 정보
* @return 등록된 매장 정보
*/
@Override
@Transactional
public StoreResponse register(StoreCreateRequest request) {
// 사용자별 매장 중복 등록 확인
if (storeRepository.findByUserId(request.getUserId()).isPresent()) {
throw new BusinessException(ErrorCode.STORE_ALREADY_EXISTS);
}
// 매장 엔티티 생성 및 저장
Store store = Store.builder()
.userId(request.getUserId())
.storeName(request.getStoreName())
.storeImage(request.getStoreImage())
.businessType(request.getBusinessType())
.address(request.getAddress())
.phoneNumber(request.getPhoneNumber())
.businessNumber(request.getBusinessNumber())
.instaAccount(request.getInstaAccount())
.naverBlogAccount(request.getNaverBlogAccount())
.openTime(request.getOpenTime())
.closeTime(request.getCloseTime())
.closedDays(request.getClosedDays())
.seatCount(request.getSeatCount())
.build();
Store savedStore = storeRepository.save(store);
return toStoreResponse(savedStore);
}
/**
* 매장 정보 조회
*
* @param storeId 조회할 매장 ID
* @return 매장 정보
*/
@Override
public StoreResponse getStore(String storeId) {
Store store = storeRepository.findByUserId(storeId)
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
return toStoreResponse(store);
}
/**
* 매장 정보 수정
*
* @param storeId 수정할 매장 ID
* @param request 매장 수정 요청 정보
* @return 수정된 매장 정보
*/
@Override
@Transactional
public StoreResponse updateStore(Long storeId, StoreUpdateRequest request) {
Store store = storeRepository.findById(storeId)
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
// 매장 정보 업데이트
store.updateStoreInfo(
request.getStoreName(),
request.getStoreImage(),
request.getAddress(),
request.getPhoneNumber(),
request.getInstaAccount(),
request.getNaverBlogAccount(),
request.getOpenTime(),
request.getCloseTime(),
request.getClosedDays(),
request.getSeatCount()
);
Store updatedStore = storeRepository.save(store);
return toStoreResponse(updatedStore);
}
/**
* Store 엔티티를 StoreResponse DTO로 변환
*
* @param store Store 엔티티
* @return StoreResponse DTO
*/
private StoreResponse toStoreResponse(Store store) {
return StoreResponse.builder()
.storeId(store.getId())
.storeName(store.getStoreName())
.storeImage(store.getStoreImage())
.businessType(store.getBusinessType())
.address(store.getAddress())
.phoneNumber(store.getPhoneNumber())
.businessNumber(store.getBusinessNumber())
.instaAccount(store.getInstaAccount())
.naverBlogAccount(store.getNaverBlogAccount())
.openTime(store.getOpenTime())
.closeTime(store.getCloseTime())
.closedDays(store.getClosedDays())
.seatCount(store.getSeatCount())
.createdAt(store.getCreatedAt())
.updatedAt(store.getUpdatedAt())
.build();
}
}
+31
View File
@@ -0,0 +1,31 @@
server:
port: ${SERVER_PORT:8082}
servlet:
context-path: /
spring:
application:
name: store-service
datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:storedb}
username: ${POSTGRES_USER:postgres}
password: ${POSTGRES_PASSWORD:postgres}
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:true}
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
springdoc:
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
api-docs:
path: /api-docs
logging:
level:
com.won.smarketing.store: ${LOG_LEVEL:DEBUG}