From 1f997e46508e617f22fdee12865e477db9ccba5a Mon Sep 17 00:00:00 2001 From: go Date: Thu, 7 May 2026 23:15:05 +0900 Subject: [PATCH] =?UTF-8?q?[6=EC=A3=BC=EC=B0=A8]=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=EB=B6=81=20=EB=AF=B8=EC=85=98=20-=20jaehyuk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jaehyuk/build.gradle | 38 ++++++++++ .../example/umc10th2/Umc10th2Application.java | 14 ++++ .../member/controller/MemberController.java | 47 ++++++++++++ .../member/converter/MemberConverter.java | 16 ++++ .../domain/member/dto/MemberReqDTO.java | 22 ++++++ .../domain/member/dto/MemberResDTO.java | 16 ++++ .../umc10th2/domain/member/entity/Food.java | 29 +++++++ .../umc10th2/domain/member/entity/Member.java | 76 +++++++++++++++++++ .../umc10th2/domain/member/entity/Term.java | 35 +++++++++ .../member/entity/mapping/MemberFood.java | 28 +++++++ .../member/entity/mapping/MemberTerm.java | 31 ++++++++ .../umc10th2/domain/member/enums/Gender.java | 3 + .../domain/member/enums/MemberStatus.java | 3 + .../domain/member/enums/SocialType.java | 3 + .../umc10th2/domain/member/enums/Term.java | 3 + .../member/exception/MemberException.java | 7 ++ .../exception/code/MemberErrorCode.java | 4 + .../exception/code/MemberSuccessCode.java | 4 + .../member/repository/MemberRepository.java | 11 +++ .../domain/member/service/MemberService.java | 24 ++++++ .../mission/controller/MissionController.java | 55 ++++++++++++++ .../mission/converter/MissionConverter.java | 68 +++++++++++++++++ .../domain/mission/dto/MissionReqDTO.java | 4 + .../domain/mission/dto/MissionResDTO.java | 56 ++++++++++++++ .../domain/mission/entity/Location.java | 36 +++++++++ .../domain/mission/entity/Mission.java | 40 ++++++++++ .../umc10th2/domain/mission/entity/Store.java | 47 ++++++++++++ .../mission/entity/mapping/MemberMission.java | 38 ++++++++++ .../domain/mission/enums/Address.java | 7 ++ .../domain/mission/enums/MissionStatus.java | 3 + .../mission/exception/MissionException.java | 7 ++ .../exception/code/MissionErrorCode.java | 4 + .../exception/code/MissionSuccessCode.java | 4 + .../repository/MemberMissionRepository.java | 33 ++++++++ .../mission/repository/MissionRepository.java | 31 ++++++++ .../mission/repository/StoreRepository.java | 7 ++ .../mission/service/MissionService.java | 57 ++++++++++++++ .../review/controller/ReviewController.java | 28 +++++++ .../review/converter/ReviewConverter.java | 29 +++++++ .../domain/review/dto/ReviewReqDTO.java | 14 ++++ .../domain/review/dto/ReviewResDTO.java | 19 +++++ .../umc10th2/domain/review/entity/Reply.java | 25 ++++++ .../umc10th2/domain/review/entity/Review.java | 37 +++++++++ .../domain/review/entity/ReviewPhoto.java | 25 ++++++ .../review/exception/ReviewException.java | 7 ++ .../exception/code/ReviewErrorCode.java | 4 + .../exception/code/ReviewSuccessCode.java | 4 + .../review/repository/ReviewRepository.java | 7 ++ .../domain/review/service/ReviewService.java | 38 ++++++++++ .../umc10th2/global/config/SwaggerConfig.java | 40 ++++++++++ .../umc10th2/global/entity/BaseEntity.java | 28 +++++++ .../exception/GlobalExceptionHandler.java | 14 ++++ .../umc10th2/global/response/ApiResponse.java | 27 +++++++ jaehyuk/src/main/resources/application.yml | 19 +++++ .../umc10th2/Umc10th2ApplicationTests.java | 13 ++++ 55 files changed, 1289 insertions(+) create mode 100644 jaehyuk/build.gradle create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/Umc10th2Application.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/controller/MemberController.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/converter/MemberConverter.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberReqDTO.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberResDTO.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Food.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Member.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Term.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberFood.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberTerm.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Gender.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/MemberStatus.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/SocialType.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Term.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/MemberException.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberErrorCode.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberSuccessCode.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/repository/MemberRepository.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/member/service/MemberService.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/controller/MissionController.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/converter/MissionConverter.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionReqDTO.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionResDTO.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Location.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Mission.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Store.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/mapping/MemberMission.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/Address.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/MissionStatus.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/MissionException.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionErrorCode.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionSuccessCode.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MemberMissionRepository.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MissionRepository.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/StoreRepository.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/mission/service/MissionService.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/controller/ReviewController.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/converter/ReviewConverter.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewReqDTO.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewResDTO.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Reply.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Review.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/ReviewPhoto.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/ReviewException.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewErrorCode.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewSuccessCode.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/repository/ReviewRepository.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/domain/review/service/ReviewService.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/global/config/SwaggerConfig.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/global/entity/BaseEntity.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/global/exception/GlobalExceptionHandler.java create mode 100644 jaehyuk/src/main/java/com/example/umc10th2/global/response/ApiResponse.java create mode 100644 jaehyuk/src/main/resources/application.yml create mode 100644 jaehyuk/src/test/java/com/example/umc10th2/Umc10th2ApplicationTests.java diff --git a/jaehyuk/build.gradle b/jaehyuk/build.gradle new file mode 100644 index 0000000..0f84362 --- /dev/null +++ b/jaehyuk/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.2' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' +description = 'umc10th2' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + runtimeOnly 'com.mysql:mysql-connector-j' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' +} + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/jaehyuk/src/main/java/com/example/umc10th2/Umc10th2Application.java b/jaehyuk/src/main/java/com/example/umc10th2/Umc10th2Application.java new file mode 100644 index 0000000..ea0408f --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/Umc10th2Application.java @@ -0,0 +1,14 @@ +package com.example.umc10th2; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@SpringBootApplication +public class Umc10th2Application { + + public static void main(String[] args) { + SpringApplication.run(Umc10th2Application.class, args); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/controller/MemberController.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/controller/MemberController.java new file mode 100644 index 0000000..6d8746e --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/controller/MemberController.java @@ -0,0 +1,47 @@ +package com.example.umc10th2.domain.member.controller; + +import com.example.umc10th2.domain.member.dto.MemberReqDTO; +import com.example.umc10th2.domain.member.dto.MemberResDTO; +import com.example.umc10th2.domain.member.service.MemberService; +import com.example.umc10th2.global.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Member", description = "회원 관련 API") +@RestController +@RequestMapping("/members") +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @Operation(summary = "회원가입") + @PostMapping("/signup") + public ApiResponse signup(@RequestBody MemberReqDTO request) { + return ApiResponse.success(null); + } + + @Operation(summary = "로그인") + @PostMapping("/login") + public ApiResponse login(@RequestBody MemberResDTO request) { + return ApiResponse.success(null); + } + + @Operation(summary = "마이페이지 조회", description = "닉네임, 이메일, 포인트 등 내 정보를 조회합니다.") + @GetMapping("/{memberId}") + public ApiResponse getMyInfo(@PathVariable Long memberId) { + return ApiResponse.success(memberService.getMyPage(memberId)); + } + + @Operation(summary = "포인트 내역 조회") + @GetMapping("/{memberId}/points") + public ApiResponse getPoints( + @PathVariable Long memberId, + @RequestParam int page, + @RequestParam int size + ) { + return ApiResponse.success(null); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/converter/MemberConverter.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/converter/MemberConverter.java new file mode 100644 index 0000000..486b383 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/converter/MemberConverter.java @@ -0,0 +1,16 @@ +package com.example.umc10th2.domain.member.converter; + +import com.example.umc10th2.domain.member.dto.MemberResDTO; +import com.example.umc10th2.domain.member.entity.Member; + +public class MemberConverter { + + public static MemberResDTO.MyPageRes toMyPageRes(Member member) { + return MemberResDTO.MyPageRes.builder() + .nickname(member.getNickname()) + .email(member.getEmail()) + .phoneNum(member.getPhoneNum()) + .point(member.getPoint()) + .build(); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberReqDTO.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberReqDTO.java new file mode 100644 index 0000000..566f50b --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberReqDTO.java @@ -0,0 +1,22 @@ +package com.example.umc10th2.domain.member.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MemberReqDTO { + private String name; + private String email; + private String password; + private String phone; + + private String gender; + private Integer birthYear; + private String address; + private List preferredCategories; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberResDTO.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberResDTO.java new file mode 100644 index 0000000..8e0b71d --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/dto/MemberResDTO.java @@ -0,0 +1,16 @@ +package com.example.umc10th2.domain.member.dto; + +import lombok.Builder; +import lombok.Getter; + +public class MemberResDTO { + + @Getter + @Builder + public static class MyPageRes { + private String nickname; + private String email; + private String phoneNum; + private Integer point; + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Food.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Food.java new file mode 100644 index 0000000..daaebec --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Food.java @@ -0,0 +1,29 @@ +package com.example.umc10th2.domain.member.entity; + +import com.example.umc10th2.domain.member.entity.mapping.MemberFood; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "food") +public class Food extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String name; + + @OneToMany(mappedBy = "food", cascade = CascadeType.ALL) + @Builder.Default + private List memberFoodList = new ArrayList<>(); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Member.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Member.java new file mode 100644 index 0000000..1f5c66e --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Member.java @@ -0,0 +1,76 @@ +package com.example.umc10th2.domain.member.entity; + +import com.example.umc10th2.domain.member.enums.Gender; +import com.example.umc10th2.domain.member.enums.MemberStatus; +import com.example.umc10th2.domain.member.enums.SocialType; +import com.example.umc10th2.domain.member.entity.mapping.MemberFood; +import com.example.umc10th2.domain.member.entity.mapping.MemberTerm; +import com.example.umc10th2.domain.mission.entity.mapping.MemberMission; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "member") +public class Member extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String name; + + @Column(nullable = false, unique = true, length = 100) + private String email; + + @Column(nullable = false) + private String password; + + @Column(name = "phone_num", length = 20) + private String phoneNum; + + @Column(length = 50) + private String nickname; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private Gender gender = Gender.NONE; + + @Column(name = "birth_year") + private Integer birthYear; + + @Enumerated(EnumType.STRING) + @Column(name = "social_type", nullable = false) + @Builder.Default + private SocialType socialType = SocialType.NONE; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private MemberStatus status = MemberStatus.ACTIVE; + + @Column(nullable = false) + @Builder.Default + private Integer point = 0; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + @Builder.Default + private List memberFoodList = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + @Builder.Default + private List memberTermList = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + @Builder.Default + private List memberMissionList = new ArrayList<>(); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Term.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Term.java new file mode 100644 index 0000000..eecaf48 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/Term.java @@ -0,0 +1,35 @@ +package com.example.umc10th2.domain.member.entity; + +import com.example.umc10th2.domain.member.entity.mapping.MemberTerm; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "term") +public class Term extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String title; + + @Column(columnDefinition = "TEXT") + private String body; + + @Column(name = "is_mandatory", nullable = false) + private Boolean mandatory; + + @OneToMany(mappedBy = "term", cascade = CascadeType.ALL) + @Builder.Default + private List memberTermList = new ArrayList<>(); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberFood.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberFood.java new file mode 100644 index 0000000..a468eb2 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberFood.java @@ -0,0 +1,28 @@ +package com.example.umc10th2.domain.member.entity.mapping; + +import com.example.umc10th2.domain.member.entity.Food; +import com.example.umc10th2.domain.member.entity.Member; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "member_food") +public class MemberFood extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "food_id", nullable = false) + private Food food; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberTerm.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberTerm.java new file mode 100644 index 0000000..2890b98 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/entity/mapping/MemberTerm.java @@ -0,0 +1,31 @@ +package com.example.umc10th2.domain.member.entity.mapping; + +import com.example.umc10th2.domain.member.entity.Member; +import com.example.umc10th2.domain.member.entity.Term; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "member_term") +public class MemberTerm extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "term_id", nullable = false) + private Term term; + + @Column(nullable = false) + private Boolean agreed; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Gender.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Gender.java new file mode 100644 index 0000000..d1e03b4 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Gender.java @@ -0,0 +1,3 @@ +package com.example.umc10th2.domain.member.enums; + +public enum Gender { MALE, FEMALE, NONE } diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/MemberStatus.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/MemberStatus.java new file mode 100644 index 0000000..76f5f6d --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/MemberStatus.java @@ -0,0 +1,3 @@ +package com.example.umc10th2.domain.member.enums; + +public enum MemberStatus { ACTIVE, INACTIVE, BANNED } diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/SocialType.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/SocialType.java new file mode 100644 index 0000000..8430e93 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/SocialType.java @@ -0,0 +1,3 @@ +package com.example.umc10th2.domain.member.enums; + +public enum SocialType { KAKAO, NAVER, GOOGLE, NONE } diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Term.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Term.java new file mode 100644 index 0000000..661a2be --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/enums/Term.java @@ -0,0 +1,3 @@ +package com.example.umc10th2.domain.member.enums; + +public enum Term { MANDATORY, OPTIONAL } diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/MemberException.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/MemberException.java new file mode 100644 index 0000000..eb67672 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/MemberException.java @@ -0,0 +1,7 @@ +package com.example.umc10th2.domain.member.exception; + +public class MemberException extends RuntimeException { + public MemberException(String message) { + super(message); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberErrorCode.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberErrorCode.java new file mode 100644 index 0000000..1c15198 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberErrorCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th2.domain.member.exception.code; + +public enum MemberErrorCode { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberSuccessCode.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberSuccessCode.java new file mode 100644 index 0000000..b0e54b1 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/exception/code/MemberSuccessCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th2.domain.member.exception.code; + +public enum MemberSuccessCode { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/repository/MemberRepository.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..0f5543a --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/repository/MemberRepository.java @@ -0,0 +1,11 @@ +package com.example.umc10th2.domain.member.repository; + +import com.example.umc10th2.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByIdAndDeletedAtIsNull(Long id); + boolean existsByEmail(String email); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/member/service/MemberService.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/service/MemberService.java new file mode 100644 index 0000000..aac3766 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/member/service/MemberService.java @@ -0,0 +1,24 @@ +package com.example.umc10th2.domain.member.service; + +import com.example.umc10th2.domain.member.converter.MemberConverter; +import com.example.umc10th2.domain.member.dto.MemberResDTO; +import com.example.umc10th2.domain.member.entity.Member; +import com.example.umc10th2.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberService { + + private final MemberRepository memberRepository; + + // 마이페이지: 내 정보 조회 + public MemberResDTO.MyPageRes getMyPage(Long memberId) { + Member member = memberRepository.findByIdAndDeletedAtIsNull(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + return MemberConverter.toMyPageRes(member); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/controller/MissionController.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..2ae2c41 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/controller/MissionController.java @@ -0,0 +1,55 @@ +package com.example.umc10th2.domain.mission.controller; + +import com.example.umc10th2.domain.mission.dto.MissionResDTO; +import com.example.umc10th2.domain.mission.service.MissionService; +import com.example.umc10th2.global.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Mission", description = "미션 관련 API") +@RestController +@RequestMapping("/missions") +@RequiredArgsConstructor +public class MissionController { + + private final MissionService missionService; + + @Operation( + summary = "내 미션 목록 조회", + description = "진행중(CHALLENGING) / 진행완료(COMPLETE) 미션을 페이징으로 조회합니다. status 미입력시 전체 조회." + ) + @GetMapping("/members/{memberId}") + public ApiResponse getMyMissions( + @PathVariable Long memberId, + @RequestParam(required = false) String status, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size + ) { + return ApiResponse.success(missionService.getMyMissions(memberId, status, page, size)); + } + + @Operation( + summary = "홈 화면 - 지역별 도전 가능한 미션 목록", + description = "선택된 지역(address)에서 아직 내가 도전하지 않은 미션을 페이징으로 조회합니다." + ) + @GetMapping("/home") + public ApiResponse getHomeMissions( + @RequestParam Long memberId, + @RequestParam String address, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size + ) { + return ApiResponse.success(missionService.getHomeMissions(memberId, address, page, size)); + } + + @Operation(summary = "미션 완료 처리") + @PatchMapping("/{missionId}/members/{memberId}/complete") + public ApiResponse completeMission( + @PathVariable Long memberId, + @PathVariable Long missionId + ) { + return ApiResponse.success(null); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/converter/MissionConverter.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/converter/MissionConverter.java new file mode 100644 index 0000000..1a6d131 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/converter/MissionConverter.java @@ -0,0 +1,68 @@ +package com.example.umc10th2.domain.mission.converter; + +import com.example.umc10th2.domain.mission.dto.MissionResDTO; +import com.example.umc10th2.domain.mission.entity.Mission; +import com.example.umc10th2.domain.mission.entity.mapping.MemberMission; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +public class MissionConverter { + + public static MissionResDTO.MyMissionItem toMyMissionItem(MemberMission mm) { + Mission mission = mm.getMission(); + return MissionResDTO.MyMissionItem.builder() + .memberMissionId(mm.getId()) + .missionId(mission.getId()) + .storeName(mission.getStore().getName()) + .introduction(mission.getIntroduction()) + .point(mission.getPoint()) + .deadline(mission.getDeadline()) + .status(mm.getStatus()) + .build(); + } + + public static MissionResDTO.MyMissionPageRes toMyMissionPageRes(Page page) { + List content = page.getContent() + .stream() + .map(MissionConverter::toMyMissionItem) + .collect(Collectors.toList()); + + return MissionResDTO.MyMissionPageRes.builder() + .content(content) + .page(page.getNumber() + 1) + .size(page.getSize()) + .totalElements(page.getTotalElements()) + .totalPages(page.getTotalPages()) + .last(page.isLast()) + .build(); + } + + public static MissionResDTO.HomeMissionItem toHomeMissionItem(Mission m) { + return MissionResDTO.HomeMissionItem.builder() + .missionId(m.getId()) + .storeName(m.getStore().getName()) + .introduction(m.getIntroduction()) + .point(m.getPoint()) + .deadline(m.getDeadline()) + .address(m.getStore().getLocation().getAddress().name()) + .build(); + } + + public static MissionResDTO.HomeMissionPageRes toHomeMissionPageRes(Page page) { + List content = page.getContent() + .stream() + .map(MissionConverter::toHomeMissionItem) + .collect(Collectors.toList()); + + return MissionResDTO.HomeMissionPageRes.builder() + .content(content) + .page(page.getNumber() + 1) + .size(page.getSize()) + .totalElements(page.getTotalElements()) + .totalPages(page.getTotalPages()) + .last(page.isLast()) + .build(); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionReqDTO.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionReqDTO.java new file mode 100644 index 0000000..fda32ca --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionReqDTO.java @@ -0,0 +1,4 @@ +package com.example.umc10th2.domain.mission.dto; + +public class MissionReqDTO { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionResDTO.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionResDTO.java new file mode 100644 index 0000000..9eed4e9 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/dto/MissionResDTO.java @@ -0,0 +1,56 @@ +package com.example.umc10th2.domain.mission.dto; + +import com.example.umc10th2.domain.mission.enums.MissionStatus; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.List; + +public class MissionResDTO { + + @Getter + @Builder + public static class MyMissionItem { + private Long memberMissionId; + private Long missionId; + private String storeName; + private String introduction; + private Integer point; + private LocalDate deadline; + private MissionStatus status; + } + + @Getter + @Builder + public static class MyMissionPageRes { + private List content; + private int page; + private int size; + private long totalElements; + private int totalPages; + private boolean last; + } + + @Getter + @Builder + public static class HomeMissionItem { + private Long missionId; + private String storeName; + private String introduction; + private Integer point; + private LocalDate deadline; + private String address; + } + + @Getter + @Builder + public static class HomeMissionPageRes { + private List content; + private int page; + private int size; + private long totalElements; + private int totalPages; + private boolean last; + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Location.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Location.java new file mode 100644 index 0000000..30c4c95 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Location.java @@ -0,0 +1,36 @@ +package com.example.umc10th2.domain.mission.entity; + +import com.example.umc10th2.domain.mission.enums.Address; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "location") +public class Location extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Address address; + + @Column(name = "x_pos", nullable = false) + private Double x; + + @Column(name = "y_pos", nullable = false) + private Double y; + + @OneToMany(mappedBy = "location", cascade = CascadeType.ALL) + @Builder.Default + private List storeList = new ArrayList<>(); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Mission.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Mission.java new file mode 100644 index 0000000..5c46ef1 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Mission.java @@ -0,0 +1,40 @@ +package com.example.umc10th2.domain.mission.entity; + +import com.example.umc10th2.domain.mission.entity.mapping.MemberMission; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "mission") +public class Mission extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id", nullable = false) + private Store store; + + @Column(nullable = false) + private Integer point; + + @Column(nullable = false, columnDefinition = "TEXT") + private String introduction; + + @Column(nullable = false) + private LocalDate deadline; + + @OneToMany(mappedBy = "mission", cascade = CascadeType.ALL) + @Builder.Default + private List memberMissionList = new ArrayList<>(); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Store.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Store.java new file mode 100644 index 0000000..9067b2e --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/Store.java @@ -0,0 +1,47 @@ +package com.example.umc10th2.domain.mission.entity; + +import com.example.umc10th2.domain.review.entity.Review; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "store") +public class Store extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id", nullable = false) + private Location location; + + @Column(nullable = false, length = 100) + private String name; + + @Column(name = "manager_number", length = 20) + private String managerNumber; + + @Column(name = "detail_address", length = 200) + private String detailAddress; + + @Column(nullable = false) + @Builder.Default + private Float score = 0.0f; + + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) + @Builder.Default + private List missionList = new ArrayList<>(); + + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) + @Builder.Default + private List reviewList = new ArrayList<>(); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/mapping/MemberMission.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/mapping/MemberMission.java new file mode 100644 index 0000000..ee58bbb --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/entity/mapping/MemberMission.java @@ -0,0 +1,38 @@ +package com.example.umc10th2.domain.mission.entity.mapping; + +import com.example.umc10th2.domain.member.entity.Member; +import com.example.umc10th2.domain.mission.entity.Mission; +import com.example.umc10th2.domain.mission.enums.MissionStatus; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "member_mission") +public class MemberMission extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "mission_id", nullable = false) + private Mission mission; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private MissionStatus status = MissionStatus.CHALLENGING; + + public void complete() { + this.status = MissionStatus.COMPLETE; + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/Address.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/Address.java new file mode 100644 index 0000000..c50026e --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/Address.java @@ -0,0 +1,7 @@ +package com.example.umc10th2.domain.mission.enums; + +public enum Address { + SEOUL, BUSAN, DAEGU, INCHEON, GWANGJU, DAEJEON, ULSAN, + SEJONG, GYEONGGI, GANGWON, CHUNGBUK, CHUNGNAM, + JEONBUK, JEONNAM, GYEONGBUK, GYEONGNAM, JEJU +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/MissionStatus.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/MissionStatus.java new file mode 100644 index 0000000..52a8255 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/enums/MissionStatus.java @@ -0,0 +1,3 @@ +package com.example.umc10th2.domain.mission.enums; + +public enum MissionStatus { AVAILABLE, CHALLENGING, COMPLETE } diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/MissionException.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/MissionException.java new file mode 100644 index 0000000..8c88144 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/MissionException.java @@ -0,0 +1,7 @@ +package com.example.umc10th2.domain.mission.exception; + +public class MissionException extends RuntimeException { + public MissionException(String message) { + super(message); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionErrorCode.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionErrorCode.java new file mode 100644 index 0000000..5810833 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionErrorCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th2.domain.mission.exception.code; + +public enum MissionErrorCode { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionSuccessCode.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionSuccessCode.java new file mode 100644 index 0000000..33bac8a --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/exception/code/MissionSuccessCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th2.domain.mission.exception.code; + +public enum MissionSuccessCode { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MemberMissionRepository.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MemberMissionRepository.java new file mode 100644 index 0000000..14a04a0 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MemberMissionRepository.java @@ -0,0 +1,33 @@ +package com.example.umc10th2.domain.mission.repository; + +import com.example.umc10th2.domain.mission.entity.mapping.MemberMission; +import com.example.umc10th2.domain.mission.enums.MissionStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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 MemberMissionRepository extends JpaRepository { + + // 내 미션 목록: 진행중 + 완료 필터 (status가 null이면 전체) + @Query(""" + SELECT mm FROM MemberMission mm + JOIN FETCH mm.mission mi + JOIN FETCH mi.store s + WHERE mm.member.id = :memberId + AND (:status IS NULL OR mm.status = :status) + AND mm.deletedAt IS NULL + """) + Page findMyMissions( + @Param("memberId") Long memberId, + @Param("status") MissionStatus status, + Pageable pageable + ); + + Optional findByIdAndMemberId(Long id, Long memberId); + + boolean existsByMemberIdAndMissionId(Long memberId, Long missionId); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MissionRepository.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MissionRepository.java new file mode 100644 index 0000000..1806d8c --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/MissionRepository.java @@ -0,0 +1,31 @@ +package com.example.umc10th2.domain.mission.repository; + +import com.example.umc10th2.domain.mission.entity.Mission; +import com.example.umc10th2.domain.mission.enums.Address; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MissionRepository extends JpaRepository { + + // 홈화면: 해당 지역에서 도전 가능한(아직 내가 도전 안 한) 미션 목록 + @Query(""" + SELECT m FROM Mission m + JOIN m.store s + JOIN s.location l + WHERE l.address = :address + AND m.deletedAt IS NULL + AND NOT EXISTS ( + SELECT mm FROM MemberMission mm + WHERE mm.mission = m + AND mm.member.id = :memberId + ) + """) + Page findAvailableMissionsByAddress( + @Param("address") Address address, + @Param("memberId") Long memberId, + Pageable pageable + ); +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/StoreRepository.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/StoreRepository.java new file mode 100644 index 0000000..87c199e --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/repository/StoreRepository.java @@ -0,0 +1,7 @@ +package com.example.umc10th2.domain.mission.repository; + +import com.example.umc10th2.domain.mission.entity.Store; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StoreRepository extends JpaRepository { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/service/MissionService.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/service/MissionService.java new file mode 100644 index 0000000..709ac6d --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/mission/service/MissionService.java @@ -0,0 +1,57 @@ +package com.example.umc10th2.domain.mission.service; + +import com.example.umc10th2.domain.mission.converter.MissionConverter; +import com.example.umc10th2.domain.mission.dto.MissionResDTO; +import com.example.umc10th2.domain.mission.entity.Mission; +import com.example.umc10th2.domain.mission.entity.mapping.MemberMission; +import com.example.umc10th2.domain.mission.enums.Address; +import com.example.umc10th2.domain.mission.enums.MissionStatus; +import com.example.umc10th2.domain.mission.repository.MemberMissionRepository; +import com.example.umc10th2.domain.mission.repository.MissionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MissionService { + + private final MissionRepository missionRepository; + private final MemberMissionRepository memberMissionRepository; + + // 내 미션 목록: 진행중(CHALLENGING) / 완료(COMPLETE) 필터 + 페이징 + public MissionResDTO.MyMissionPageRes getMyMissions( + Long memberId, String statusStr, int page, int size) { + + MissionStatus status = (statusStr != null) + ? MissionStatus.valueOf(statusStr.toUpperCase()) + : null; + + PageRequest pageable = PageRequest.of(page - 1, size, + Sort.by(Sort.Direction.DESC, "createdAt")); + + Page result = + memberMissionRepository.findMyMissions(memberId, status, pageable); + + return MissionConverter.toMyMissionPageRes(result); + } + + // 홈 화면: 선택된 지역에서 도전 가능한 미션 목록 + 페이징 + public MissionResDTO.HomeMissionPageRes getHomeMissions( + Long memberId, String addressStr, int page, int size) { + + Address address = Address.valueOf(addressStr.toUpperCase()); + + PageRequest pageable = PageRequest.of(page - 1, size, + Sort.by(Sort.Direction.ASC, "deadline")); + + Page result = + missionRepository.findAvailableMissionsByAddress(address, memberId, pageable); + + return MissionConverter.toHomeMissionPageRes(result); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/controller/ReviewController.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/controller/ReviewController.java new file mode 100644 index 0000000..73cd971 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/controller/ReviewController.java @@ -0,0 +1,28 @@ +package com.example.umc10th2.domain.review.controller; + +import com.example.umc10th2.domain.review.dto.ReviewReqDTO; +import com.example.umc10th2.domain.review.dto.ReviewResDTO; +import com.example.umc10th2.domain.review.service.ReviewService; +import com.example.umc10th2.global.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Review", description = "리뷰 관련 API") +@RestController +@RequestMapping("/reviews") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @Operation(summary = "리뷰 작성", description = "가게에 대한 리뷰를 작성합니다. (사진 제외)") + @PostMapping("/members/{memberId}") + public ApiResponse createReview( + @PathVariable Long memberId, + @RequestBody ReviewReqDTO request + ) { + return ApiResponse.success(reviewService.createReview(memberId, request)); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/converter/ReviewConverter.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/converter/ReviewConverter.java new file mode 100644 index 0000000..b6086f1 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/converter/ReviewConverter.java @@ -0,0 +1,29 @@ +package com.example.umc10th2.domain.review.converter; + +import com.example.umc10th2.domain.member.entity.Member; +import com.example.umc10th2.domain.mission.entity.Store; +import com.example.umc10th2.domain.review.dto.ReviewReqDTO; +import com.example.umc10th2.domain.review.dto.ReviewResDTO; +import com.example.umc10th2.domain.review.entity.Review; + +public class ReviewConverter { + + public static Review toReview(ReviewReqDTO req, Member member, Store store) { + return Review.builder() + .member(member) + .store(store) + .rating(req.getRating()) + .content(req.getContent()) + .build(); + } + + public static ReviewResDTO.CreateRes toCreateRes(Review review) { + return ReviewResDTO.CreateRes.builder() + .reviewId(review.getId()) + .storeId(review.getStore().getId()) + .rating(review.getRating()) + .content(review.getContent()) + .createdAt(review.getCreatedAt()) + .build(); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewReqDTO.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewReqDTO.java new file mode 100644 index 0000000..8bb5465 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewReqDTO.java @@ -0,0 +1,14 @@ +package com.example.umc10th2.domain.review.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class ReviewReqDTO { + private Long storeId; + private Float rating; + private String content; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewResDTO.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewResDTO.java new file mode 100644 index 0000000..46b22f9 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/dto/ReviewResDTO.java @@ -0,0 +1,19 @@ +package com.example.umc10th2.domain.review.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +public class ReviewResDTO { + + @Getter + @Builder + public static class CreateRes { + private Long reviewId; + private Long storeId; + private Float rating; + private String content; + private LocalDateTime createdAt; + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Reply.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Reply.java new file mode 100644 index 0000000..360af5f --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Reply.java @@ -0,0 +1,25 @@ +package com.example.umc10th2.domain.review.entity; + +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "reply") +public class Reply extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id", nullable = false) + private Review review; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Review.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Review.java new file mode 100644 index 0000000..6a1d060 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/Review.java @@ -0,0 +1,37 @@ +package com.example.umc10th2.domain.review.entity; + +import com.example.umc10th2.domain.member.entity.Member; +import com.example.umc10th2.domain.mission.entity.Store; +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "review") +public class Review extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id", nullable = false) + private Store store; + + @Column(nullable = false) + private Float rating; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @OneToOne(mappedBy = "review", cascade = CascadeType.ALL) + private Reply reply; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/ReviewPhoto.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/ReviewPhoto.java new file mode 100644 index 0000000..90375c1 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/entity/ReviewPhoto.java @@ -0,0 +1,25 @@ +package com.example.umc10th2.domain.review.entity; + +import com.example.umc10th2.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "review_photo") +public class ReviewPhoto extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id", nullable = false) + private Review review; + + @Column(name = "photo_url", nullable = false) + private String photoUrl; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/ReviewException.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/ReviewException.java new file mode 100644 index 0000000..fb654d9 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/ReviewException.java @@ -0,0 +1,7 @@ +package com.example.umc10th2.domain.review.exception; + +public class ReviewException extends RuntimeException { + public ReviewException(String message) { + super(message); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewErrorCode.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewErrorCode.java new file mode 100644 index 0000000..d1379f2 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewErrorCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th2.domain.review.exception.code; + +public enum ReviewErrorCode { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewSuccessCode.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewSuccessCode.java new file mode 100644 index 0000000..d18326b --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/exception/code/ReviewSuccessCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th2.domain.review.exception.code; + +public enum ReviewSuccessCode { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/repository/ReviewRepository.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/repository/ReviewRepository.java new file mode 100644 index 0000000..6785b49 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/repository/ReviewRepository.java @@ -0,0 +1,7 @@ +package com.example.umc10th2.domain.review.repository; + +import com.example.umc10th2.domain.review.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository { +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/domain/review/service/ReviewService.java b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/service/ReviewService.java new file mode 100644 index 0000000..048c51c --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/domain/review/service/ReviewService.java @@ -0,0 +1,38 @@ +package com.example.umc10th2.domain.review.service; + +import com.example.umc10th2.domain.member.entity.Member; +import com.example.umc10th2.domain.member.repository.MemberRepository; +import com.example.umc10th2.domain.mission.entity.Store; +import com.example.umc10th2.domain.mission.repository.StoreRepository; +import com.example.umc10th2.domain.review.converter.ReviewConverter; +import com.example.umc10th2.domain.review.dto.ReviewReqDTO; +import com.example.umc10th2.domain.review.dto.ReviewResDTO; +import com.example.umc10th2.domain.review.entity.Review; +import com.example.umc10th2.domain.review.repository.ReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final MemberRepository memberRepository; + private final StoreRepository storeRepository; + + @Transactional + public ReviewResDTO.CreateRes createReview(Long memberId, ReviewReqDTO req) { + Member member = memberRepository.findByIdAndDeletedAtIsNull(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + + Store store = storeRepository.findById(req.getStoreId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 가게입니다.")); + + Review review = ReviewConverter.toReview(req, member, store); + reviewRepository.save(review); + + return ReviewConverter.toCreateRes(review); + } +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/global/config/SwaggerConfig.java b/jaehyuk/src/main/java/com/example/umc10th2/global/config/SwaggerConfig.java new file mode 100644 index 0000000..2c4bccd --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/global/config/SwaggerConfig.java @@ -0,0 +1,40 @@ +package com.example.umc10th2.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; // ✅ 수정 +import io.swagger.v3.oas.models.servers.Server; // ✅ 수정 +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration // ✅ 여기 하나만 있으면 됨 +public class SwaggerConfig { + + @Bean + public OpenAPI swagger() { + Info info = new Info() + .title("UMC10th") + .description("10기 Swagger") + .version("0.0.1"); + + String securityScheme = "JWT TOKEN"; + + SecurityRequirement securityRequirement = + new SecurityRequirement().addList(securityScheme); + + Components components = new Components() + .addSecuritySchemes(securityScheme, new SecurityScheme() + .name(securityScheme) + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT")); + + return new OpenAPI() + .info(info) + .addServersItem(new Server().url("/")) + .addSecurityItem(securityRequirement) + .components(components); + } +} \ No newline at end of file diff --git a/jaehyuk/src/main/java/com/example/umc10th2/global/entity/BaseEntity.java b/jaehyuk/src/main/java/com/example/umc10th2/global/entity/BaseEntity.java new file mode 100644 index 0000000..099573f --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/global/entity/BaseEntity.java @@ -0,0 +1,28 @@ +package com.example.umc10th2.global.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; +} diff --git a/jaehyuk/src/main/java/com/example/umc10th2/global/exception/GlobalExceptionHandler.java b/jaehyuk/src/main/java/com/example/umc10th2/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..35bb4ec --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,14 @@ +package com.example.umc10th2.global.exception; + +import com.example.umc10th2.global.response.ApiResponse; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ApiResponse handleException(Exception e) { + return ApiResponse.fail("COMMON500", e.getMessage()); + } +} \ No newline at end of file diff --git a/jaehyuk/src/main/java/com/example/umc10th2/global/response/ApiResponse.java b/jaehyuk/src/main/java/com/example/umc10th2/global/response/ApiResponse.java new file mode 100644 index 0000000..614bc55 --- /dev/null +++ b/jaehyuk/src/main/java/com/example/umc10th2/global/response/ApiResponse.java @@ -0,0 +1,27 @@ +package com.example.umc10th2.global.response; + +import lombok.Getter; + +@Getter +public class ApiResponse { + + private final boolean isSuccess; + private final String code; + private final String message; + private final T result; + + private ApiResponse(boolean isSuccess, String code, String message, T result) { + this.isSuccess = isSuccess; + this.code = code; + this.message = message; + this.result = result; + } + + public static ApiResponse success(T result) { + return new ApiResponse<>(true, "COMMON200", "성공", result); + } + + public static ApiResponse fail(String code, String message) { + return new ApiResponse<>(false, code, message, null); + } +} diff --git a/jaehyuk/src/main/resources/application.yml b/jaehyuk/src/main/resources/application.yml new file mode 100644 index 0000000..9a7d6e6 --- /dev/null +++ b/jaehyuk/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + application: + name: "프로젝트 명" # "umc10th2" + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC 드라이버 클래스 이름 + url: jdbc:mysql://localhost:3306/umc10th2?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: 285409aa! + + jpa: + database: mysql # 사용할 데이터베이스 유형 지정 (MySQL) + database-platform: org.hibernate.dialect.MySQLDialect # Hibernate에서 사용할 MySQL 방언(dialect) 설정 + show-sql: true # 실행된 SQL 쿼리를 콘솔에 출력할지 여부 설정 + hibernate: + ddl-auto: update # 애플리케이션 실행 시 데이터베이스 스키마의 상태를 설정 + properties: + hibernate: + format_sql: true # 출력되는 SQL 쿼리를 보기 좋게 포맷팅 \ No newline at end of file diff --git a/jaehyuk/src/test/java/com/example/umc10th2/Umc10th2ApplicationTests.java b/jaehyuk/src/test/java/com/example/umc10th2/Umc10th2ApplicationTests.java new file mode 100644 index 0000000..b1ad01a --- /dev/null +++ b/jaehyuk/src/test/java/com/example/umc10th2/Umc10th2ApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.umc10th2; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Umc10th2ApplicationTests { + + @Test + void contextLoads() { + } + +}