Skip to content

Commit 453bacf

Browse files
authored
Merge pull request #80 from YAPP-Github/feat/PRODUCT-148
[Feat] 스토리 등록 구현
2 parents d008489 + e527cf5 commit 453bacf

File tree

23 files changed

+801
-12
lines changed

23 files changed

+801
-12
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package eatda.controller.story;
2+
3+
public record FilteredSearchResult(
4+
String kakaoId,
5+
String name,
6+
String address,
7+
String category
8+
) {
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package eatda.controller.story;
2+
3+
import java.util.List;
4+
5+
public record StoriesResponse(
6+
List<StoryPreview> stories
7+
) {
8+
public record StoryPreview(
9+
Long storyId,
10+
String imageUrl
11+
) {
12+
}
13+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package eatda.controller.story;
2+
3+
import eatda.controller.web.auth.LoginMember;
4+
import eatda.service.story.StoryService;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.PostMapping;
9+
import org.springframework.web.bind.annotation.RequestPart;
10+
import org.springframework.web.bind.annotation.RestController;
11+
import org.springframework.web.multipart.MultipartFile;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
public class StoryController {
16+
17+
private final StoryService storyService;
18+
19+
@PostMapping("/api/stories")
20+
public ResponseEntity<Void> registerStory(
21+
@RequestPart("request") StoryRegisterRequest request,
22+
@RequestPart("image") MultipartFile image,
23+
LoginMember member
24+
) {
25+
storyService.registerStory(request, image, member.id());
26+
return ResponseEntity.status(HttpStatus.CREATED).build();
27+
}
28+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package eatda.controller.story;
2+
3+
public record StoryRegisterRequest(
4+
String query,
5+
String storeKakaoId,
6+
String description
7+
) {
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package eatda.controller.story;
2+
3+
public record StoryResponse(
4+
String storeKakaoId,
5+
String category,
6+
String storeName,
7+
String storeAddress,
8+
String description,
9+
String imageUrl
10+
) {
11+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package eatda.domain.story;
2+
3+
import eatda.domain.AuditingEntity;
4+
import eatda.domain.member.Member;
5+
import eatda.exception.BusinessErrorCode;
6+
import eatda.exception.BusinessException;
7+
import jakarta.persistence.Column;
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.FetchType;
10+
import jakarta.persistence.GeneratedValue;
11+
import jakarta.persistence.GenerationType;
12+
import jakarta.persistence.Id;
13+
import jakarta.persistence.JoinColumn;
14+
import jakarta.persistence.ManyToOne;
15+
import jakarta.persistence.Table;
16+
import lombok.AccessLevel;
17+
import lombok.Builder;
18+
import lombok.Getter;
19+
import lombok.NoArgsConstructor;
20+
21+
@Table(name = "story")
22+
@Entity
23+
@Getter
24+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
25+
public class Story extends AuditingEntity {
26+
27+
@Id
28+
@GeneratedValue(strategy = GenerationType.IDENTITY)
29+
private Long id;
30+
31+
@ManyToOne(fetch = FetchType.LAZY)
32+
@JoinColumn(name = "member_id", nullable = false)
33+
private Member member;
34+
35+
@Column(name = "store_kakao_id", nullable = false)
36+
private String storeKakaoId;
37+
38+
@Column(name = "store_name", nullable = false)
39+
private String storeName;
40+
41+
@Column(name = "store_address", nullable = false)
42+
private String storeAddress;
43+
44+
@Column(name = "store_category", nullable = false)
45+
private String storeCategory;
46+
47+
@Column(name = "description", nullable = false)
48+
private String description;
49+
50+
@Column(name = "image_key", nullable = false)
51+
private String imageKey;
52+
53+
@Builder
54+
private Story(
55+
Member member,
56+
String storeKakaoId,
57+
String storeName,
58+
String storeAddress,
59+
String storeCategory,
60+
String description,
61+
String imageKey
62+
) {
63+
validateMember(member);
64+
validateStore(storeKakaoId, storeName, storeAddress, storeCategory);
65+
validateStory(description, imageKey);
66+
67+
this.member = member;
68+
this.storeKakaoId = storeKakaoId;
69+
this.storeName = storeName;
70+
this.storeAddress = storeAddress;
71+
this.storeCategory = storeCategory;
72+
this.description = description;
73+
this.imageKey = imageKey;
74+
}
75+
76+
private void validateMember(Member member) {
77+
if (member == null) {
78+
throw new BusinessException(BusinessErrorCode.STORY_MEMBER_REQUIRED);
79+
}
80+
}
81+
82+
private void validateStore(String storeKakaoId, String storeName, String storeAddress, String storeCategory) {
83+
validateStoreKakaoId(storeKakaoId);
84+
validateStoreName(storeName);
85+
validateStoreAddress(storeAddress);
86+
validateStoreCategory(storeCategory);
87+
}
88+
89+
private void validateStory(String description, String imageKey) {
90+
validateDescription(description);
91+
validateImage(imageKey);
92+
}
93+
94+
private void validateStoreKakaoId(String storeKakaoId) {
95+
if (storeKakaoId == null || storeKakaoId.isBlank()) {
96+
throw new BusinessException(BusinessErrorCode.INVALID_STORE_KAKAO_ID);
97+
}
98+
}
99+
100+
private void validateStoreName(String storeName) {
101+
if (storeName == null || storeName.isBlank()) {
102+
throw new BusinessException(BusinessErrorCode.INVALID_STORE_NAME);
103+
}
104+
}
105+
106+
private void validateStoreAddress(String storeAddress) {
107+
if (storeAddress == null || storeAddress.isBlank()) {
108+
throw new BusinessException(BusinessErrorCode.INVALID_STORE_ADDRESS);
109+
}
110+
}
111+
112+
private void validateStoreCategory(String storeCategory) {
113+
if (storeCategory == null || storeCategory.isBlank()) {
114+
throw new BusinessException(BusinessErrorCode.INVALID_STORE_CATEGORY);
115+
}
116+
}
117+
118+
private void validateDescription(String description) {
119+
if (description == null || description.isBlank()) {
120+
throw new BusinessException(BusinessErrorCode.INVALID_STORY_DESCRIPTION);
121+
}
122+
}
123+
124+
private void validateImage(String imageKey) {
125+
if (imageKey == null || imageKey.isBlank()) {
126+
throw new BusinessException(BusinessErrorCode.INVALID_STORY_IMAGE_KEY);
127+
}
128+
}
129+
}

src/main/java/eatda/exception/BusinessErrorCode.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public enum BusinessErrorCode {
2121
INVALID_STORE_COORDINATES_NULL("STO007", "좌표 값은 필수입니다."),
2222
OUT_OF_SEOUL_LATITUDE_RANGE("STO010", "서비스 지역(서울)을 벗어난 위도 값입니다."),
2323
OUT_OF_SEOUL_LONGITUDE_RANGE("STO011", "서비스 지역(서울)을 벗어난 경도 값입니다."),
24+
STORE_NOT_FOUND("ST0012", "해당 가게 정보를 찾을수 없습니다."),
2425

2526
// Cheer
2627
INVALID_CHEER_DESCRIPTION("CHE001", "응원 메시지는 필수입니다."),
@@ -40,7 +41,17 @@ public enum BusinessErrorCode {
4041
FILE_UPLOAD_FAILED("SERVER002", "파일 업로드에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
4142
FILE_URL_GENERATION_FAILED("SERVER003", "파일 URL 생성에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
4243
PRESIGNED_URL_GENERATION_FAILED("SERVER004", "Presigned URL 생성에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
43-
;
44+
45+
//story
46+
INVALID_STORY_DESCRIPTION("STY001", "스토리 본문은 필수입니다."),
47+
INVALID_STORY_IMAGE_KEY("STY002", "스토리 이미지 Key는 필수입니다."),
48+
STORY_MEMBER_REQUIRED("STY003", "스토리 작성 시 회원 정보는 필수입니다."),
49+
STORY_STORE_REQUIRED("STY004", "스토리 작성 시 가게 정보는 필수입니다."),
50+
STORY_NOT_FOUND("STY005", "스토리를 찾을 수 없습니다."),
51+
INVALID_STORE_ID("STY006", "유효하지 않은 가게 ID입니다."),
52+
INVALID_STORE_KAKAO_ID("STY007", "스토어 Kakao ID는 필수입니다."),
53+
INVALID_STORE_NAME("STY008", "스토어 이름은 필수입니다."),
54+
INVALID_STORE_ADDRESS("STY009", "스토어 주소는 필수입니다.");
4455

4556
private final String code;
4657
private final String message;

src/main/java/eatda/exception/GlobalExceptionHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import jakarta.validation.ConstraintViolationException;
44
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
56
import org.apache.catalina.connector.ClientAbortException;
67
import org.springframework.http.ResponseEntity;
78
import org.springframework.validation.BindException;
@@ -15,6 +16,7 @@
1516
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
1617
import org.springframework.web.servlet.resource.NoResourceFoundException;
1718

19+
@Slf4j
1820
@RestControllerAdvice
1921
@RequiredArgsConstructor
2022
public class GlobalExceptionHandler {
@@ -77,13 +79,15 @@ public ResponseEntity<ErrorResponse> handleMissingServletRequestParameterExcepti
7779

7880
@ExceptionHandler(BusinessException.class)
7981
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException exception) {
82+
log.error("[BusinessException] handled: {}", exception.getErrorCode());
8083
ErrorResponse response = new ErrorResponse(exception.getErrorCode());
8184
return ResponseEntity.status(exception.getStatus())
8285
.body(response);
8386
}
8487

8588
@ExceptionHandler(Exception.class)
8689
public ResponseEntity<ErrorResponse> handleException(Exception exception) {
90+
log.error("[Unhandled Exception] {}: {}", exception.getClass().getSimpleName(), exception.getMessage(), exception);
8791
return toErrorResponse(EtcErrorCode.INTERNAL_SERVER_ERROR);
8892
}
8993

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package eatda.repository.story;
2+
3+
import eatda.domain.story.Story;
4+
import eatda.exception.BusinessErrorCode;
5+
import eatda.exception.BusinessException;
6+
import java.util.Optional;
7+
import org.springframework.data.repository.Repository;
8+
9+
public interface StoryRepository extends Repository<Story, Long> {
10+
11+
Story save(Story story);
12+
13+
Optional<Story> findById(Long id);
14+
15+
default Story getById(Long id) {
16+
return findById(id)
17+
.orElseThrow(() -> new BusinessException(BusinessErrorCode.STORY_NOT_FOUND));
18+
}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package eatda.service.common;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
@Getter
7+
@RequiredArgsConstructor
8+
public enum ImageDomain {
9+
ARTICLE("article"),
10+
STORE("store"),
11+
MEMBER("member"),
12+
STORY("story");
13+
14+
private final String name;
15+
}

0 commit comments

Comments
 (0)