diff --git a/umc9th/.gitignore b/umc9th/.gitignore index c2065bc..5fc642b 100644 --- a/umc9th/.gitignore +++ b/umc9th/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +# Spring Boot application configuration +application.yml \ No newline at end of file diff --git a/umc9th/build.gradle b/umc9th/build.gradle index c663a55..a1c7908 100644 --- a/umc9th/build.gradle +++ b/umc9th/build.gradle @@ -35,8 +35,41 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // QueryDSL : OpenFeign + implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" + implementation "io.github.openfeign.querydsl:querydsl-core:7.0" + annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13' + + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { useJUnitPlatform() } + +// QueryDSL 관련 설정 +// generated/querydsl 폴더 생성 & 삽입 +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +// 소스 세트에 생성 경로 추가 (구체적인 경로 지정) +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +// 컴파일 시 생성 경로 지정 +tasks.withType(JavaCompile).configureEach { + options.generatedSourceOutputDirectory.set(querydslDir) +} + +// clean 태스크에 생성 폴더 삭제 로직 추가 +clean.doLast { + file(querydslDir).deleteDir() +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java b/umc9th/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java new file mode 100644 index 0000000..a67f97c --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java @@ -0,0 +1,29 @@ +package com.example.umc9th.domain.member.controller; + +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; +import com.example.umc9th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc9th.domain.member.service.command.MemberCommandService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class MemberController { + + private final MemberCommandService memberCommandService; + + // 회원가입 + @PostMapping("/sign-up") + public ApiResponse signUp( + @RequestBody @Valid MemberReqDTO.JoinDTO dto + ){ + return ApiResponse.onSuccess(MemberSuccessCode.FOUND, memberCommandService.signup(dto)); + } + + +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java b/umc9th/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java new file mode 100644 index 0000000..ec9676d --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java @@ -0,0 +1,31 @@ +package com.example.umc9th.domain.member.converter; + +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; +import com.example.umc9th.domain.member.entity.Member; + +public class MemberConverter { + + // Entity -> DTO + public static MemberResDTO.JoinDTO toJoinDTO( + Member member + ){ + return MemberResDTO.JoinDTO.builder() + .memberId(member.getId()) + .createAt(member.getCreatedAt()) + .build(); + } + + // DTO -> Entity + public static Member toMember(MemberReqDTO.JoinDTO dto) { + return Member.builder() + .name(dto.name()) + .birth(dto.birth()) + .address(dto.address()) + .gender(dto.gender()) + .email(dto.email()) + .password(dto.password()) + .phone(dto.phone()) + .build(); + } +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java b/umc9th/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java new file mode 100644 index 0000000..4589e75 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java @@ -0,0 +1,22 @@ +package com.example.umc9th.domain.member.dto; + +import com.example.umc9th.domain.member.enums.Gender; +import com.example.umc9th.global.annotation.ExistFoods; + +import java.time.LocalDate; +import java.util.List; + +public class MemberReqDTO { + + public record JoinDTO( + String name, + Gender gender, + LocalDate birth, + String address, + String email, + String password, + String phone, + @ExistFoods + List preferCategory + ){} +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/dto/MemberResDTO.java b/umc9th/src/main/java/com/example/umc9th/domain/member/dto/MemberResDTO.java new file mode 100644 index 0000000..9bc5c5e --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/dto/MemberResDTO.java @@ -0,0 +1,14 @@ +package com.example.umc9th.domain.member.dto; + +import lombok.Builder; + +import java.time.LocalDateTime; + +public class MemberResDTO { + + @Builder + public record JoinDTO( + Long memberId, + LocalDateTime createAt + ){} +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Interest.java b/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Interest.java index 2701268..4a83acd 100644 --- a/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Interest.java +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Interest.java @@ -22,9 +22,9 @@ public class Interest { @Enumerated(EnumType.STRING) private FoodName name; - //연관 관계 - @OneToMany(mappedBy = "interest") - private List memberInterests = new ArrayList<>(); +// //연관 관계 +// @OneToMany(mappedBy = "interest") +// private List memberInterests = new ArrayList<>(); } diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Member.java b/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Member.java index e6387f3..c3dd01f 100644 --- a/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Member.java +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/entity/Member.java @@ -2,6 +2,7 @@ import com.example.umc9th.domain.member.enums.Gender; import com.example.umc9th.global.entity.BaseEntity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; import java.time.LocalDate; @@ -47,11 +48,13 @@ public class Member extends BaseEntity { @Column(name = "status") private boolean status; - //연관 관계 - @OneToMany(mappedBy = "member") - private List memberInterests = new ArrayList<>(); +// //연관 관계 +// @OneToMany(mappedBy = "member") +// @JsonIgnore +// private List memberInterests = new ArrayList<>(); - @OneToMany(mappedBy = "member") - private List memberTerms = new ArrayList<>(); +// @OneToMany(mappedBy = "member") +// @JsonIgnore +// private List memberTerms = new ArrayList<>(); } diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/enums/FoodName.java b/umc9th/src/main/java/com/example/umc9th/domain/member/enums/FoodName.java index 701fc3d..a73d87e 100644 --- a/umc9th/src/main/java/com/example/umc9th/domain/member/enums/FoodName.java +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/enums/FoodName.java @@ -1,5 +1,5 @@ package com.example.umc9th.domain.member.enums; public enum FoodName { - A, B, C, D, E, F; + KOREAN, JAPAN, CHINA; } diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/exception/InterestException.java b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/InterestException.java new file mode 100644 index 0000000..d9e83c2 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/InterestException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.member.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class InterestException extends GeneralException { + public InterestException(BaseErrorCode code) { + super(code); + } +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/exception/MemberException.java b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/MemberException.java new file mode 100644 index 0000000..43f63f3 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/MemberException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.member.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class MemberException extends GeneralException { + public MemberException(BaseErrorCode code) { + super(code); + } +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/InterestErrorCode.java b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/InterestErrorCode.java new file mode 100644 index 0000000..e3bd901 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/InterestErrorCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.member.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum InterestErrorCode implements BaseErrorCode { + + NOT_FOUND(HttpStatus.NOT_FOUND, + "INTERSET404_1", + "해당 관심음식을 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/InterestSuccessCode.java b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/InterestSuccessCode.java new file mode 100644 index 0000000..c905377 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/InterestSuccessCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.member.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum InterestSuccessCode implements BaseSuccessCode { + + FOUND(HttpStatus.OK, + "INTEREST200_1", + "성공적으로 관심음식를 조회했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java new file mode 100644 index 0000000..64645ae --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.member.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MemberErrorCode implements BaseErrorCode { + + NOT_FOUND(HttpStatus.NOT_FOUND, + "MEMBER404_1", + "해당 사용자를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/MemberSuccessCode.java b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/MemberSuccessCode.java new file mode 100644 index 0000000..5b1e0e0 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/exception/code/MemberSuccessCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.member.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MemberSuccessCode implements BaseSuccessCode { + + FOUND(HttpStatus.OK, + "MEMBER200_1", + "성공적으로 사용자를 조회했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/repository/InterestRepository.java b/umc9th/src/main/java/com/example/umc9th/domain/member/repository/InterestRepository.java new file mode 100644 index 0000000..57bc460 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/repository/InterestRepository.java @@ -0,0 +1,7 @@ +package com.example.umc9th.domain.member.repository; + +import com.example.umc9th.domain.member.entity.Interest; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterestRepository extends JpaRepository { +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/repository/MemberInterestRepository.java b/umc9th/src/main/java/com/example/umc9th/domain/member/repository/MemberInterestRepository.java new file mode 100644 index 0000000..9b2bddd --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/repository/MemberInterestRepository.java @@ -0,0 +1,7 @@ +package com.example.umc9th.domain.member.repository; + +import com.example.umc9th.domain.member.entity.MemberInterest; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberInterestRepository extends JpaRepository { +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java b/umc9th/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..a9a3f27 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java @@ -0,0 +1,9 @@ +package com.example.umc9th.domain.member.repository; + +import com.example.umc9th.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByIdAndStatusIsTrue(Long memberId); +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandService.java b/umc9th/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandService.java new file mode 100644 index 0000000..03ec6f2 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandService.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.member.service.command; + +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; + +public interface MemberCommandService { + public MemberResDTO.JoinDTO signup( + MemberReqDTO.JoinDTO dto + ); +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java b/umc9th/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java new file mode 100644 index 0000000..3793a11 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java @@ -0,0 +1,70 @@ +package com.example.umc9th.domain.member.service.command; + +import com.example.umc9th.domain.member.converter.MemberConverter; +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; +import com.example.umc9th.domain.member.entity.Interest; +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.entity.MemberInterest; +import com.example.umc9th.domain.member.exception.InterestException; +import com.example.umc9th.domain.member.exception.code.InterestErrorCode; +import com.example.umc9th.domain.member.repository.InterestRepository; +import com.example.umc9th.domain.member.repository.MemberInterestRepository; +import com.example.umc9th.domain.member.repository.MemberRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MemberCommandServiceImpl implements MemberCommandService{ + + private final MemberRepository memberRepository; + private final MemberInterestRepository memberInterestRepository; + private final InterestRepository interestRepository; + + // 회원가입 + @Override + @Transactional + public MemberResDTO.JoinDTO signup( + MemberReqDTO.JoinDTO dto + ){ + //사용자 생성 + Member member = MemberConverter.toMember(dto); + //DB 적용 + memberRepository.save(member); + + //선호 음식 여부 존재 확인 + if(dto.preferCategory().size() > 0){ + List memberInterestList = new ArrayList<>(); + + // 선호 음식 ID별 조회 + for (Long id : dto.preferCategory()){ + + // 음식 존재 여부 검증 + Interest interest = interestRepository.findById(id) + .orElseThrow(() -> new InterestException(InterestErrorCode.NOT_FOUND)); + + // MemberFood 엔티티 생성 (컨버터 사용해야 함) + MemberInterest memberFood = MemberInterest.builder() + .member(member) + .interest(interest) + .build(); + + // 사용자 - 음식 (선호 음식) 추가 + memberInterestList.add(memberFood); + } + + // 모든 관심 음식 추가: DB 적용 + memberInterestRepository.saveAll(memberInterestList); + + + } + // 응답 DTO 생성 + return MemberConverter.toJoinDTO(member); + + } +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryService.java b/umc9th/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryService.java new file mode 100644 index 0000000..286df25 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryService.java @@ -0,0 +1,4 @@ +package com.example.umc9th.domain.member.service.query; + +public class MemberQueryService { +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryServiceImpl.java b/umc9th/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryServiceImpl.java new file mode 100644 index 0000000..7ce41c8 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryServiceImpl.java @@ -0,0 +1,4 @@ +package com.example.umc9th.domain.member.service.query; + +public class MemberQueryServiceImpl { +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..9a5e558 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java @@ -0,0 +1,75 @@ +package com.example.umc9th.domain.mission.controller; + +import com.example.umc9th.domain.mission.dto.MissionReqDTO; +import com.example.umc9th.domain.mission.dto.MissionResDTO; +import com.example.umc9th.domain.mission.exception.code.MissionSuccessCode; +import com.example.umc9th.domain.mission.service.MissionService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/missions") +public class MissionController { + + private final MissionService missionService; + + // 미션 도전하기 + @PostMapping("/challenge") + public ApiResponse challengeMission( + @RequestBody MissionReqDTO.ChallengeDTO dto) { + + MissionResDTO.ChallengeDTO result = + missionService.challengeMission(dto.memberId(), dto.missionId(), dto.deadline()); + + return ApiResponse.onSuccess(MissionSuccessCode.CREATE, result); + } + + // 내 미션 목록 조회 (진행중 + 성공) + @GetMapping("/my/{memberId}") + public ApiResponse> getMyMissions(@PathVariable Long memberId) { + return ApiResponse.onSuccess( + MissionSuccessCode.OK, + missionService.getMyMissions(memberId) + ); + } + + // 미션 성공 처리 + @PostMapping("/success") + public ApiResponse successMission( + @RequestBody MissionReqDTO.SuccessDTO dto) { + + MissionResDTO.SuccessDTO result = + missionService.successMission(dto.memberId(), dto.userMissionId()); + + return ApiResponse.onSuccess(MissionSuccessCode.SUCCESS, result); + } + + // 특정 가게의 미션 목록 (페이징, 1페이지부터 시작) + @GetMapping("/store/{storeId}") + public ApiResponse getStoreMissions( + @PathVariable Long storeId, + @RequestParam(name = "page") int page // 나중에 @PositivePage 같은 커스텀 어노테이션 붙이면 됨 + ) { + MissionResDTO.StoreMissionPageDTO result = + missionService.getStoreMissions(storeId, page); + + return ApiResponse.onSuccess(MissionSuccessCode.OK, result); + } + + //내가 진행중인 미션(status = true)만 조회 + @GetMapping("/my/{memberId}/challenging") + public ApiResponse getMyChallengingMissions( + @PathVariable Long memberId, + @RequestParam(name = "page") int page + ) { + MissionResDTO.MyMissionPageDTO result = + missionService.getMyChallengingMissions(memberId, page); + + return ApiResponse.onSuccess(MissionSuccessCode.OK, result); + } + +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java new file mode 100644 index 0000000..b075554 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java @@ -0,0 +1,93 @@ +package com.example.umc9th.domain.mission.converter; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.mission.dto.MissionResDTO; +import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.mission.entity.UserMission; +import org.springframework.data.domain.Page; + +import java.time.LocalDate; +import java.util.List; + +public class MissionConverter { + + // UserMission 생성 (도전하기) + public static UserMission toUserMission(Member member, Mission mission, LocalDate deadline) { + return UserMission.builder() + .member(member) + .mission(mission) + .deadline(deadline) + .status(true) // 도전 시작 → 진행중 + .build(); + } + + // 도전 응답 DTO + public static MissionResDTO.ChallengeDTO toChallengeDTO(UserMission userMission) { + return MissionResDTO.ChallengeDTO.builder() + .userMissionId(userMission.getId()) + .deadline(userMission.getDeadline()) + .build(); + } + + // 내 미션 목록 DTO + public static MissionResDTO.MyMissionDTO toMyMissionDTO(UserMission um) { + return MissionResDTO.MyMissionDTO.builder() + .userMissionId(um.getId()) + .missionId(um.getMission().getId()) + .missionStatus(um.isStatus() ? "CHALLENGING" : "SUCCESS") + .point(um.getMission().getPoint()) + .storeName(um.getMission().getStore().getName()) + .deadline(um.getDeadline()) + .build(); + } + + // 미션 성공 응답 DTO + public static MissionResDTO.SuccessDTO toSuccessDTO(UserMission um) { + return MissionResDTO.SuccessDTO.builder() + .userMissionId(um.getId()) + .missionStatus("SUCCESS") + .build(); + } + + // 특정 가게 미션 단일 DTO + public static MissionResDTO.StoreMissionDTO toStoreMissionDTO(Mission mission) { + return MissionResDTO.StoreMissionDTO.builder() + .missionId(mission.getId()) + .point(mission.getPoint()) + .status(mission.isStatus()) + .build(); + } + + // 특정 가게 미션 페이징 DTO + public static MissionResDTO.StoreMissionPageDTO toStoreMissionPageDTO(Page missionPage) { + + List list = missionPage.getContent().stream() + .map(MissionConverter::toStoreMissionDTO) + .toList(); + + return MissionResDTO.StoreMissionPageDTO.builder() + .contents(list) + .page(missionPage.getNumber() + 1) // 내부는 0부터, 응답은 1부터 + .size(missionPage.getSize()) + .totalElements(missionPage.getTotalElements()) + .totalPages(missionPage.getTotalPages()) + .build(); + } + + public static MissionResDTO.MyMissionPageDTO toMyMissionPageDTO( + Page missionPage + ) { + + List list = missionPage.getContent().stream() + .map(MissionConverter::toMyMissionDTO) + .toList(); + + return MissionResDTO.MyMissionPageDTO.builder() + .contents(list) + .page(missionPage.getNumber() + 1) + .size(missionPage.getSize()) + .totalElements(missionPage.getTotalElements()) + .totalPages(missionPage.getTotalPages()) + .build(); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/HomeMissionDto.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/HomeMissionDto.java new file mode 100644 index 0000000..b76afc8 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/HomeMissionDto.java @@ -0,0 +1,22 @@ +package com.example.umc9th.domain.mission.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 홈 화면에서 지역별 미참여 미션을 조회할 때 사용되는 DTO + * MissionRepository의 JPQL DTO Projection에서 사용됨. + */ +@Getter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class HomeMissionDto { + + private String region; // 사용자가 선택한 지역명 + private Long missionId; // 미션 ID + private String storeName; // 가게 이름 + private int point; // 미션 포인트 +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/MissionReqDTO.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/MissionReqDTO.java new file mode 100644 index 0000000..bb96078 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/MissionReqDTO.java @@ -0,0 +1,19 @@ +package com.example.umc9th.domain.mission.dto; + +import java.time.LocalDate; + +public class MissionReqDTO { + + // 미션 도전하기 요청 + public record ChallengeDTO( + Long memberId, + Long missionId, + LocalDate deadline // null이면 서비스에서 기본값(예: +7일) 설정 + ) {} + + // 미션 성공 처리 요청 + public record SuccessDTO( + Long memberId, + Long userMissionId // 도전한 user_mission PK + ) {} +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/MissionResDTO.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/MissionResDTO.java new file mode 100644 index 0000000..defe36a --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/dto/MissionResDTO.java @@ -0,0 +1,69 @@ +package com.example.umc9th.domain.mission.dto; + +import lombok.Builder; + +import java.time.LocalDate; +import java.util.List; + +public class MissionResDTO { + + // 미션 도전 성공 응답 + @Builder + public record ChallengeDTO( + Long userMissionId, + LocalDate deadline + ) {} + + // 내 미션 목록 (진행중/성공) + @Builder + public record MyMissionDTO( + Long userMissionId, + Long missionId, + String missionStatus, // "CHALLENGING" 또는 "SUCCESS" + int point, + String storeName, + LocalDate deadline + ) {} + + // 미션 단일 정보 (필요하면 사용) + @Builder + public record MissionDTO( + Long missionId, + int point, + boolean status + ) {} + + // 미션 성공 처리 응답 + @Builder + public record SuccessDTO( + Long userMissionId, + String missionStatus // 항상 "SUCCESS" + ) {} + + // 특정 가게의 미션 단일 DTO + @Builder + public record StoreMissionDTO( + Long missionId, + int point, + boolean status + ) {} + + // 특정 가게의 미션 페이징 DTO + @Builder + public record StoreMissionPageDTO( + List contents, + int page, + int size, + long totalElements, + int totalPages + ) {} + + @Builder + public record MyMissionPageDTO( + List contents, + int page, + int size, + long totalElements, + int totalPages + ) {} +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/entity/UserMission.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/entity/UserMission.java index d596315..e3de7fa 100644 --- a/umc9th/src/main/java/com/example/umc9th/domain/mission/entity/UserMission.java +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/entity/UserMission.java @@ -22,6 +22,8 @@ public class UserMission extends BaseEntity { @Column(name = "deadline", nullable = false) private LocalDate deadline; + // true = 진행중(CHALLENGING) + // false = 성공(SUCCESS) @Column(name = "status", nullable = false) private boolean status; @@ -33,4 +35,9 @@ public class UserMission extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "mission_id", nullable = false) private Mission mission; + + // ==== 비즈니스 메서드 ==== + public void complete() { + this.status = false; + } } diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/MissionException.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/MissionException.java new file mode 100644 index 0000000..222838a --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/MissionException.java @@ -0,0 +1,15 @@ +package com.example.umc9th.domain.mission.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.Getter; + +@Getter +public class MissionException extends RuntimeException { + + private final BaseErrorCode errorCode; + + public MissionException(BaseErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionErrorCode.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionErrorCode.java new file mode 100644 index 0000000..251ba56 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionErrorCode.java @@ -0,0 +1,27 @@ +package com.example.umc9th.domain.mission.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MissionErrorCode implements BaseErrorCode { + + // 미션 조회 오류 + NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION_404", "해당 미션을 찾을 수 없습니다."), + + // 이미 도전 중 + ALREADY_CHALLENGING(HttpStatus.CONFLICT, "MISSION_409", "이미 도전 중인 미션입니다."), + + // 유저 미션 조회 오류 + USER_MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION_404_U", "해당 유저의 미션 정보를 찾을 수 없습니다."), + + // 미션 비활성화 상태 + MISSION_DISABLED(HttpStatus.BAD_REQUEST, "MISSION_400", "활성화되지 않은 미션입니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java new file mode 100644 index 0000000..48bf48f --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java @@ -0,0 +1,19 @@ +package com.example.umc9th.domain.mission.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MissionSuccessCode implements BaseSuccessCode { + + OK(HttpStatus.OK, "MISSION_200", "미션 조회 성공"), + CREATE(HttpStatus.CREATED, "MISSION_201", "미션 도전 성공"), + SUCCESS(HttpStatus.OK, "MISSION_200_S", "미션 성공 처리 완료"); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/repository/MissionRepository.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/repository/MissionRepository.java new file mode 100644 index 0000000..39cd1be --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/repository/MissionRepository.java @@ -0,0 +1,40 @@ +package com.example.umc9th.domain.mission.repository; + +import com.example.umc9th.domain.mission.dto.HomeMissionDto; +import com.example.umc9th.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.*; +import org.springframework.data.repository.query.Param; + +public interface MissionRepository extends JpaRepository { + + @Query("SELECT COUNT(um) FROM UserMission um WHERE um.member.id = :memberId AND um.status = true") + long countPerformedMissions(@Param("memberId") Long memberId); + + @Query(""" + SELECT new com.example.umc9th.domain.mission.dto.HomeMissionDto( + :region, + m.id, + s.name, + m.point + ) + FROM Mission m + JOIN m.store s + WHERE s.address LIKE CONCAT(:region, '%') + AND NOT EXISTS ( + SELECT 1 FROM UserMission um + WHERE um.mission.id = m.id + AND um.member.id = :memberId + ) + ORDER BY m.id DESC + """) + Page findUnjoinedMissions( + @Param("region") String region, + @Param("memberId") Long memberId, + Pageable pageable + ); + + // 특정 가게의 활성 미션 목록 (status = true) + Page findByStoreIdAndStatusTrue(Long storeId, Pageable pageable); +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/repository/UserMissionRepository.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/repository/UserMissionRepository.java new file mode 100644 index 0000000..d6d3659 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/repository/UserMissionRepository.java @@ -0,0 +1,26 @@ +package com.example.umc9th.domain.mission.repository; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.mission.entity.UserMission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface UserMissionRepository extends JpaRepository { + + List findByMemberIdAndStatusInOrderByDeadlineDesc(Long memberId, List statuses); + + boolean existsByMemberAndMission(Member member, Mission mission); + + List findByMemberId(Long memberId); + + // 미션 성공 처리 시: 해당 회원의 해당 user_mission만 조회 + Optional findByIdAndMemberId(Long id, Long memberId); + + //내가 진행중인 미션 조회 + Page findByMemberIdAndStatus(Long memberId, boolean status, Pageable pageable); +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/mission/service/MissionService.java b/umc9th/src/main/java/com/example/umc9th/domain/mission/service/MissionService.java new file mode 100644 index 0000000..eb02d4a --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/mission/service/MissionService.java @@ -0,0 +1,141 @@ +package com.example.umc9th.domain.mission.service; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.mission.converter.MissionConverter; +import com.example.umc9th.domain.mission.dto.MissionResDTO; +import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.mission.entity.UserMission; +import com.example.umc9th.domain.mission.exception.MissionException; +import com.example.umc9th.domain.mission.exception.code.MissionErrorCode; +import com.example.umc9th.domain.mission.repository.MissionRepository; +import com.example.umc9th.domain.mission.repository.UserMissionRepository; +import jakarta.transaction.Transactional; +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 java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MissionService { + + private final MemberRepository memberRepository; + private final MissionRepository missionRepository; + private final UserMissionRepository userMissionRepository; + + /** + * 미션 도전하기 + */ + @Transactional + public MissionResDTO.ChallengeDTO challengeMission(Long memberId, Long missionId, LocalDate deadline) { + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + Mission mission = missionRepository.findById(missionId) + .orElseThrow(() -> new MissionException(MissionErrorCode.NOT_FOUND)); + + // 이미 도전 중인지 체크 + if (userMissionRepository.existsByMemberAndMission(member, mission)) { + throw new MissionException(MissionErrorCode.ALREADY_CHALLENGING); + } + + // 마감 기한 기본값 설정 (null이면 +7일) + LocalDate finalDeadline = (deadline != null) ? deadline : LocalDate.now().plusDays(7); + + // UserMission 생성 및 저장 + UserMission userMission = MissionConverter.toUserMission(member, mission, finalDeadline); + userMissionRepository.save(userMission); + + return MissionConverter.toChallengeDTO(userMission); + } + + /** + * 내 미션 목록 조회 (진행중 + 성공) + */ + @Transactional + public List getMyMissions(Long memberId) { + + // 유저 유효성 검사 + memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + List list = userMissionRepository.findByMemberId(memberId); + + return list.stream() + .map(MissionConverter::toMyMissionDTO) + .toList(); + } + + /** + * 미션 성공 처리 + */ + @Transactional + public MissionResDTO.SuccessDTO successMission(Long memberId, Long userMissionId) { + + // 해당 회원의 user_mission인지 검증 + UserMission userMission = userMissionRepository.findByIdAndMemberId(userMissionId, memberId) + .orElseThrow(() -> new MissionException(MissionErrorCode.USER_MISSION_NOT_FOUND)); + + // 이미 성공 상태면 그대로 응답 (idempotent) + if (!userMission.isStatus()) { + return MissionConverter.toSuccessDTO(userMission); + } + + // 진행중 → 성공 처리 + userMission.complete(); // status = false + + return MissionConverter.toSuccessDTO(userMission); + } + + /** + * 특정 가게의 미션 목록 (status=true, 활성 미션만, 페이징) + */ + @Transactional + public MissionResDTO.StoreMissionPageDTO getStoreMissions(Long storeId, int page) { + + // 프론트는 page 1부터 보낸다 → JPA는 0부터라 -1 + int pageIndex = Math.max(page - 1, 0); + + PageRequest pageable = PageRequest.of( + pageIndex, + 10, + Sort.by(Sort.Direction.DESC, "id") + ); + + Page missionPage = + missionRepository.findByStoreIdAndStatusTrue(storeId, pageable); + + return MissionConverter.toStoreMissionPageDTO(missionPage); + } + + //내가 진행중인 미션 조회 + @Transactional + public MissionResDTO.MyMissionPageDTO getMyChallengingMissions(Long memberId, int page) { + + // 유저 검증 + memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + int pageIndex = Math.max(page - 1, 0); + + PageRequest pageable = PageRequest.of( + pageIndex, + 10, + Sort.by(Sort.Direction.DESC, "deadline") + ); + + // 진행중 status = true + Page missionPage = + userMissionRepository.findByMemberIdAndStatus(memberId, true, pageable); + + return MissionConverter.toMyMissionPageDTO(missionPage); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/umc9th/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java new file mode 100644 index 0000000..e6d4517 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -0,0 +1,59 @@ +package com.example.umc9th.domain.review.controller; + +import com.example.umc9th.domain.review.dto.ReviewReqDTO; +import com.example.umc9th.domain.review.dto.ReviewResDTO; +import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.exception.code.ReviewSuccessCode; +import com.example.umc9th.domain.review.service.ReviewService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; +import com.example.umc9th.global.annotation.PositivePage; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/reviews") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + // 테스트 검색 + @GetMapping("/search") + public ApiResponse search(@RequestParam String query, @RequestParam String type) { + return ApiResponse.onSuccess( + GeneralSuccessCode.SUCCESS, + reviewService.searchReview(query, type) + ); + } + + // 9주차 미션 — 내가 작성한 리뷰(페이징)(기존 Review 리스트 반환에서 Dto 반황으로 변경) + @GetMapping("/my") + public ApiResponse myReviews( + @RequestParam(required = false) String storeName, + @RequestParam(required = false) Integer star, + @PositivePage @RequestParam int page + ) { + long memberId = 1; // 로그인 생략 + + Pageable pageable = PageRequest.of(page - 1, 10); + + ReviewResDTO.MyReviewPageDTO result = + reviewService.getMyReviews(memberId, storeName, star, pageable); + + return ApiResponse.onSuccess(GeneralSuccessCode.SUCCESS, result); + } + + // 리뷰 생성 + @PostMapping + public ApiResponse createReview( + @RequestBody ReviewReqDTO.CreateDTO dto + ) { + return ApiResponse.onSuccess( + ReviewSuccessCode.CREATE, + reviewService.createReview(dto) + ); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java b/umc9th/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java new file mode 100644 index 0000000..9d16d3e --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java @@ -0,0 +1,55 @@ +package com.example.umc9th.domain.review.converter; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.review.dto.ReviewReqDTO; +import com.example.umc9th.domain.review.dto.ReviewResDTO; +import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.store.entity.Store; +import org.springframework.data.domain.Page; + +import java.util.List; + +public class ReviewConverter { + + public static Review toEntity(ReviewReqDTO.CreateDTO dto, Member member, Store store) { + return Review.builder() + .star(dto.star()) + .content(dto.content()) + .member(member) + .store(store) + .status(true) + .build(); + } + + public static ReviewResDTO.CreateDTO toCreateDTO(Review review) { + return ReviewResDTO.CreateDTO.builder() + .reviewId(review.getId()) + .createdAt(review.getCreatedAt().toLocalDate()) + .build(); + } + + public static ReviewResDTO.MyReviewDTO toMyReviewDTO(Review review) { + return ReviewResDTO.MyReviewDTO.builder() + .reviewId(review.getId()) + .storeName(review.getStore().getName()) + .star(review.getStar()) + .content(review.getContent()) + .createdAt(review.getCreatedAt().toLocalDate()) + .build(); + } + + public static ReviewResDTO.MyReviewPageDTO toMyReviewPageDTO(Page reviewPage) { + + List list = reviewPage.getContent().stream() + .map(ReviewConverter::toMyReviewDTO) + .toList(); + + return ReviewResDTO.MyReviewPageDTO.builder() + .contents(list) + .page(reviewPage.getNumber() + 1) + .size(reviewPage.getSize()) + .totalElements(reviewPage.getTotalElements()) + .totalPages(reviewPage.getTotalPages()) + .build(); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/dto/ReviewReqDTO.java b/umc9th/src/main/java/com/example/umc9th/domain/review/dto/ReviewReqDTO.java new file mode 100644 index 0000000..1d34903 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/dto/ReviewReqDTO.java @@ -0,0 +1,14 @@ +package com.example.umc9th.domain.review.dto; + +import java.time.LocalDate; + +public class ReviewReqDTO { + + public record CreateDTO( + Long memberId, + Long storeId, + Float star, + String content, + String imageUrl + ) {} +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/dto/ReviewResDTO.java b/umc9th/src/main/java/com/example/umc9th/domain/review/dto/ReviewResDTO.java new file mode 100644 index 0000000..67e455c --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/dto/ReviewResDTO.java @@ -0,0 +1,33 @@ +package com.example.umc9th.domain.review.dto; + +import lombok.Builder; + +import java.time.LocalDate; +import java.util.List; + +public class ReviewResDTO { + + @Builder + public record CreateDTO( + Long reviewId, + LocalDate createdAt + ) {} + + @Builder + public record MyReviewDTO( + Long reviewId, + String storeName, + double star, + String content, + LocalDate createdAt + ) {} + + @Builder + public record MyReviewPageDTO( + List contents, + int page, + int size, + long totalElements, + int totalPages + ) {} +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/exception/ReviewException.java b/umc9th/src/main/java/com/example/umc9th/domain/review/exception/ReviewException.java new file mode 100644 index 0000000..afde6b0 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/exception/ReviewException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.review.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class ReviewException extends GeneralException { + public ReviewException(BaseErrorCode code) { + super(code); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewErrorCode.java b/umc9th/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewErrorCode.java new file mode 100644 index 0000000..bdd86b2 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewErrorCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.review.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReviewErrorCode implements BaseErrorCode { + + NOT_FOUND(HttpStatus.NOT_FOUND, + "STORE404_1", + "해당 리뷰를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + } diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java b/umc9th/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java new file mode 100644 index 0000000..efdc9c0 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java @@ -0,0 +1,24 @@ +package com.example.umc9th.domain.review.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReviewSuccessCode implements BaseSuccessCode { + + FOUND(HttpStatus.OK, + "INTEREST200_1", + "성공적으로 리뷰를 조회했습니다."), + + CREATE(HttpStatus.CREATED, + "REVIEW200_2", + "리뷰 작성 성공") + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryDsl.java b/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryDsl.java new file mode 100644 index 0000000..1e117d0 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryDsl.java @@ -0,0 +1,13 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.review.entity.Review; +import com.querydsl.core.types.Predicate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface ReviewQueryDsl { + Page searchReviewWithPaging(Predicate predicate, Pageable pageable); + List searchReview(Predicate predicate); +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryDslImpl.java b/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryDslImpl.java new file mode 100644 index 0000000..21fd7c9 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryDslImpl.java @@ -0,0 +1,59 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.review.entity.QReview; +import com.example.umc9th.domain.review.entity.Review; +import com.querydsl.core.types.Predicate; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageImpl; + +import java.util.List; + +@RequiredArgsConstructor +public class ReviewQueryDslImpl implements ReviewQueryDsl { + + private final EntityManager em; + + @Override + public List searchReview(Predicate predicate) { + JPAQueryFactory queryFactory = new JPAQueryFactory(em); + QReview review = QReview.review; + + return queryFactory + .selectFrom(review) + .leftJoin(review.store).fetchJoin() + .leftJoin(review.member).fetchJoin() + .where(predicate) + .orderBy(review.createdAt.desc()) + .fetch(); + } + + // 내가 작성한 리뷰 페이지 조회 + @Override + public Page searchReviewWithPaging(Predicate predicate, Pageable pageable) { + + JPAQueryFactory queryFactory = new JPAQueryFactory(em); + QReview review = QReview.review; + + List reviews = queryFactory + .selectFrom(review) + .leftJoin(review.store).fetchJoin() + .leftJoin(review.member).fetchJoin() + .where(predicate) + .orderBy(review.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = queryFactory + .select(review.count()) + .from(review) + .where(predicate) + .fetchOne(); + + return new PageImpl<>(reviews, pageable, total); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java b/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java new file mode 100644 index 0000000..46be9a4 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.review.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReviewRepository + extends JpaRepository, ReviewQueryDsl { +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java b/umc9th/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java new file mode 100644 index 0000000..a68a1cc --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java @@ -0,0 +1,97 @@ +package com.example.umc9th.domain.review.service; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.ReviewReqDTO; +import com.example.umc9th.domain.review.dto.ReviewResDTO; +import com.example.umc9th.domain.review.entity.QReview; +import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.repository.ReviewRepository; +import com.example.umc9th.domain.store.entity.Store; +import com.example.umc9th.domain.store.exception.StoreException; +import com.example.umc9th.domain.store.exception.code.StoreErrorCode; +import com.example.umc9th.domain.store.repository.StoreRepository; +import com.querydsl.core.BooleanBuilder; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final MemberRepository memberRepository; + private final StoreRepository storeRepository; + + // QueryDSL 검색 테스트 + public List searchReview(String query, String type) { + QReview review = QReview.review; + BooleanBuilder builder = new BooleanBuilder(); + + if ("location".equals(type)) { + builder.and(review.store.address.contains(query)); + } else if ("star".equals(type)) { + builder.and(review.star.goe(Integer.parseInt(query))); + } else if ("both".equals(type)) { + String[] parts = query.split("&"); + builder.and(review.store.address.contains(parts[0])); + builder.and(review.star.goe(Integer.parseInt(parts[1]))); + } + + return reviewRepository.searchReview(builder); + } + + // 9주차 — 내가 작성한 리뷰 조회 (페이징) + public ReviewResDTO.MyReviewPageDTO getMyReviews( + long memberId, + String storeName, + Integer star, + Pageable pageable + ) { + + memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + QReview review = QReview.review; + BooleanBuilder builder = new BooleanBuilder(); + + builder.and(review.member.id.eq(memberId)); + + if (storeName != null && !storeName.isBlank()) { + builder.and(review.store.name.containsIgnoreCase(storeName)); + } + + if (star != null) { + builder.and(review.star.goe(star)); + } + + Page reviewPage = + reviewRepository.searchReviewWithPaging(builder, pageable); + + return ReviewConverter.toMyReviewPageDTO(reviewPage); + } + + // 리뷰 생성 + @Transactional + public ReviewResDTO.CreateDTO createReview(ReviewReqDTO.CreateDTO dto) { + + Member member = memberRepository.findById(dto.memberId()) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + Store store = storeRepository.findById(dto.storeId()) + .orElseThrow(() -> new StoreException(StoreErrorCode.NOT_FOUND)); + + Review review = ReviewConverter.toEntity(dto, member, store); + reviewRepository.save(review); + + return ReviewConverter.toCreateDTO(review); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/store/entity/Store.java b/umc9th/src/main/java/com/example/umc9th/domain/store/entity/Store.java index 4a183e2..77c5c8d 100644 --- a/umc9th/src/main/java/com/example/umc9th/domain/store/entity/Store.java +++ b/umc9th/src/main/java/com/example/umc9th/domain/store/entity/Store.java @@ -3,6 +3,7 @@ import com.example.umc9th.domain.mission.entity.Mission; import com.example.umc9th.domain.review.entity.Review; import com.example.umc9th.global.entity.BaseEntity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; @@ -41,8 +42,10 @@ public class Store extends BaseEntity { // 연관 관계 @OneToMany(mappedBy = "store") + @JsonIgnore private List reviews = new ArrayList<>(); @OneToMany(mappedBy = "store") + @JsonIgnore private List missions = new ArrayList<>(); } diff --git a/umc9th/src/main/java/com/example/umc9th/domain/store/exception/StoreException.java b/umc9th/src/main/java/com/example/umc9th/domain/store/exception/StoreException.java new file mode 100644 index 0000000..a6e94bc --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/store/exception/StoreException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.store.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class StoreException extends GeneralException { + public StoreException(BaseErrorCode code) { + super(code); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/store/exception/code/StoreErrorCode.java b/umc9th/src/main/java/com/example/umc9th/domain/store/exception/code/StoreErrorCode.java new file mode 100644 index 0000000..b7a6d8b --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/store/exception/code/StoreErrorCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.store.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum StoreErrorCode implements BaseErrorCode { + + NOT_FOUND(HttpStatus.NOT_FOUND, + "STORE404_1", + "해당 식당을 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + } diff --git a/umc9th/src/main/java/com/example/umc9th/domain/store/exception/code/StoreSuccessCode.java b/umc9th/src/main/java/com/example/umc9th/domain/store/exception/code/StoreSuccessCode.java new file mode 100644 index 0000000..bcb92e2 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/store/exception/code/StoreSuccessCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.store.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum StoreSuccessCode implements BaseSuccessCode { + + FOUND(HttpStatus.OK, + "INTEREST200_1", + "성공적으로 식당을 조회했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/store/repository/StoreRepository.java b/umc9th/src/main/java/com/example/umc9th/domain/store/repository/StoreRepository.java new file mode 100644 index 0000000..11861a5 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/store/repository/StoreRepository.java @@ -0,0 +1,9 @@ +package com.example.umc9th.domain.store.repository; + +import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.store.entity.Store; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; + +public interface StoreRepository extends JpaRepository { +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java b/umc9th/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java new file mode 100644 index 0000000..af81183 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java @@ -0,0 +1,24 @@ +package com.example.umc9th.domain.test.converter; + +import com.example.umc9th.domain.test.dto.res.TestResDTO; + +public class TestConverter { + + // 객체 -> DTO + public static TestResDTO.Testing toTestingDTO( + String testing + ) { + return TestResDTO.Testing.builder() + .testString(testing) + .build(); + } + + // 객체 -> DTO + public static TestResDTO.Exception toExceptionDTO( + String testing + ){ + return TestResDTO.Exception.builder() + .testString(testing) + .build(); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java b/umc9th/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java new file mode 100644 index 0000000..9f13f52 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java @@ -0,0 +1,19 @@ +package com.example.umc9th.domain.test.dto.res; + +import lombok.Builder; +import lombok.Getter; + +public class TestResDTO { + + @Builder + @Getter + public static class Testing { + private String testString; + } + + @Builder + @Getter + public static class Exception { + private String testString; + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/domain/test/exception/TestException.java b/umc9th/src/main/java/com/example/umc9th/domain/test/exception/TestException.java new file mode 100644 index 0000000..3507b6c --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/test/exception/TestException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.test.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class TestException extends GeneralException { + public TestException(BaseErrorCode code) { + super(code); + } +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/domain/test/exception/code/TestErrorCode.java b/umc9th/src/main/java/com/example/umc9th/domain/test/exception/code/TestErrorCode.java new file mode 100644 index 0000000..7264f21 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/domain/test/exception/code/TestErrorCode.java @@ -0,0 +1,19 @@ +package com.example.umc9th.domain.test.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum TestErrorCode implements BaseErrorCode { + + // For test + TEST_EXCEPTION(HttpStatus.BAD_REQUEST, "TEST400_1", "이거는 테스트"), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/global/annotation/ExistFoods.java b/umc9th/src/main/java/com/example/umc9th/global/annotation/ExistFoods.java new file mode 100644 index 0000000..01c1172 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/annotation/ExistFoods.java @@ -0,0 +1,26 @@ +package com.example.umc9th.global.annotation; + +import com.example.umc9th.global.validator.FoodExistValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +// +@Documented +@Constraint(validatedBy = FoodExistValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistFoods { + //여기서 디폴트 메시지를 설정합니다. + String message() default "해당 음식이 존재하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/global/annotation/PositivePage.java b/umc9th/src/main/java/com/example/umc9th/global/annotation/PositivePage.java new file mode 100644 index 0000000..aac0635 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/annotation/PositivePage.java @@ -0,0 +1,15 @@ +package com.example.umc9th.global.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = PositivePageValidator.class) +public @interface PositivePage { + String message() default "page는 1 이상이어야 합니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/umc9th/src/main/java/com/example/umc9th/global/annotation/PositivePageValidator.java b/umc9th/src/main/java/com/example/umc9th/global/annotation/PositivePageValidator.java new file mode 100644 index 0000000..bf81fae --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/annotation/PositivePageValidator.java @@ -0,0 +1,12 @@ +package com.example.umc9th.global.annotation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class PositivePageValidator implements ConstraintValidator { + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + return value != null && value >= 1; + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java new file mode 100644 index 0000000..809ea7c --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java @@ -0,0 +1,36 @@ +package com.example.umc9th.global.apiPayload; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class ApiResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + + @JsonProperty("code") + private final String code; + + @JsonProperty("message") + private final String message; + + @JsonProperty("result") + private T result; + + // 성공한 경우 (result 포함) + public static ApiResponse onSuccess(BaseSuccessCode code, T result) { + return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); + } + + // 실패한 경우 (result 포함) + public static ApiResponse onFailure(BaseErrorCode code, T result) { + return new ApiResponse<>(false, code.getCode(), code.getMessage(), result); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/BaseErrorCode.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/BaseErrorCode.java new file mode 100644 index 0000000..3830b84 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/BaseErrorCode.java @@ -0,0 +1,10 @@ +package com.example.umc9th.global.apiPayload.code; + +import org.springframework.http.HttpStatus; + +public interface BaseErrorCode { + + HttpStatus getStatus(); + String getCode(); + String getMessage(); +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java new file mode 100644 index 0000000..2924540 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java @@ -0,0 +1,10 @@ +package com.example.umc9th.global.apiPayload.code; + +import org.springframework.http.HttpStatus; + +public interface BaseSuccessCode { + + HttpStatus getStatus(); + String getCode(); + String getMessage(); +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java new file mode 100644 index 0000000..3410535 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java @@ -0,0 +1,35 @@ +package com.example.umc9th.global.apiPayload.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GeneralErrorCode implements BaseErrorCode{ + + BAD_REQUEST(HttpStatus.BAD_REQUEST, + "COMMON400_1", + "잘못된 요청입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "AUTH401_1", + "인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, + "AUTH403_1", + "요청이 거부되었습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, + "COMMON404_1", + "요청한 리소스를 찾을 수 없습니다."), + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, + "COMMON500_1", + "예기치 않은 서버 에러가 발생했습니다."), + + VALID_FAIL(HttpStatus.BAD_REQUEST, + "COMMON400_2", + "요청 값이 유효하지 않습니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralSuccessCode.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralSuccessCode.java new file mode 100644 index 0000000..aea35aa --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralSuccessCode.java @@ -0,0 +1,17 @@ +package com.example.umc9th.global.apiPayload.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GeneralSuccessCode implements BaseSuccessCode { + + SUCCESS(HttpStatus.OK, "COMMON200", "요청이 성공적으로 처리되었습니다."), + REVIEW_LIST_SUCCESS(HttpStatus.OK, "REVIEW200", "내가 작성한 리뷰 조회"), + ; + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/exception/GeneralException.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/exception/GeneralException.java new file mode 100644 index 0000000..420007c --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/exception/GeneralException.java @@ -0,0 +1,13 @@ +package com.example.umc9th.global.apiPayload.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GeneralException extends RuntimeException { + + private final BaseErrorCode errorCode; + +} diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/exception/GlobalExceptionHandler.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..fcf2a44 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/exception/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +package com.example.umc9th.global.apiPayload.exception; + +import com.example.umc9th.global.apiPayload.ApiResponse; +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 비즈니스 로직에서 발생시키는 Custom Exception 처리 + */ + @ExceptionHandler(GeneralException.class) + public ResponseEntity> handleGeneralException(GeneralException e) { + + BaseErrorCode errorCode = e.getErrorCode(); + + log.warn("[GeneralException] {} - {}", errorCode.getCode(), errorCode.getMessage()); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(ApiResponse.onFailure(errorCode, null)); + } + + /** + * 잘못된 입력 예외 처리 + */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { + + log.warn("[IllegalArgumentException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.BAD_REQUEST.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.BAD_REQUEST, null)); + } + + /** + * 인증 실패 예외 처리 (예: JWT 검증 실패) + */ + @ExceptionHandler(SecurityException.class) + public ResponseEntity> handleSecurityException(SecurityException e) { + + log.warn("[SecurityException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.UNAUTHORIZED.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null)); + } + + /** + * 예상하지 못한 모든 서버 오류 처리 + */ + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnexpectedException(Exception e) { + + log.error("[UnexpectedException]", e); + + return ResponseEntity + .status(GeneralErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.INTERNAL_SERVER_ERROR, null)); + } +} diff --git a/umc9th/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java new file mode 100644 index 0000000..7512f95 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -0,0 +1,65 @@ +package com.example.umc9th.global.apiPayload.handler; + +import com.example.umc9th.global.apiPayload.ApiResponse; +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GeneralExceptionAdvice { + + // 애플리케이션에서 발생하는 커스텀 예외를 처리 + @ExceptionHandler(GeneralException.class) + public ResponseEntity> handleException( + GeneralException ex + ) { + + return ResponseEntity.status(ex.getErrorCode().getStatus()) + .body(ApiResponse.onFailure( + ex.getErrorCode(), + null + ) + ); + } + + // 그 외의 정의되지 않은 모든 예외 처리 + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException( + Exception ex + ) { + + BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR; + return ResponseEntity.status(code.getStatus()) + .body(ApiResponse.onFailure( + code, + ex.getMessage() + ) + ); + } + + // 컨트롤러 메서드에서 @Valid 어노테이션을 사용하여 DTO의 유효성 검사를 수행 + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity>> handleMethodArgumentNotValidException( + MethodArgumentNotValidException ex + ) { + // 검사에 실패한 필드와 그에 대한 메시지를 저장하는 Map + Map errors = new HashMap<>(); + ex.getBindingResult().getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage()) + ); + + GeneralErrorCode code = GeneralErrorCode.VALID_FAIL; + ApiResponse> errorResponse = ApiResponse.onFailure(code, errors); + + // 에러 코드, 메시지와 함께 errors를 반환 + return ResponseEntity.status(code.getStatus()).body(errorResponse); + } + +} diff --git a/umc9th/src/main/java/com/example/umc9th/global/config/SwaggerConfig.java b/umc9th/src/main/java/com/example/umc9th/global/config/SwaggerConfig.java new file mode 100644 index 0000000..e3e85a2 --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/config/SwaggerConfig.java @@ -0,0 +1,37 @@ +package com.example.umc9th.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.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +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("UMC9th").description("Spring Boot").version("0.0.1"); + + // JWT 토큰 헤더 방식 + 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/umc9th/src/main/java/com/example/umc9th/global/validator/FoodExistValidator.java b/umc9th/src/main/java/com/example/umc9th/global/validator/FoodExistValidator.java new file mode 100644 index 0000000..57324cf --- /dev/null +++ b/umc9th/src/main/java/com/example/umc9th/global/validator/FoodExistValidator.java @@ -0,0 +1,33 @@ +package com.example.umc9th.global.validator; + +import com.example.umc9th.domain.member.exception.code.InterestErrorCode; +import com.example.umc9th.domain.member.repository.InterestRepository; +import com.example.umc9th.global.annotation.ExistFoods; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class FoodExistValidator implements ConstraintValidator> { + + private final InterestRepository interestRepository; + + @Override + public boolean isValid(List values, ConstraintValidatorContext context) { + boolean isValid = values.stream() + .allMatch(value -> interestRepository.existsById(value)); + + if (!isValid) { + // 이 부분에서 아까 디폴트 메시지를 초기화 시키고, 새로운 메시지로 덮어씌우게 됩니다. + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(InterestErrorCode.NOT_FOUND.getMessage()).addConstraintViolation(); + } + + return isValid; + + } +} \ No newline at end of file diff --git a/umc9th/src/main/resources/application.yml b/umc9th/src/main/resources/application.yml index d820e86..87375cb 100644 --- a/umc9th/src/main/resources/application.yml +++ b/umc9th/src/main/resources/application.yml @@ -13,7 +13,8 @@ spring: database-platform: org.hibernate.dialect.MySQLDialect # Hibernate에서 사용할 MySQL 방언(dialect) 설정 show-sql: true # 실행된 SQL 쿼리를 콘솔에 출력할지 여부 설정 hibernate: - ddl-auto: create # 애플리케이션 실행 시 데이터베이스 스키마의 상태를 설정 + ddl-auto: update # 애플리케이션 실행 시 데이터베이스 스키마의 상태를 설정 properties: hibernate: - format_sql: true # 출력되는 SQL 쿼리를 보기 좋게 포맷팅 \ No newline at end of file + format_sql: true # 출력되는 SQL 쿼리를 보기 좋게 포맷팅 + diff --git a/umc9th/src/test/java/com/example/umc9th/domain/review/ReviewRepositoryTest.java b/umc9th/src/test/java/com/example/umc9th/domain/review/ReviewRepositoryTest.java new file mode 100644 index 0000000..3310b6d --- /dev/null +++ b/umc9th/src/test/java/com/example/umc9th/domain/review/ReviewRepositoryTest.java @@ -0,0 +1,40 @@ +package com.example.umc9th.domain.review; + +import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.repository.ReviewRepository; +import com.querydsl.core.BooleanBuilder; +import com.example.umc9th.domain.review.entity.QReview; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@SpringBootTest +@Transactional +public class ReviewRepositoryTest { + + @Autowired + private ReviewRepository reviewRepository; + + @Test + void testSearchReview_ByStoreName() { + // given + QReview review = QReview.review; + BooleanBuilder builder = new BooleanBuilder(); + + builder.and(review.store.name.containsIgnoreCase("반이학생마라탕마라반")); + + // when + List results = reviewRepository.searchReview(builder); + + // then + System.out.println("✅ 검색 결과 개수: " + results.size()); + for (Review r : results) { + System.out.println("가게명: " + r.getStore().getName() + + ", 별점: " + r.getStar() + + ", 작성자: " + r.getMember().getName()); + } + } +} diff --git a/week0/ERD.vuerd.json b/week0/ERD.vuerd.json index 9b9623c..bf2867b 100644 --- a/week0/ERD.vuerd.json +++ b/week0/ERD.vuerd.json @@ -4,9 +4,9 @@ "settings": { "width": 2000, "height": 2000, - "scrollTop": -263.7869, - "scrollLeft": -267.5756, - "zoomLevel": 0.52, + "scrollTop": -146.1667, + "scrollLeft": -46, + "zoomLevel": 1, "show": 431, "database": 4, "databaseName": "", @@ -38,7 +38,8 @@ "ETcc-k8p-2Xj7Avc_QuIL", "YsXMe4aWI4vSU7M4Pdeny", "ORir_zY3zWs8fj9eKIhij", - "EPYnTEbnzHcSM-TV7Yogy" + "zrNDU9R9OQONz9IYpCOQv", + "yu3szoId7FctUvSa0xoTn" ], "relationshipIds": [ "fr8aQL5wJeCHd6nA3FkpV", @@ -48,8 +49,8 @@ "27fFwlvcry7s3zwMedMPL", "mokzjvYQM38mTea-1naJe", "leblF8kHmiRMhcnCVAceD", - "onHzSgg1u3-Nk4tYVf7t0", - "zdTWn5Wpu4-atC3FVpk1E" + "wbLj5mUjPu5rltCDHj3XD", + "8aesalFlcdiXVhsHAAd_f" ], "indexIds": [], "memoIds": [] @@ -58,7 +59,7 @@ "tableEntities": { "2y1bvJOIG1e0BeEWgxI08": { "id": "2y1bvJOIG1e0BeEWgxI08", - "name": "user", + "name": "member", "comment": "사용자", "columnIds": [ "vDFt6LsnU_OsODOI3QFtU", @@ -72,7 +73,8 @@ "-gL9Tv6k4tzEhExARRMVF", "rAo8blJvJWiSMdcXxPEQm", "AdD_6uo7QsyR4vZ5mWieR", - "sbMtd5RQq7uaV0Cru8P_v" + "sbMtd5RQq7uaV0Cru8P_v", + "rgoW8fTWbtyd9QtgnCv4X" ], "seqColumnIds": [ "WkqAgrjlXhLG2sEbIA5US", @@ -87,18 +89,19 @@ "-gL9Tv6k4tzEhExARRMVF", "rAo8blJvJWiSMdcXxPEQm", "AdD_6uo7QsyR4vZ5mWieR", - "sbMtd5RQq7uaV0Cru8P_v" + "sbMtd5RQq7uaV0Cru8P_v", + "rgoW8fTWbtyd9QtgnCv4X" ], "ui": { - "x": 212.9919, - "y": 486.4899, + "x": 298.9682, + "y": 607.0328, "zIndex": 2, "widthName": 60, "widthComment": 60, "color": "" }, "meta": { - "updateAt": 1758003899942, + "updateAt": 1759818994411, "createAt": 1758002169271 } }, @@ -115,15 +118,15 @@ "K_vOBQsbyMWekqQ_MEI4N" ], "ui": { - "x": 1246.7656, - "y": 203.1501, + "x": 1265.3757, + "y": 430.4087, "zIndex": 10, "widthName": 60, "widthComment": 60, "color": "" }, "meta": { - "updateAt": 1758005023914, + "updateAt": 1759818391682, "createAt": 1758002208352 } }, @@ -152,15 +155,15 @@ "TOi3d5nz-qA4ggbf5SGaA" ], "ui": { - "x": 689.4692, - "y": 190.8081, + "x": 793.9093, + "y": 419.843, "zIndex": 11, "widthName": 69, "widthComment": 67, "color": "" }, "meta": { - "updateAt": 1758005001026, + "updateAt": 1759818393286, "createAt": 1758002209969 } }, @@ -170,7 +173,6 @@ "comment": "가게", "columnIds": [ "F_CCSEveysTRYLn8lx8gE", - "GnAxf5fMaPNYtyzoiYMA-", "chq0SKqnzdsl00Sjku66M", "CXpU5qG_eglJfOISVq81C", "4Zf3gYPB2YGTzKHUoubEC", @@ -193,15 +195,15 @@ "uM2ODWrSWgyAfDgK5Acu0" ], "ui": { - "x": 1246.1474, - "y": 487.642, + "x": 1285.0693, + "y": 681.9737, "zIndex": 12, "widthName": 60, "widthComment": 60, "color": "" }, "meta": { - "updateAt": 1758005005400, + "updateAt": 1759818983111, "createAt": 1758002211392 } }, @@ -235,15 +237,15 @@ "6KXjwhh7zBv5U9ju-mYIk" ], "ui": { - "x": 743.7147, - "y": 492.0608, + "x": 798.4637, + "y": 668.3929, "zIndex": 13, "widthName": 60, "widthComment": 60, "color": "" }, "meta": { - "updateAt": 1758005004123, + "updateAt": 1759818388503, "createAt": 1758002211983 } }, @@ -255,8 +257,8 @@ "w4-iEtBZVv4oZMo29nEVr", "qyv68yagM3C9_IGyT2VLt", "DpzdVYsCL9lkCfts-ehwz", - "P5y2uZFuvYjeM7xY-aY56", "p5gVywEZV25DrR5jz4fkH", + "P5y2uZFuvYjeM7xY-aY56", "S2qqHB5cNDTVqitPQMpQ1" ], "seqColumnIds": [ @@ -265,20 +267,20 @@ "JtA1FPZRI3bkk9CFQzhXP", "qyv68yagM3C9_IGyT2VLt", "DpzdVYsCL9lkCfts-ehwz", - "P5y2uZFuvYjeM7xY-aY56", "p5gVywEZV25DrR5jz4fkH", + "P5y2uZFuvYjeM7xY-aY56", "S2qqHB5cNDTVqitPQMpQ1" ], "ui": { - "x": 205.9873, - "y": 937.228, + "x": 306.263, + "y": 1072.3362, "zIndex": 14, "widthName": 67, "widthComment": 85, "color": "" }, "meta": { - "updateAt": 1758004995283, + "updateAt": 1759818383231, "createAt": 1758002212209 } }, @@ -305,53 +307,68 @@ "ucsLuC5D65W0EisT2Jycw" ], "ui": { - "x": 889.8041, - "y": 952.9649, + "x": 839.0704, + "y": 1075.7449, "zIndex": 15, "widthName": 60, "widthComment": 60, "color": "" }, "meta": { - "updateAt": 1758004989746, + "updateAt": 1759818384505, "createAt": 1758002213387 } }, - "EPYnTEbnzHcSM-TV7Yogy": { - "id": "EPYnTEbnzHcSM-TV7Yogy", - "name": "alam", - "comment": "알람", + "zrNDU9R9OQONz9IYpCOQv": { + "id": "zrNDU9R9OQONz9IYpCOQv", + "name": "term", + "comment": "약관", + "columnIds": [ + "lzTP7Y9Q2wHxnor3iYbUN", + "n97RJkZ1d-qHS3OQ5IFWn" + ], + "seqColumnIds": [ + "lzTP7Y9Q2wHxnor3iYbUN", + "n97RJkZ1d-qHS3OQ5IFWn" + ], + "ui": { + "x": 312.2384, + "y": 156.6367, + "zIndex": 16, + "widthName": 60, + "widthComment": 60, + "color": "" + }, + "meta": { + "updateAt": 1759818397498, + "createAt": 1759711809275 + } + }, + "yu3szoId7FctUvSa0xoTn": { + "id": "yu3szoId7FctUvSa0xoTn", + "name": "User_term", + "comment": "유저약관", "columnIds": [ - "4djG9DS3q5mf3YpwU2HxJ", - "nbxToDPDboiyNNJkXi3gK", - "8AHxK4llaRij7qCl2APTB", - "SRBlLiACwF7a2KELunbz9", - "-d9Y5vU42vkDQB2q9tUd0", - "btij5fWDwHCTeHvbkXJGC", - "b5UklzcMtcMi11RLRp0Tz", - "CCq1c0NyEngIoj0AErl8d" + "Ad9Q86E6ce1GUaw49K9ni", + "IJ-OWDU5a6JkkFcx_lY2a", + "TgXJQ_3fDRKT5RxMLCCCW" ], "seqColumnIds": [ - "4djG9DS3q5mf3YpwU2HxJ", - "nbxToDPDboiyNNJkXi3gK", - "8AHxK4llaRij7qCl2APTB", - "SRBlLiACwF7a2KELunbz9", - "-d9Y5vU42vkDQB2q9tUd0", - "btij5fWDwHCTeHvbkXJGC", - "b5UklzcMtcMi11RLRp0Tz", - "CCq1c0NyEngIoj0AErl8d" + "Ad9Q86E6ce1GUaw49K9ni", + "IJ-OWDU5a6JkkFcx_lY2a", + "TgXJQ_3fDRKT5RxMLCCCW" ], "ui": { - "x": 119.2208, - "y": 96.1056, - "zIndex": 539, + "x": 300.585, + "y": 363.9731, + "zIndex": 61, "widthName": 60, "widthComment": 60, "color": "" }, "meta": { - "updateAt": 1758004999298, - "createAt": 1758004663649 + "updateAt": 1759818395578, + "createAt": 1759711945588 } } }, @@ -399,7 +416,7 @@ "KpiXQMwL5ze-vJKrQlQ1V": { "id": "KpiXQMwL5ze-vJKrQlQ1V", "tableId": "2y1bvJOIG1e0BeEWgxI08", - "name": "sex", + "name": "gender", "comment": "", "dataType": "INT", "default": "", @@ -412,7 +429,7 @@ "widthDefault": 60 }, "meta": { - "updateAt": 1758002410593, + "updateAt": 1759819677323, "createAt": 1758002177165 } }, @@ -1676,34 +1693,14 @@ "createAt": 1758004517394 } }, - "4djG9DS3q5mf3YpwU2HxJ": { - "id": "4djG9DS3q5mf3YpwU2HxJ", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "id", + "GnAxf5fMaPNYtyzoiYMA-": { + "id": "GnAxf5fMaPNYtyzoiYMA-", + "tableId": "NA3wwYX9_yPbc5rR8NQP3", + "name": "categori_id", "comment": "", "dataType": "BIGINT", "default": "", - "options": 10, - "ui": { - "keys": 1, - "widthName": 60, - "widthComment": 60, - "widthDataType": 60, - "widthDefault": 60 - }, - "meta": { - "updateAt": 1758004789027, - "createAt": 1758004685866 - } - }, - "8AHxK4llaRij7qCl2APTB": { - "id": "8AHxK4llaRij7qCl2APTB", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "content", - "comment": "", - "dataType": "VARCHAR", - "default": "", - "options": 0, + "options": 8, "ui": { "keys": 0, "widthName": 60, @@ -1712,16 +1709,16 @@ "widthDefault": 60 }, "meta": { - "updateAt": 1758004760600, - "createAt": 1758004748516 + "updateAt": 1758004970506, + "createAt": 1758004959005 } }, - "SRBlLiACwF7a2KELunbz9": { - "id": "SRBlLiACwF7a2KELunbz9", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "dtype", + "n97RJkZ1d-qHS3OQ5IFWn": { + "id": "n97RJkZ1d-qHS3OQ5IFWn", + "tableId": "zrNDU9R9OQONz9IYpCOQv", + "name": "name", "comment": "", - "dataType": "VARCHAR", + "dataType": "ENUM", "default": "", "options": 0, "ui": { @@ -1732,78 +1729,58 @@ "widthDefault": 60 }, "meta": { - "updateAt": 1758004786975, - "createAt": 1758004761964 - } - }, - "-d9Y5vU42vkDQB2q9tUd0": { - "id": "-d9Y5vU42vkDQB2q9tUd0", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "is_confirmed", - "comment": "", - "dataType": "BOOLEAN", - "default": "", - "options": 0, - "ui": { - "keys": 0, - "widthName": 69, - "widthComment": 60, - "widthDataType": 60, - "widthDefault": 60 - }, - "meta": { - "updateAt": 1758004819783, - "createAt": 1758004803972 + "updateAt": 1759711854656, + "createAt": 1759711822863 } }, - "btij5fWDwHCTeHvbkXJGC": { - "id": "btij5fWDwHCTeHvbkXJGC", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "createdAt", + "lzTP7Y9Q2wHxnor3iYbUN": { + "id": "lzTP7Y9Q2wHxnor3iYbUN", + "tableId": "zrNDU9R9OQONz9IYpCOQv", + "name": "term_id", "comment": "", - "dataType": "DATETIME", + "dataType": "BIGINT", "default": "", - "options": 0, + "options": 10, "ui": { - "keys": 0, + "keys": 1, "widthName": 60, "widthComment": 60, "widthDataType": 60, "widthDefault": 60 }, "meta": { - "updateAt": 1758004834316, - "createAt": 1758004821577 + "updateAt": 1759712966327, + "createAt": 1759711868594 } }, - "b5UklzcMtcMi11RLRp0Tz": { - "id": "b5UklzcMtcMi11RLRp0Tz", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "updatedAt", + "Ad9Q86E6ce1GUaw49K9ni": { + "id": "Ad9Q86E6ce1GUaw49K9ni", + "tableId": "yu3szoId7FctUvSa0xoTn", + "name": "", "comment": "", - "dataType": "DATETIME", + "dataType": "", "default": "", - "options": 0, + "options": 10, "ui": { - "keys": 0, + "keys": 1, "widthName": 60, "widthComment": 60, "widthDataType": 60, "widthDefault": 60 }, "meta": { - "updateAt": 1758004844394, - "createAt": 1758004838567 + "updateAt": 1759712914169, + "createAt": 1759712914169 } }, - "nbxToDPDboiyNNJkXi3gK": { - "id": "nbxToDPDboiyNNJkXi3gK", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "title", + "rgoW8fTWbtyd9QtgnCv4X": { + "id": "rgoW8fTWbtyd9QtgnCv4X", + "tableId": "2y1bvJOIG1e0BeEWgxI08", + "name": "", "comment": "", - "dataType": "VARCHAR", + "dataType": "", "default": "", - "options": 0, + "options": 8, "ui": { "keys": 0, "widthName": 60, @@ -1812,14 +1789,14 @@ "widthDefault": 60 }, "meta": { - "updateAt": 1758004859027, - "createAt": 1758004845381 + "updateAt": 1759712916893, + "createAt": 1759712916893 } }, - "CCq1c0NyEngIoj0AErl8d": { - "id": "CCq1c0NyEngIoj0AErl8d", - "tableId": "EPYnTEbnzHcSM-TV7Yogy", - "name": "id", + "IJ-OWDU5a6JkkFcx_lY2a": { + "id": "IJ-OWDU5a6JkkFcx_lY2a", + "tableId": "yu3szoId7FctUvSa0xoTn", + "name": "user_id", "comment": "", "dataType": "BIGINT", "default": "", @@ -1832,14 +1809,14 @@ "widthDefault": 60 }, "meta": { - "updateAt": 1758004886190, - "createAt": 1758004886190 + "updateAt": 1759712959800, + "createAt": 1759712932112 } }, - "GnAxf5fMaPNYtyzoiYMA-": { - "id": "GnAxf5fMaPNYtyzoiYMA-", - "tableId": "NA3wwYX9_yPbc5rR8NQP3", - "name": "categori_id", + "TgXJQ_3fDRKT5RxMLCCCW": { + "id": "TgXJQ_3fDRKT5RxMLCCCW", + "tableId": "yu3szoId7FctUvSa0xoTn", + "name": "term_id", "comment": "", "dataType": "BIGINT", "default": "", @@ -1852,96 +1829,12 @@ "widthDefault": 60 }, "meta": { - "updateAt": 1758004970506, - "createAt": 1758004959005 + "updateAt": 1759712966327, + "createAt": 1759712939784 } } }, "relationshipEntities": { - "xK2wIzn8YvFuChzw0Dagu": { - "id": "xK2wIzn8YvFuChzw0Dagu", - "identification": false, - "relationshipType": 8, - "startRelationshipType": 2, - "start": { - "tableId": "2y1bvJOIG1e0BeEWgxI08", - "columnIds": [ - "vDFt6LsnU_OsODOI3QFtU" - ], - "x": 409.8077, - "y": 211.3846, - "direction": 4 - }, - "end": { - "tableId": "C70HRfoZAc7T12El2mwAl", - "columnIds": [ - "ZOfk9HJZaFLK5-NEKO8vl" - ], - "x": 424.5, - "y": 172, - "direction": 8 - }, - "meta": { - "updateAt": 1758003750071, - "createAt": 1758003750071 - } - }, - "CsYdwbemu7dScsssDAGrm": { - "id": "CsYdwbemu7dScsssDAGrm", - "identification": false, - "relationshipType": 16, - "startRelationshipType": 2, - "start": { - "tableId": "2y1bvJOIG1e0BeEWgxI08", - "columnIds": [ - "vDFt6LsnU_OsODOI3QFtU" - ], - "x": 304.2419, - "y": 486.4899, - "direction": 4 - }, - "end": { - "tableId": "C70HRfoZAc7T12El2mwAl", - "columnIds": [ - "DTmy4dcbiGAdTi4A-XWI2" - ], - "x": 305.1974, - "y": 281.6316, - "direction": 8 - }, - "meta": { - "updateAt": 1758003899738, - "createAt": 1758003899738 - } - }, - "SCM3Y82XG6UqMZaqywoaC": { - "id": "SCM3Y82XG6UqMZaqywoaC", - "identification": false, - "relationshipType": 16, - "startRelationshipType": 2, - "start": { - "tableId": "rtTGV1n8fXrv3EOos4XcJ", - "columnIds": [ - "aNu4U_tr1NuWc674pipto" - ], - "x": 821.6032, - "y": 187.4332, - "direction": 1 - }, - "end": { - "tableId": "C70HRfoZAc7T12El2mwAl", - "columnIds": [ - "mHW9ecFk_GQMdjVjAyLpl" - ], - "x": 578.9474, - "y": 193.6316, - "direction": 2 - }, - "meta": { - "updateAt": 1758003967414, - "createAt": 1758003967414 - } - }, "fr8aQL5wJeCHd6nA3FkpV": { "id": "fr8aQL5wJeCHd6nA3FkpV", "identification": false, @@ -1952,8 +1845,8 @@ "columnIds": [ "vDFt6LsnU_OsODOI3QFtU" ], - "x": 577.9919, - "y": 658.4899, + "x": 663.9682, + "y": 883.0328, "direction": 2 }, "end": { @@ -1961,8 +1854,8 @@ "columnIds": [ "2JRKA_JcUA-Bcyt770hvg" ], - "x": 743.7147, - "y": 628.0608, + "x": 798.4637, + "y": 804.3929, "direction": 1 }, "meta": { @@ -1970,146 +1863,6 @@ "createAt": 1758004034426 } }, - "tZExJN6wzYQQN6Tca1jgN": { - "id": "tZExJN6wzYQQN6Tca1jgN", - "identification": false, - "relationshipType": 8, - "startRelationshipType": 2, - "start": { - "tableId": "ETcc-k8p-2Xj7Avc_QuIL", - "columnIds": [ - "PMYbggYiPE_sMz3U_2LqK" - ], - "x": 1241.6559, - "y": 648.0608, - "direction": 2 - }, - "end": { - "tableId": "NA3wwYX9_yPbc5rR8NQP3", - "columnIds": [ - "uM2ODWrSWgyAfDgK5Acu0" - ], - "x": 1510.0089, - "y": 659.2758, - "direction": 1 - }, - "meta": { - "updateAt": 1758004056510, - "createAt": 1758004056510 - } - }, - "gp8EmzSFJVNx_IWEC5SS_": { - "id": "gp8EmzSFJVNx_IWEC5SS_", - "identification": false, - "relationshipType": 8, - "startRelationshipType": 2, - "start": { - "tableId": "2y1bvJOIG1e0BeEWgxI08", - "columnIds": [ - "vDFt6LsnU_OsODOI3QFtU" - ], - "x": 395.4919, - "y": 486.4899, - "direction": 4 - }, - "end": { - "tableId": "C70HRfoZAc7T12El2mwAl", - "columnIds": [ - "ZbsPFDxV-m07EPay0qcI2" - ], - "x": 396.4474, - "y": 257.6316, - "direction": 8 - }, - "meta": { - "updateAt": 1758004190658, - "createAt": 1758004190658 - } - }, - "pAmwkZWnp6pSWMBQJSz0W": { - "id": "pAmwkZWnp6pSWMBQJSz0W", - "identification": false, - "relationshipType": 8, - "startRelationshipType": 2, - "start": { - "tableId": "rtTGV1n8fXrv3EOos4XcJ", - "columnIds": [ - "aNu4U_tr1NuWc674pipto" - ], - "x": 821.6032, - "y": 213.4332, - "direction": 1 - }, - "end": { - "tableId": "C70HRfoZAc7T12El2mwAl", - "columnIds": [ - "lRREo8Fk9Tkbs54oI_Cdg" - ], - "x": 578.9474, - "y": 243.6316, - "direction": 2 - }, - "meta": { - "updateAt": 1758004206973, - "createAt": 1758004206973 - } - }, - "X4sknonb_IgA-_vhYKmss": { - "id": "X4sknonb_IgA-_vhYKmss", - "identification": false, - "relationshipType": 8, - "startRelationshipType": 2, - "start": { - "tableId": "rtTGV1n8fXrv3EOos4XcJ", - "columnIds": [ - "aNu4U_tr1NuWc674pipto" - ], - "x": 842.6559, - "y": 196.6438, - "direction": 1 - }, - "end": { - "tableId": "C70HRfoZAc7T12El2mwAl", - "columnIds": [ - "VThr2Y2U3_ZJAmivzYUhu" - ], - "x": 578.9474, - "y": 181.6316, - "direction": 2 - }, - "meta": { - "updateAt": 1758004221323, - "createAt": 1758004221323 - } - }, - "fi6hg4FJxXwO9G-Q4dkuQ": { - "id": "fi6hg4FJxXwO9G-Q4dkuQ", - "identification": false, - "relationshipType": 16, - "startRelationshipType": 2, - "start": { - "tableId": "NA3wwYX9_yPbc5rR8NQP3", - "columnIds": [ - "F_CCSEveysTRYLn8lx8gE" - ], - "x": 1457.3773, - "y": 592.4335, - "direction": 1 - }, - "end": { - "tableId": "ETcc-k8p-2Xj7Avc_QuIL", - "columnIds": [ - "6KXjwhh7zBv5U9ju-mYIk" - ], - "x": 1241.6559, - "y": 592.0608, - "direction": 2 - }, - "meta": { - "updateAt": 1758004261137, - "createAt": 1758004261137 - } - }, "vUfGlcmJ3e4oTKMnvlBzJ": { "id": "vUfGlcmJ3e4oTKMnvlBzJ", "identification": false, @@ -2120,17 +1873,17 @@ "columnIds": [ "vDFt6LsnU_OsODOI3QFtU" ], - "x": 486.7419, - "y": 486.4899, - "direction": 4 + "x": 663.9682, + "y": 699.0328, + "direction": 2 }, "end": { "tableId": "C70HRfoZAc7T12El2mwAl", "columnIds": [ "mO1A7j7Qok_C85BipXICi" ], - "x": 689.4692, - "y": 254.8081, + "x": 793.9093, + "y": 483.843, "direction": 1 }, "meta": { @@ -2148,8 +1901,8 @@ "columnIds": [ "aNu4U_tr1NuWc674pipto" ], - "x": 1246.7656, - "y": 255.1501, + "x": 1265.3757, + "y": 482.4087, "direction": 1 }, "end": { @@ -2157,8 +1910,8 @@ "columnIds": [ "TOi3d5nz-qA4ggbf5SGaA" ], - "x": 1054.4692, - "y": 254.8081, + "x": 1158.9093, + "y": 483.843, "direction": 2 }, "meta": { @@ -2176,8 +1929,8 @@ "columnIds": [ "F_CCSEveysTRYLn8lx8gE" ], - "x": 1246.1474, - "y": 623.642, + "x": 1285.0693, + "y": 805.9737, "direction": 1 }, "end": { @@ -2185,8 +1938,8 @@ "columnIds": [ "YxWKD6oE6Vx5FQS9ihF1j" ], - "x": 1108.7147, - "y": 628.0608, + "x": 1163.4637, + "y": 804.3929, "direction": 2 }, "meta": { @@ -2204,8 +1957,8 @@ "columnIds": [ "F_CCSEveysTRYLn8lx8gE" ], - "x": 1436.1474, - "y": 759.642, + "x": 1475.0693, + "y": 929.9737, "direction": 8 }, "end": { @@ -2213,8 +1966,8 @@ "columnIds": [ "kFwVTJyPh2e9OaCMgJwgf" ], - "x": 1254.8040999999998, - "y": 1052.9649, + "x": 1204.0704, + "y": 1175.7449, "direction": 2 }, "meta": { @@ -2232,8 +1985,8 @@ "columnIds": [ "5rnWTeGw4icmvmMekej_n" ], - "x": 889.8041, - "y": 1052.9649, + "x": 839.0704, + "y": 1175.7449, "direction": 1 }, "end": { @@ -2241,8 +1994,8 @@ "columnIds": [ "w4-iEtBZVv4oZMo29nEVr" ], - "x": 570.9873, - "y": 1037.228, + "x": 671.2629999999999, + "y": 1172.3362, "direction": 2 }, "meta": { @@ -2260,8 +2013,8 @@ "columnIds": [ "vDFt6LsnU_OsODOI3QFtU" ], - "x": 395.4919, - "y": 830.4899, + "x": 481.4682, + "y": 975.0328, "direction": 8 }, "end": { @@ -2269,8 +2022,8 @@ "columnIds": [ "qyv68yagM3C9_IGyT2VLt" ], - "x": 388.4873, - "y": 937.228, + "x": 488.763, + "y": 1072.3362, "direction": 4 }, "meta": { @@ -2278,65 +2031,138 @@ "createAt": 1758004517394 } }, - "onHzSgg1u3-Nk4tYVf7t0": { - "id": "onHzSgg1u3-Nk4tYVf7t0", + "zdTWn5Wpu4-atC3FVpk1E": { + "id": "zdTWn5Wpu4-atC3FVpk1E", "identification": false, - "relationshipType": 4, + "relationshipType": 8, + "startRelationshipType": 2, + "start": { + "tableId": "rtTGV1n8fXrv3EOos4XcJ", + "columnIds": [ + "aNu4U_tr1NuWc674pipto" + ], + "x": 1447.8757, + "y": 534.4087, + "direction": 8 + }, + "end": { + "tableId": "NA3wwYX9_yPbc5rR8NQP3", + "columnIds": [ + "GnAxf5fMaPNYtyzoiYMA-" + ], + "x": 1446.3018, + "y": 661.4258, + "direction": 4 + }, + "meta": { + "updateAt": 1758004959005, + "createAt": 1758004959005 + } + }, + "nfjgOevdJXMmtpcAEuJM0": { + "id": "nfjgOevdJXMmtpcAEuJM0", + "identification": false, + "relationshipType": 16, "startRelationshipType": 2, "start": { + "tableId": "yu3szoId7FctUvSa0xoTn", + "columnIds": [ + "Ad9Q86E6ce1GUaw49K9ni" + ], + "x": 400.0516, + "y": 338.969, + "direction": 2 + }, + "end": { "tableId": "2y1bvJOIG1e0BeEWgxI08", "columnIds": [ - "vDFt6LsnU_OsODOI3QFtU" + "rgoW8fTWbtyd9QtgnCv4X" ], "x": 304.2419, "y": 486.4899, "direction": 4 }, + "meta": { + "updateAt": 1759712916893, + "createAt": 1759712916893 + } + }, + "wbLj5mUjPu5rltCDHj3XD": { + "id": "wbLj5mUjPu5rltCDHj3XD", + "identification": false, + "relationshipType": 16, + "startRelationshipType": 2, + "start": { + "tableId": "2y1bvJOIG1e0BeEWgxI08", + "columnIds": [ + "vDFt6LsnU_OsODOI3QFtU" + ], + "x": 481.4682, + "y": 607.0328, + "direction": 4 + }, "end": { - "tableId": "EPYnTEbnzHcSM-TV7Yogy", + "tableId": "yu3szoId7FctUvSa0xoTn", "columnIds": [ - "CCq1c0NyEngIoj0AErl8d" + "IJ-OWDU5a6JkkFcx_lY2a" ], - "x": 306.2208, - "y": 344.1056, + "x": 483.085, + "y": 491.9731, "direction": 8 }, "meta": { - "updateAt": 1758004886190, - "createAt": 1758004886190 + "updateAt": 1759712932112, + "createAt": 1759712932112 } }, - "zdTWn5Wpu4-atC3FVpk1E": { - "id": "zdTWn5Wpu4-atC3FVpk1E", + "8aesalFlcdiXVhsHAAd_f": { + "id": "8aesalFlcdiXVhsHAAd_f", "identification": false, - "relationshipType": 8, + "relationshipType": 16, "startRelationshipType": 2, "start": { - "tableId": "rtTGV1n8fXrv3EOos4XcJ", + "tableId": "zrNDU9R9OQONz9IYpCOQv", "columnIds": [ - "aNu4U_tr1NuWc674pipto" + "lzTP7Y9Q2wHxnor3iYbUN" ], - "x": 1429.2656, - "y": 307.1501, + "x": 494.7384, + "y": 260.6367, "direction": 8 }, "end": { - "tableId": "NA3wwYX9_yPbc5rR8NQP3", + "tableId": "yu3szoId7FctUvSa0xoTn", "columnIds": [ - "GnAxf5fMaPNYtyzoiYMA-" + "TgXJQ_3fDRKT5RxMLCCCW" ], - "x": 1436.1474, - "y": 487.642, + "x": 483.085, + "y": 363.9731, "direction": 4 }, "meta": { - "updateAt": 1758004959005, - "createAt": 1758004959005 + "updateAt": 1759712939785, + "createAt": 1759712939785 } } }, "indexEntities": {}, "indexColumnEntities": {}, - "memoEntities": {} + "memoEntities": { + "s-_sL_Y8zYQKmGll8eCR7": { + "id": "s-_sL_Y8zYQKmGll8eCR7", + "value": "", + "ui": { + "x": 196.90721649484536, + "y": 191.75257731958766, + "zIndex": 68, + "width": 116, + "height": 100, + "color": "" + }, + "meta": { + "updateAt": 1759712899185, + "createAt": 1759712899185 + } + } + } } } \ No newline at end of file