diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..627c48a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "mcp__sequential-thinking__sequentialthinking" + ], + "deny": [], + "ask": [] + } +} diff --git a/src/main/java/com/example/umc/UmcApplication.java b/src/main/java/com/example/umc/UmcApplication.java index edb2b55..9ada0a6 100644 --- a/src/main/java/com/example/umc/UmcApplication.java +++ b/src/main/java/com/example/umc/UmcApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class UmcApplication { public static void main(String[] args) { diff --git a/src/main/java/com/example/umc/domain/category/exception/CategoryException.java b/src/main/java/com/example/umc/domain/category/exception/CategoryException.java new file mode 100644 index 0000000..3886476 --- /dev/null +++ b/src/main/java/com/example/umc/domain/category/exception/CategoryException.java @@ -0,0 +1,10 @@ +package com.example.umc.domain.category.exception; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.exception.GeneralException; + +public class CategoryException extends GeneralException { + public CategoryException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc/domain/category/exception/code/CategoryErrorCode.java b/src/main/java/com/example/umc/domain/category/exception/code/CategoryErrorCode.java new file mode 100644 index 0000000..b815027 --- /dev/null +++ b/src/main/java/com/example/umc/domain/category/exception/code/CategoryErrorCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.category.exception.code; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum CategoryErrorCode implements BaseErrorCode { + + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "CATEGORY404_1", "해당 카테고리를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .isSuccess(false) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/category/repository/PreferCategoryRepository.java b/src/main/java/com/example/umc/domain/category/repository/PreferCategoryRepository.java new file mode 100644 index 0000000..213934a --- /dev/null +++ b/src/main/java/com/example/umc/domain/category/repository/PreferCategoryRepository.java @@ -0,0 +1,9 @@ +package com.example.umc.domain.category.repository; + +import com.example.umc.domain.category.entity.PreferCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PreferCategoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/umc/domain/member/controller/MemberController.java b/src/main/java/com/example/umc/domain/member/controller/MemberController.java new file mode 100644 index 0000000..1634ccb --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/controller/MemberController.java @@ -0,0 +1,31 @@ +package com.example.umc.domain.member.controller; + +import com.example.umc.domain.member.dto.MemberReqDTO; +import com.example.umc.domain.member.dto.MemberResDTO; +import com.example.umc.domain.member.exception.code.MemberSuccessCode; +import com.example.umc.domain.member.service.MemberCommandService; +import com.example.umc.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/members") +@RequiredArgsConstructor +@Tag(name = "회원", description = "회원 관련 API") +public class MemberController { + + private final MemberCommandService memberCommandService; + + // 회원가입 + @PostMapping("/signup") + @Operation(summary = "회원가입", description = "새로운 회원을 등록합니다.") + public ApiResponse signUp( + @RequestBody @Valid MemberReqDTO.JoinDTO dto + ) { + MemberResDTO.JoinDTO response = memberCommandService.signup(dto); + return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_CREATED, response); + } +} diff --git a/src/main/java/com/example/umc/domain/member/converter/MemberConverter.java b/src/main/java/com/example/umc/domain/member/converter/MemberConverter.java new file mode 100644 index 0000000..35ca04c --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/converter/MemberConverter.java @@ -0,0 +1,26 @@ +package com.example.umc.domain.member.converter; + +import com.example.umc.domain.member.dto.MemberReqDTO; +import com.example.umc.domain.member.dto.MemberResDTO; +import com.example.umc.domain.user.entity.User; + +public class MemberConverter { + + // Entity -> DTO + public static MemberResDTO.JoinDTO toJoinDTO(User member) { + return MemberResDTO.JoinDTO.builder() + .memberId(member.getUserId()) + .createdAt(member.getCreatedAt()) + .build(); + } + + // DTO -> Entity + public static User toMember(MemberReqDTO.JoinDTO dto) { + return User.builder() + .name(dto.name()) + .birth(dto.birth()) + .address(dto.address() != null ? dto.address().toString() : null) + .gender(dto.gender()) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/member/dto/MemberReqDTO.java b/src/main/java/com/example/umc/domain/member/dto/MemberReqDTO.java new file mode 100644 index 0000000..3c2ee60 --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/dto/MemberReqDTO.java @@ -0,0 +1,17 @@ +package com.example.umc.domain.member.dto; + +import com.example.umc.domain.user.enums.Gender; + +import java.time.LocalDate; +import java.util.List; + +public class MemberReqDTO { + public record JoinDTO( + String name, + Gender gender, + LocalDate birth, + String address, + String specAddress, + List preferCategory + ) {} +} diff --git a/src/main/java/com/example/umc/domain/member/dto/MemberResDTO.java b/src/main/java/com/example/umc/domain/member/dto/MemberResDTO.java new file mode 100644 index 0000000..cf3abee --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/dto/MemberResDTO.java @@ -0,0 +1,13 @@ +package com.example.umc.domain.member.dto; + +import lombok.Builder; + +import java.time.LocalDateTime; + +public class MemberResDTO { + @Builder + public record JoinDTO( + Long memberId, + LocalDateTime createdAt + ) {} +} diff --git a/src/main/java/com/example/umc/domain/member/exception/MemberException.java b/src/main/java/com/example/umc/domain/member/exception/MemberException.java new file mode 100644 index 0000000..e9a19e5 --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/exception/MemberException.java @@ -0,0 +1,10 @@ +package com.example.umc.domain.member.exception; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.exception.GeneralException; + +public class MemberException extends GeneralException { + public MemberException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc/domain/member/exception/code/MemberErrorCode.java b/src/main/java/com/example/umc/domain/member/exception/code/MemberErrorCode.java new file mode 100644 index 0000000..2639071 --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/exception/code/MemberErrorCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.member.exception.code; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MemberErrorCode implements BaseErrorCode { + + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 사용자를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .isSuccess(false) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/member/exception/code/MemberSuccessCode.java b/src/main/java/com/example/umc/domain/member/exception/code/MemberSuccessCode.java new file mode 100644 index 0000000..41ed101 --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/exception/code/MemberSuccessCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.member.exception.code; + +import com.example.umc.global.apiPayload.code.BaseCode; +import com.example.umc.global.apiPayload.code.ReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MemberSuccessCode implements BaseCode { + + MEMBER_CREATED(HttpStatus.CREATED, "MEMBER201_1", "성공적으로 사용자가 생성되었습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return ReasonDto.builder() + .isSuccess(true) + .code(code) + .message(message) + .build(); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return ReasonDto.builder() + .isSuccess(true) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/member/repository/MemberRepository.java b/src/main/java/com/example/umc/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..cab6e98 --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/repository/MemberRepository.java @@ -0,0 +1,9 @@ +package com.example.umc.domain.member.repository; + +import com.example.umc.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/umc/domain/member/service/MemberCommandService.java b/src/main/java/com/example/umc/domain/member/service/MemberCommandService.java new file mode 100644 index 0000000..b720c07 --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/service/MemberCommandService.java @@ -0,0 +1,9 @@ +package com.example.umc.domain.member.service; + +import com.example.umc.domain.member.dto.MemberReqDTO; +import com.example.umc.domain.member.dto.MemberResDTO; + +public interface MemberCommandService { + // 회원가입 + MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto); +} diff --git a/src/main/java/com/example/umc/domain/member/service/MemberCommandServiceImpl.java b/src/main/java/com/example/umc/domain/member/service/MemberCommandServiceImpl.java new file mode 100644 index 0000000..2410fa5 --- /dev/null +++ b/src/main/java/com/example/umc/domain/member/service/MemberCommandServiceImpl.java @@ -0,0 +1,61 @@ +package com.example.umc.domain.member.service; + +import com.example.umc.domain.member.converter.MemberConverter; +import com.example.umc.domain.member.dto.MemberReqDTO; +import com.example.umc.domain.member.dto.MemberResDTO; +import com.example.umc.domain.member.repository.MemberRepository; +import com.example.umc.domain.user.entity.User; +import com.example.umc.domain.user.entity.UserPrefer; +import com.example.umc.domain.user.repository.UserPreferRepository; +import com.example.umc.domain.category.entity.PreferCategory; +import com.example.umc.domain.category.repository.PreferCategoryRepository; +import com.example.umc.domain.category.exception.CategoryException; +import com.example.umc.domain.category.exception.code.CategoryErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class MemberCommandServiceImpl implements MemberCommandService { + + private final MemberRepository memberRepository; + private final UserPreferRepository userPreferRepository; + private final PreferCategoryRepository preferCategoryRepository; + + @Override + @Transactional + public MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto) { + // 사용자 생성 + User member = MemberConverter.toMember(dto); + + // DB 적용 + memberRepository.save(member); + + // 선호 음식 존재 여부 확인 + if (dto.preferCategory() != null && dto.preferCategory().size() > 0) { + List userPreferList = dto.preferCategory().stream() + .map(id -> { + // 선호 카테고리 존재 여부 검증 + PreferCategory preferCategory = preferCategoryRepository.findById(id) + .orElseThrow(() -> new CategoryException(CategoryErrorCode.CATEGORY_NOT_FOUND)); + + // UserPrefer 엔티티 생성 + return UserPrefer.builder() + .user(member) + .preferCategory(preferCategory) + .build(); + }) + .collect(Collectors.toList()); + + // 모든 선호 음식 추가: DB 적용 + userPreferRepository.saveAll(userPreferList); + } + + // 응답 DTO 생성 + return MemberConverter.toJoinDTO(member); + } +} diff --git a/src/main/java/com/example/umc/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..a5b5c96 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/controller/MissionController.java @@ -0,0 +1,41 @@ +package com.example.umc.domain.mission.controller; + +import com.example.umc.domain.mission.dto.MissionReqDTO; +import com.example.umc.domain.mission.dto.MissionResDTO; +import com.example.umc.domain.mission.exception.code.MissionSuccessCode; +import com.example.umc.domain.mission.service.MissionCommandService; +import com.example.umc.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/missions") +@RequiredArgsConstructor +@Tag(name = "미션", description = "미션 관련 API") +public class MissionController { + + private final MissionCommandService missionCommandService; + + // 미션 도전하기 + @PostMapping("/challenge") + @Operation(summary = "미션 도전하기", description = "가게의 미션을 도전 중인 미션에 추가합니다.") + public ApiResponse challengeMission( + @RequestBody @Valid MissionReqDTO.ChallengeMissionDTO dto + ) { + MissionResDTO.ChallengeMissionDTO response = missionCommandService.challengeMission(dto); + return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CHALLENGED, response); + } + + // 미션 추가하기 + @PostMapping("") + @Operation(summary = "미션 추가", description = "가게에 미션을 추가합니다.") + public ApiResponse createMission( + @RequestBody @Valid MissionReqDTO.CreateMissionDTO dto + ) { + MissionResDTO.CreateMissionDTO response = missionCommandService.createMission(dto); + return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CREATED, response); + } +} diff --git a/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java new file mode 100644 index 0000000..60769fd --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/converter/MissionConverter.java @@ -0,0 +1,49 @@ +package com.example.umc.domain.mission.converter; + +import com.example.umc.domain.mission.dto.MissionReqDTO; +import com.example.umc.domain.mission.dto.MissionResDTO; +import com.example.umc.domain.mission.entity.Mission; +import com.example.umc.domain.mission.entity.UserMission; +import com.example.umc.domain.store.entity.Store; +import com.example.umc.domain.user.entity.User; + +import java.time.LocalDateTime; + +public class MissionConverter { + + // Entity -> DTO (ChallengeMission) + public static MissionResDTO.ChallengeMissionDTO toChallengeMissionDTO(UserMission userMission) { + return MissionResDTO.ChallengeMissionDTO.builder() + .challengeMissionId(userMission.getChallengeMissionId()) + .createdAt(userMission.getCreatedAt()) + .build(); + } + + // DTO -> Entity (ChallengeMission) + public static UserMission toUserMission(Mission mission, User user, Store store) { + return UserMission.builder() + .user(user) + .mission(mission) + .store(store) + .challengeAt(LocalDateTime.now()) + .build(); + } + + // Entity -> DTO (CreateMission) + public static MissionResDTO.CreateMissionDTO toCreateMissionDTO(Mission mission) { + return MissionResDTO.CreateMissionDTO.builder() + .missionId(mission.getMissionId()) + .createdAt(mission.getCreatedAt()) + .build(); + } + + // DTO -> Entity (CreateMission) + public static Mission toMission(MissionReqDTO.CreateMissionDTO dto, Store store) { + return Mission.builder() + .store(store) + .region(dto.region()) + .missionMoney(dto.missionMoney()) + .missionPoint(dto.missionPoint()) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/mission/dto/MissionReqDTO.java b/src/main/java/com/example/umc/domain/mission/dto/MissionReqDTO.java new file mode 100644 index 0000000..08fbd46 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/dto/MissionReqDTO.java @@ -0,0 +1,18 @@ +package com.example.umc.domain.mission.dto; + +import com.example.umc.domain.mission.enums.Region; +import com.example.umc.domain.mission.validation.ExistMissions; +import com.example.umc.domain.store.validation.ExistStores; + +public class MissionReqDTO { + public record ChallengeMissionDTO( + @ExistMissions Long missionId) { + } + + public record CreateMissionDTO( + @ExistStores Long storeId, + Region region, + Long missionMoney, + Long missionPoint) { + } +} diff --git a/src/main/java/com/example/umc/domain/mission/dto/MissionResDTO.java b/src/main/java/com/example/umc/domain/mission/dto/MissionResDTO.java new file mode 100644 index 0000000..748b09a --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/dto/MissionResDTO.java @@ -0,0 +1,19 @@ +package com.example.umc.domain.mission.dto; + +import lombok.Builder; + +import java.time.LocalDateTime; + +public class MissionResDTO { + @Builder + public record ChallengeMissionDTO( + Long challengeMissionId, + LocalDateTime createdAt + ) {} + + @Builder + public record CreateMissionDTO( + Long missionId, + LocalDateTime createdAt + ) {} +} diff --git a/src/main/java/com/example/umc/domain/mission/exception/MissionException.java b/src/main/java/com/example/umc/domain/mission/exception/MissionException.java new file mode 100644 index 0000000..952f073 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/exception/MissionException.java @@ -0,0 +1,10 @@ +package com.example.umc.domain.mission.exception; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.exception.GeneralException; + +public class MissionException extends GeneralException { + public MissionException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc/domain/mission/exception/code/MissionErrorCode.java b/src/main/java/com/example/umc/domain/mission/exception/code/MissionErrorCode.java new file mode 100644 index 0000000..177db0a --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/exception/code/MissionErrorCode.java @@ -0,0 +1,39 @@ +package com.example.umc.domain.mission.exception.code; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MissionErrorCode implements BaseErrorCode { + + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404_1", "해당 미션을 찾지 못했습니다."), + MISSION_ALREADY_CHALLENGED(HttpStatus.BAD_REQUEST, "MISSION400_1", "이미 도전 중인 미션입니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .isSuccess(false) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/mission/exception/code/MissionSuccessCode.java b/src/main/java/com/example/umc/domain/mission/exception/code/MissionSuccessCode.java new file mode 100644 index 0000000..c815b05 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/exception/code/MissionSuccessCode.java @@ -0,0 +1,39 @@ +package com.example.umc.domain.mission.exception.code; + +import com.example.umc.global.apiPayload.code.BaseCode; +import com.example.umc.global.apiPayload.code.ReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MissionSuccessCode implements BaseCode { + + MISSION_CHALLENGED(HttpStatus.CREATED, "MISSION201_1", "미션 도전이 성공적으로 등록되었습니다."), + MISSION_CREATED(HttpStatus.CREATED, "MISSION201_2", "미션이 성공적으로 생성되었습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return ReasonDto.builder() + .isSuccess(true) + .code(code) + .message(message) + .build(); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return ReasonDto.builder() + .isSuccess(true) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionCommandService.java b/src/main/java/com/example/umc/domain/mission/service/MissionCommandService.java new file mode 100644 index 0000000..0f6c401 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/service/MissionCommandService.java @@ -0,0 +1,12 @@ +package com.example.umc.domain.mission.service; + +import com.example.umc.domain.mission.dto.MissionReqDTO; +import com.example.umc.domain.mission.dto.MissionResDTO; + +public interface MissionCommandService { + // 미션 도전하기 + MissionResDTO.ChallengeMissionDTO challengeMission(MissionReqDTO.ChallengeMissionDTO dto); + + // 미션 추가하기 + MissionResDTO.CreateMissionDTO createMission(MissionReqDTO.CreateMissionDTO dto); +} diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionCommandServiceImpl.java b/src/main/java/com/example/umc/domain/mission/service/MissionCommandServiceImpl.java new file mode 100644 index 0000000..e0f29a6 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/service/MissionCommandServiceImpl.java @@ -0,0 +1,80 @@ +package com.example.umc.domain.mission.service; + +import com.example.umc.domain.mission.converter.MissionConverter; +import com.example.umc.domain.mission.dto.MissionReqDTO; +import com.example.umc.domain.mission.dto.MissionResDTO; +import com.example.umc.domain.mission.entity.Mission; +import com.example.umc.domain.mission.entity.UserMission; +import com.example.umc.domain.mission.exception.MissionException; +import com.example.umc.domain.mission.exception.code.MissionErrorCode; +import com.example.umc.domain.mission.repository.MissionRepository; +import com.example.umc.domain.mission.repository.UserMissionRepository; +import com.example.umc.domain.store.entity.Store; +import com.example.umc.domain.store.exception.StoreException; +import com.example.umc.domain.store.exception.code.StoreErrorCode; +import com.example.umc.domain.store.repository.StoreRepository; +import com.example.umc.domain.user.entity.User; +import com.example.umc.domain.user.exception.UserException; +import com.example.umc.domain.user.exception.code.UserErrorCode; +import com.example.umc.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MissionCommandServiceImpl implements MissionCommandService { + + private final MissionRepository missionRepository; + private final UserMissionRepository userMissionRepository; + private final UserRepository userRepository; + private final StoreRepository storeRepository; + + @Override + @Transactional + public MissionResDTO.ChallengeMissionDTO challengeMission(MissionReqDTO.ChallengeMissionDTO dto) { + // 하드코딩된 사용자 조회 (user_id = 1) + User user = userRepository.findById(1L) + .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + + // 미션 존재 여부 검증 + Mission mission = missionRepository.findById(dto.missionId()) + .orElseThrow(() -> new MissionException(MissionErrorCode.MISSION_NOT_FOUND)); + + // 가게 정보는 미션에서 가져옴 + Store store = mission.getStore(); + + // 이미 도전 중인 미션인지 확인 (선택적 - 필요시 구현) + // boolean alreadyChallenged = + // userMissionRepository.existsByUserAndMission(user, mission); + // if (alreadyChallenged) { + // throw new MissionException(MissionErrorCode.MISSION_ALREADY_CHALLENGED); + // } + + // UserMission 엔티티 생성 + UserMission userMission = MissionConverter.toUserMission(mission, user, store); + + // DB 저장 + userMissionRepository.save(userMission); + + // 응답 DTO 생성 + return MissionConverter.toChallengeMissionDTO(userMission); + } + + @Override + @Transactional + public MissionResDTO.CreateMissionDTO createMission(MissionReqDTO.CreateMissionDTO dto) { + // 가게 존재 여부 검증 + Store store = storeRepository.findById(dto.storeId()) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + // Mission 엔티티 생성 + Mission mission = MissionConverter.toMission(dto, store); + + // DB 저장 + missionRepository.save(mission); + + // 응답 DTO 생성 + return MissionConverter.toCreateMissionDTO(mission); + } +} diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java b/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java new file mode 100644 index 0000000..3ab16aa --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/service/MissionQueryService.java @@ -0,0 +1,6 @@ +package com.example.umc.domain.mission.service; + +public interface MissionQueryService { + // 미션 존재 여부 확인 + boolean existsById(Long missionId); +} \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java b/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java new file mode 100644 index 0000000..5265c72 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/service/MissionQueryServiceImpl.java @@ -0,0 +1,19 @@ +package com.example.umc.domain.mission.service; + +import com.example.umc.domain.mission.repository.MissionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MissionQueryServiceImpl implements MissionQueryService { + + private final MissionRepository missionRepository; + + @Override + public boolean existsById(Long missionId) { + return missionRepository.existsById(missionId); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc/domain/mission/validation/ExistMissions.java b/src/main/java/com/example/umc/domain/mission/validation/ExistMissions.java new file mode 100644 index 0000000..72915d5 --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/validation/ExistMissions.java @@ -0,0 +1,20 @@ +package com.example.umc.domain.mission.validation; + +import com.example.umc.domain.mission.validation.validator.MissionExistValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = MissionExistValidator.class) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistMissions { + // 디폴트 메시지 생성 + String message() default "해당 미션이 존재하지 않습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/umc/domain/mission/validation/validator/MissionExistValidator.java b/src/main/java/com/example/umc/domain/mission/validation/validator/MissionExistValidator.java new file mode 100644 index 0000000..64bf13e --- /dev/null +++ b/src/main/java/com/example/umc/domain/mission/validation/validator/MissionExistValidator.java @@ -0,0 +1,32 @@ +package com.example.umc.domain.mission.validation.validator; + +import com.example.umc.domain.mission.exception.code.MissionErrorCode; +import com.example.umc.domain.mission.service.MissionQueryService; +import com.example.umc.domain.mission.validation.ExistMissions; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MissionExistValidator implements ConstraintValidator { + + private final MissionQueryService missionQueryService; + + @Override + public boolean isValid(Long value, ConstraintValidatorContext context) { + if (value == null) { + return true; // null은 @NotNull로 처리 + } + + boolean isValid = missionQueryService.existsById(value); + if (!isValid) { + // 디폴트 메시지 초기화 및 새로운 메시지로 덮어씌우기 + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(MissionErrorCode.MISSION_NOT_FOUND.getMessage()) + .addConstraintViolation(); + } + return isValid; + } +} diff --git a/src/main/java/com/example/umc/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc/domain/review/controller/ReviewController.java index 3ab24b8..c7f9901 100644 --- a/src/main/java/com/example/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc/domain/review/controller/ReviewController.java @@ -1,12 +1,17 @@ package com.example.umc.domain.review.controller; +import com.example.umc.domain.review.dto.ReviewReqDTO; +import com.example.umc.domain.review.dto.ReviewResDTO; import com.example.umc.domain.review.dto.ReviewResponseDto; +import com.example.umc.domain.review.exception.code.ReviewSuccessCode; +import com.example.umc.domain.review.service.ReviewCommandService; import com.example.umc.domain.review.service.ReviewService; import com.example.umc.global.apiPayload.ApiResponse; import com.example.umc.global.apiPayload.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -20,6 +25,17 @@ public class ReviewController { private final ReviewService reviewService; + private final ReviewCommandService reviewCommandService; + + // 리뷰 작성 + @PostMapping("") + @Operation(summary = "리뷰 작성", description = "가게에 리뷰를 작성합니다.") + public ApiResponse createReview( + @RequestBody @Valid ReviewReqDTO.CreateReviewDTO dto + ) { + ReviewResDTO.CreateReviewDTO response = reviewCommandService.createReview(dto); + return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_CREATED, response); + } @GetMapping("/my-reviews") @Operation(summary = "내가 작성한 리뷰 조회", description = "특정 사용자가 작성한 리뷰를 가게명과 별점 범위로 필터링하여 조회합니다.") diff --git a/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java new file mode 100644 index 0000000..92dd30c --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/converter/ReviewConverter.java @@ -0,0 +1,28 @@ +package com.example.umc.domain.review.converter; + +import com.example.umc.domain.review.dto.ReviewReqDTO; +import com.example.umc.domain.review.dto.ReviewResDTO; +import com.example.umc.domain.review.entity.Review; +import com.example.umc.domain.store.entity.Store; +import com.example.umc.domain.user.entity.User; + +public class ReviewConverter { + + // Entity -> DTO + public static ReviewResDTO.CreateReviewDTO toCreateReviewDTO(Review review) { + return ReviewResDTO.CreateReviewDTO.builder() + .reviewId(review.getReviewId()) + .createdAt(review.getCreatedAt()) + .build(); + } + + // DTO -> Entity + public static Review toReview(ReviewReqDTO.CreateReviewDTO dto, User user, Store store) { + return Review.builder() + .user(user) + .store(store) + .reviewText(dto.reviewText()) + .score(dto.score()) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/review/dto/ReviewReqDTO.java b/src/main/java/com/example/umc/domain/review/dto/ReviewReqDTO.java new file mode 100644 index 0000000..2a70c28 --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/dto/ReviewReqDTO.java @@ -0,0 +1,13 @@ +package com.example.umc.domain.review.dto; + +import com.example.umc.domain.store.validation.ExistStores; + +import java.math.BigDecimal; + +public class ReviewReqDTO { + public record CreateReviewDTO( + @ExistStores Long storeId, + String reviewText, + BigDecimal score) { + } +} diff --git a/src/main/java/com/example/umc/domain/review/dto/ReviewResDTO.java b/src/main/java/com/example/umc/domain/review/dto/ReviewResDTO.java new file mode 100644 index 0000000..77803a0 --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/dto/ReviewResDTO.java @@ -0,0 +1,13 @@ +package com.example.umc.domain.review.dto; + +import lombok.Builder; + +import java.time.LocalDateTime; + +public class ReviewResDTO { + @Builder + public record CreateReviewDTO( + Long reviewId, + LocalDateTime createdAt + ) {} +} diff --git a/src/main/java/com/example/umc/domain/review/exception/ReviewException.java b/src/main/java/com/example/umc/domain/review/exception/ReviewException.java new file mode 100644 index 0000000..3ea52fc --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/exception/ReviewException.java @@ -0,0 +1,10 @@ +package com.example.umc.domain.review.exception; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.exception.GeneralException; + +public class ReviewException extends GeneralException { + public ReviewException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc/domain/review/exception/code/ReviewErrorCode.java b/src/main/java/com/example/umc/domain/review/exception/code/ReviewErrorCode.java new file mode 100644 index 0000000..3e0c4af --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/exception/code/ReviewErrorCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.review.exception.code; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReviewErrorCode implements BaseErrorCode { + + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 리뷰를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .isSuccess(false) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/review/exception/code/ReviewSuccessCode.java b/src/main/java/com/example/umc/domain/review/exception/code/ReviewSuccessCode.java new file mode 100644 index 0000000..c4bbc3c --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/exception/code/ReviewSuccessCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.review.exception.code; + +import com.example.umc.global.apiPayload.code.BaseCode; +import com.example.umc.global.apiPayload.code.ReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReviewSuccessCode implements BaseCode { + + REVIEW_CREATED(HttpStatus.CREATED, "REVIEW201_1", "리뷰가 성공적으로 작성되었습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return ReasonDto.builder() + .isSuccess(true) + .code(code) + .message(message) + .build(); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return ReasonDto.builder() + .isSuccess(true) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/review/service/ReviewCommandService.java b/src/main/java/com/example/umc/domain/review/service/ReviewCommandService.java new file mode 100644 index 0000000..2a45b3b --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/service/ReviewCommandService.java @@ -0,0 +1,9 @@ +package com.example.umc.domain.review.service; + +import com.example.umc.domain.review.dto.ReviewReqDTO; +import com.example.umc.domain.review.dto.ReviewResDTO; + +public interface ReviewCommandService { + // 리뷰 작성 + ReviewResDTO.CreateReviewDTO createReview(ReviewReqDTO.CreateReviewDTO dto); +} diff --git a/src/main/java/com/example/umc/domain/review/service/ReviewCommandServiceImpl.java b/src/main/java/com/example/umc/domain/review/service/ReviewCommandServiceImpl.java new file mode 100644 index 0000000..b3fe7d2 --- /dev/null +++ b/src/main/java/com/example/umc/domain/review/service/ReviewCommandServiceImpl.java @@ -0,0 +1,48 @@ +package com.example.umc.domain.review.service; + +import com.example.umc.domain.review.converter.ReviewConverter; +import com.example.umc.domain.review.dto.ReviewReqDTO; +import com.example.umc.domain.review.dto.ReviewResDTO; +import com.example.umc.domain.review.entity.Review; +import com.example.umc.domain.review.repository.ReviewRepository; +import com.example.umc.domain.store.entity.Store; +import com.example.umc.domain.store.exception.StoreException; +import com.example.umc.domain.store.exception.code.StoreErrorCode; +import com.example.umc.domain.store.repository.StoreRepository; +import com.example.umc.domain.user.entity.User; +import com.example.umc.domain.user.exception.UserException; +import com.example.umc.domain.user.exception.code.UserErrorCode; +import com.example.umc.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ReviewCommandServiceImpl implements ReviewCommandService { + + private final ReviewRepository reviewRepository; + private final UserRepository userRepository; + private final StoreRepository storeRepository; + + @Override + @Transactional + public ReviewResDTO.CreateReviewDTO createReview(ReviewReqDTO.CreateReviewDTO dto) { + // 하드코딩된 사용자 조회 (user_id = 1) + User user = userRepository.findById(1L) + .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + + // 가게 존재 여부 검증 + Store store = storeRepository.findById(dto.storeId()) + .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + // 리뷰 엔티티 생성 + Review review = ReviewConverter.toReview(dto, user, store); + + // DB 저장 + reviewRepository.save(review); + + // 응답 DTO 생성 + return ReviewConverter.toCreateReviewDTO(review); + } +} diff --git a/src/main/java/com/example/umc/domain/review/service/ReviewService.java b/src/main/java/com/example/umc/domain/review/service/ReviewService.java index 06c83e1..5cd5ce3 100644 --- a/src/main/java/com/example/umc/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/umc/domain/review/service/ReviewService.java @@ -3,7 +3,6 @@ import com.example.umc.domain.review.entity.QReview; import com.example.umc.domain.review.entity.Review; import com.example.umc.domain.review.repository.ReviewRepository; -import com.example.umc.domain.store.entity.QStore; import com.querydsl.core.BooleanBuilder; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/umc/domain/store/controller/StoreController.java b/src/main/java/com/example/umc/domain/store/controller/StoreController.java new file mode 100644 index 0000000..6236854 --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/controller/StoreController.java @@ -0,0 +1,31 @@ +package com.example.umc.domain.store.controller; + +import com.example.umc.domain.store.dto.StoreReqDTO; +import com.example.umc.domain.store.dto.StoreResDTO; +import com.example.umc.domain.store.exception.code.StoreSuccessCode; +import com.example.umc.domain.store.service.StoreCommandService; +import com.example.umc.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/stores") +@RequiredArgsConstructor +@Tag(name = "가게", description = "가게 관련 API") +public class StoreController { + + private final StoreCommandService storeCommandService; + + // 가게 추가 + @PostMapping("") + @Operation(summary = "가게 추가", description = "특정 지역에 가게를 추가합니다.") + public ApiResponse createStore( + @RequestBody @Valid StoreReqDTO.CreateStoreDTO dto + ) { + StoreResDTO.CreateStoreDTO response = storeCommandService.createStore(dto); + return ApiResponse.onSuccess(StoreSuccessCode.STORE_CREATED, response); + } +} diff --git a/src/main/java/com/example/umc/domain/store/converter/StoreConverter.java b/src/main/java/com/example/umc/domain/store/converter/StoreConverter.java new file mode 100644 index 0000000..f9f449b --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/converter/StoreConverter.java @@ -0,0 +1,25 @@ +package com.example.umc.domain.store.converter; + +import com.example.umc.domain.store.dto.StoreReqDTO; +import com.example.umc.domain.store.dto.StoreResDTO; +import com.example.umc.domain.store.entity.Store; + +public class StoreConverter { + + // Entity -> DTO + public static StoreResDTO.CreateStoreDTO toCreateStoreDTO(Store store) { + return StoreResDTO.CreateStoreDTO.builder() + .storeId(store.getStoreId()) + .createdAt(store.getCreatedAt()) + .build(); + } + + // DTO -> Entity + public static Store toStore(StoreReqDTO.CreateStoreDTO dto) { + return Store.builder() + .storeName(dto.storeName()) + .storeAddress(dto.storeAddress()) + .storeType(dto.storeType()) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/store/dto/StoreReqDTO.java b/src/main/java/com/example/umc/domain/store/dto/StoreReqDTO.java new file mode 100644 index 0000000..f055a9a --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/dto/StoreReqDTO.java @@ -0,0 +1,12 @@ +package com.example.umc.domain.store.dto; + +import com.example.umc.domain.store.enums.StoreType; + +public class StoreReqDTO { + public record CreateStoreDTO( + String storeName, + String storeAddress, + StoreType storeType, + String regionName // 지역 이름 + ) {} +} diff --git a/src/main/java/com/example/umc/domain/store/dto/StoreResDTO.java b/src/main/java/com/example/umc/domain/store/dto/StoreResDTO.java new file mode 100644 index 0000000..65abf7f --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/dto/StoreResDTO.java @@ -0,0 +1,13 @@ +package com.example.umc.domain.store.dto; + +import lombok.Builder; + +import java.time.LocalDateTime; + +public class StoreResDTO { + @Builder + public record CreateStoreDTO( + Long storeId, + LocalDateTime createdAt + ) {} +} diff --git a/src/main/java/com/example/umc/domain/store/exception/StoreException.java b/src/main/java/com/example/umc/domain/store/exception/StoreException.java new file mode 100644 index 0000000..cd24b15 --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/exception/StoreException.java @@ -0,0 +1,10 @@ +package com.example.umc.domain.store.exception; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.exception.GeneralException; + +public class StoreException extends GeneralException { + public StoreException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc/domain/store/exception/code/StoreErrorCode.java b/src/main/java/com/example/umc/domain/store/exception/code/StoreErrorCode.java new file mode 100644 index 0000000..859e300 --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/exception/code/StoreErrorCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.store.exception.code; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum StoreErrorCode implements BaseErrorCode { + + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404_1", "해당 가게를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .isSuccess(false) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/store/exception/code/StoreSuccessCode.java b/src/main/java/com/example/umc/domain/store/exception/code/StoreSuccessCode.java new file mode 100644 index 0000000..187441f --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/exception/code/StoreSuccessCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.store.exception.code; + +import com.example.umc.global.apiPayload.code.BaseCode; +import com.example.umc.global.apiPayload.code.ReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum StoreSuccessCode implements BaseCode { + + STORE_CREATED(HttpStatus.CREATED, "STORE201_1", "가게가 성공적으로 생성되었습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return ReasonDto.builder() + .isSuccess(true) + .code(code) + .message(message) + .build(); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return ReasonDto.builder() + .isSuccess(true) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/store/repository/StoreRepository.java b/src/main/java/com/example/umc/domain/store/repository/StoreRepository.java new file mode 100644 index 0000000..f5832fa --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/repository/StoreRepository.java @@ -0,0 +1,9 @@ +package com.example.umc.domain.store.repository; + +import com.example.umc.domain.store.entity.Store; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface StoreRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/umc/domain/store/service/StoreCommandService.java b/src/main/java/com/example/umc/domain/store/service/StoreCommandService.java new file mode 100644 index 0000000..c31642b --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/service/StoreCommandService.java @@ -0,0 +1,9 @@ +package com.example.umc.domain.store.service; + +import com.example.umc.domain.store.dto.StoreReqDTO; +import com.example.umc.domain.store.dto.StoreResDTO; + +public interface StoreCommandService { + // 가게 추가 + StoreResDTO.CreateStoreDTO createStore(StoreReqDTO.CreateStoreDTO dto); +} diff --git a/src/main/java/com/example/umc/domain/store/service/StoreCommandServiceImpl.java b/src/main/java/com/example/umc/domain/store/service/StoreCommandServiceImpl.java new file mode 100644 index 0000000..fd26360 --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/service/StoreCommandServiceImpl.java @@ -0,0 +1,34 @@ +package com.example.umc.domain.store.service; + +import com.example.umc.domain.store.converter.StoreConverter; +import com.example.umc.domain.store.dto.StoreReqDTO; +import com.example.umc.domain.store.dto.StoreResDTO; +import com.example.umc.domain.store.entity.Store; +import com.example.umc.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class StoreCommandServiceImpl implements StoreCommandService { + + private final StoreRepository storeRepository; + + @Override + @Transactional + public StoreResDTO.CreateStoreDTO createStore(StoreReqDTO.CreateStoreDTO dto) { + // 가게 엔티티 생성 + Store store = StoreConverter.toStore(dto); + + // DB 저장 + storeRepository.save(store); + + log.info("가게 생성 완료: {} (지역: {})", store.getStoreName(), dto.regionName()); + + // 응답 DTO 생성 + return StoreConverter.toCreateStoreDTO(store); + } +} diff --git a/src/main/java/com/example/umc/domain/store/validation/ExistStores.java b/src/main/java/com/example/umc/domain/store/validation/ExistStores.java new file mode 100644 index 0000000..e74c297 --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/validation/ExistStores.java @@ -0,0 +1,20 @@ +package com.example.umc.domain.store.validation; + +import com.example.umc.domain.store.validation.validator.StoreExistValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = StoreExistValidator.class) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistStores { + // 디폴트 메시지 생성 + String message() default "해당 가게가 존재하지 않습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/umc/domain/store/validation/validator/StoreExistValidator.java b/src/main/java/com/example/umc/domain/store/validation/validator/StoreExistValidator.java new file mode 100644 index 0000000..0582dd7 --- /dev/null +++ b/src/main/java/com/example/umc/domain/store/validation/validator/StoreExistValidator.java @@ -0,0 +1,32 @@ +package com.example.umc.domain.store.validation.validator; + +import com.example.umc.domain.store.exception.code.StoreErrorCode; +import com.example.umc.domain.store.repository.StoreRepository; +import com.example.umc.domain.store.validation.ExistStores; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StoreExistValidator implements ConstraintValidator { + + private final StoreRepository storeRepository; + + @Override + public boolean isValid(Long value, ConstraintValidatorContext context) { + if (value == null) { + return true; // null은 @NotNull로 처리 + } + + boolean isValid = storeRepository.existsById(value); + if (!isValid) { + // 디폴트 메시지 초기화 및 새로운 메시지로 덮어씌우기 + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(StoreErrorCode.STORE_NOT_FOUND.getMessage()) + .addConstraintViolation(); + } + return isValid; + } +} diff --git a/src/main/java/com/example/umc/domain/user/exception/UserException.java b/src/main/java/com/example/umc/domain/user/exception/UserException.java new file mode 100644 index 0000000..9349a67 --- /dev/null +++ b/src/main/java/com/example/umc/domain/user/exception/UserException.java @@ -0,0 +1,10 @@ +package com.example.umc.domain.user.exception; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.exception.GeneralException; + +public class UserException extends GeneralException { + public UserException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc/domain/user/exception/code/UserErrorCode.java b/src/main/java/com/example/umc/domain/user/exception/code/UserErrorCode.java new file mode 100644 index 0000000..98f23d6 --- /dev/null +++ b/src/main/java/com/example/umc/domain/user/exception/code/UserErrorCode.java @@ -0,0 +1,38 @@ +package com.example.umc.domain.user.exception.code; + +import com.example.umc.global.apiPayload.code.BaseErrorCode; +import com.example.umc.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum UserErrorCode implements BaseErrorCode { + + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404_1", "해당 사용자를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .isSuccess(false) + .httpStatus(status) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/example/umc/domain/user/repository/UserPreferRepository.java b/src/main/java/com/example/umc/domain/user/repository/UserPreferRepository.java new file mode 100644 index 0000000..aa392cc --- /dev/null +++ b/src/main/java/com/example/umc/domain/user/repository/UserPreferRepository.java @@ -0,0 +1,10 @@ +package com.example.umc.domain.user.repository; + +import com.example.umc.domain.user.entity.UserPrefer; +import com.example.umc.domain.user.entity.UserPreferId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserPreferRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/umc/global/config/SecurityConfig.java b/src/main/java/com/example/umc/global/config/SecurityConfig.java index 307085d..bd138b5 100644 --- a/src/main/java/com/example/umc/global/config/SecurityConfig.java +++ b/src/main/java/com/example/umc/global/config/SecurityConfig.java @@ -18,6 +18,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authz -> authz // API 경로 허용 .requestMatchers("/api/v1/reviews/**").permitAll() + .requestMatchers("/api/v1/stores/**").permitAll() + .requestMatchers("/api/v1/missions/**").permitAll() + .requestMatchers("/api/v1/members/**").permitAll() .requestMatchers("/temp/**").permitAll() .requestMatchers("/swagger-ui/**").permitAll() .requestMatchers("/v3/api-docs/**").permitAll() diff --git a/src/main/java/com/example/umc/global/config/SwaggerConfig.java b/src/main/java/com/example/umc/global/config/SwaggerConfig.java new file mode 100644 index 0000000..30f54f3 --- /dev/null +++ b/src/main/java/com/example/umc/global/config/SwaggerConfig.java @@ -0,0 +1,36 @@ +package com.example.umc.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("Project").description("Project Swagger").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); + } +} diff --git a/src/main/java/com/example/umc/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/umc/global/exception/GlobalExceptionHandler.java index b77edc9..2f14b77 100644 --- a/src/main/java/com/example/umc/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/umc/global/exception/GlobalExceptionHandler.java @@ -68,10 +68,21 @@ public ResponseEntity handleConstraintViolationException(ConstraintViola protected ResponseEntity handleExceptionInternal(Exception e, Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) { + // body가 null인 경우 기본 에러 응답 반환 + if (body == null) { + return handleExceptionInternalFalse(e, + ApiResponse.onFailure( + ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getCode(), + ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getMessage(), + null), + headers, statusCode, request, e.getMessage()); + } + + ErrorReasonDto errorReason = (ErrorReasonDto) body; return handleExceptionInternalFalse(e, ApiResponse.onFailure( - ((ErrorReasonDto) body).getCode(), - ((ErrorReasonDto) body).getMessage(), - null), headers, statusCode, request, ((ErrorReasonDto) body).getMessage()); + errorReason.getCode(), + errorReason.getMessage(), + null), headers, statusCode, request, errorReason.getMessage()); } private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHeaders headers,