diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30__1_57__Changes_1.xml b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30__1_57__Changes_1.xml
deleted file mode 100644
index 2646ec10..00000000
--- a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30__1_57__Changes_1.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git "a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30_\354\230\244\355\233\204_1_57_[Changes]1/shelved.patch" "b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30_\354\230\244\355\233\204_1_57_[Changes]1/shelved.patch"
deleted file mode 100644
index e4e70c9a..00000000
--- "a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2026-04-30_\354\230\244\355\233\204_1_57_[Changes]1/shelved.patch"
+++ /dev/null
@@ -1,206 +0,0 @@
-Index: Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
-IDEA additional info:
-Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
-<+>package com.example.umc10th.domain.member.service;\r\n\r\npublic class MemberService {\r\n}\r\n
-Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
-<+>UTF-8
-===================================================================
-diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java (revision fdc37513896120be201c90f5927b89f8037b3aa3)
-+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java (date 1777524542470)
-@@ -1,4 +1,30 @@
- package com.example.umc10th.domain.member.service;
-
-+import com.example.umc10th.domain.member.code.MemberErrorCode;
-+import com.example.umc10th.domain.member.dto.MemberReqDTO;
-+import com.example.umc10th.domain.member.dto.MemberResDTO;
-+import com.example.umc10th.domain.member.entity.Member;
-+import com.example.umc10th.domain.member.exception.MemberException;
-+import com.example.umc10th.domain.member.repository.MemberRepository;
-+
- public class MemberService {
-+
-+ private final MemberRepository memberRepository;
-+
-+ public MemberService(MemberRepository memberRepository) {
-+ this.memberRepository = memberRepository;
-+ }
-+
-+ public MemberResDTO.GetInfo getInfo(
-+ MemberReqDTO.GetInfo dto
-+ ) {
-+ // DTO에서 유저 ID를 추출
-+ Long memebrId = dto.id();
-+ // DB에서 해당 유저 ID로 데이터 조회
-+ Member member = memberRepository.findById(memberId)
-+ .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
-+ }
-+
-+ public Object getInfo(MemberReqDTO.GetInfo dto) {
-+ }
- }
-Index: Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
-IDEA additional info:
-Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
-<+>package com.example.umc10th.domain.member.converter;\r\n\r\npublic class MemberConverter {\r\n}\r\n
-Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
-<+>UTF-8
-===================================================================
-diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java (revision fdc37513896120be201c90f5927b89f8037b3aa3)
-+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java (date 1777523938149)
-@@ -1,4 +1,20 @@
- package com.example.umc10th.domain.member.converter;
-
-+import com.example.umc10th.domain.member.dto.MemberResDTO;
-+import com.example.umc10th.domain.member.entity.Member;
-+
- public class MemberConverter {
--}
-+
-+ // 마이페이지 정보 조회를 위한 DTO 변환
-+ public static MemberResDTO.GetInfo toGetInfo(Member member) {
-+
-+ // 빌더 패턴을 사용하여 엔티티의 데이터를 DTO에 매핑합니다.
-+ return MemberResDTO.GetInfo.builder()
-+ .email(member.getEmail())
-+ .name(member.getName())
-+ .point(member.getPoint())
-+ .phoneNumber(member.getPhoneNumber())
-+ .profileUrl(member.getProfileUrl())
-+ .build();
-+ }
-+}
-\ No newline at end of file
-Index: Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
-IDEA additional info:
-Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
-<+>package com.example.umc10th.domain.member.controller;\r\n\r\nimport com.example.umc10th.domain.member.dto.MemberReqDTO;\r\nimport com.example.umc10th.domain.member.dto.MemberResDTO;\r\nimport com.example.umc10th.global.apiPayload.ApiResponse;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\n@RestController\r\n@RequestMapping(\"/api\")\r\npublic class MemberController {\r\n\r\n // 마이페이지\r\n @PostMapping(\"/v1/users/me\")\r\n public ApiResponse getInfo(@RequestBody MemberReqDTO.GetInfo dto) {\r\n\r\n\r\n MemberResDTO.GetInfo result = MemberResDTO.GetInfo.builder()\r\n .name(\"철수\")\r\n .profileUrl(\"https://example.com/profile.jpg\")\r\n .email(\"patrick@hansung.ac.kr\")\r\n .phoneNumber(\"010-1234-5678\")\r\n .point(22500)\r\n .build();\r\n\r\n return ApiResponse.onSuccess(result);\r\n }\r\n}
-Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
-<+>UTF-8
-===================================================================
-diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java (revision fdc37513896120be201c90f5927b89f8037b3aa3)
-+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java (date 1777524675505)
-@@ -1,27 +1,27 @@
- package com.example.umc10th.domain.member.controller;
-
-+import com.example.umc10th.domain.member.code.MemberSuccessCode;
- import com.example.umc10th.domain.member.dto.MemberReqDTO;
- import com.example.umc10th.domain.member.dto.MemberResDTO;
-+import com.example.umc10th.domain.member.service.MemberService;
- import com.example.umc10th.global.apiPayload.ApiResponse;
-+import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
-+import lombok.RequiredArgsConstructor;
- import org.springframework.web.bind.annotation.*;
-
- @RestController
-+@RequiredArgsConstructor
- @RequestMapping("/api")
- public class MemberController {
-
-+ private final MemberService memberService;
-+
- // 마이페이지
- @PostMapping("/v1/users/me")
-- public ApiResponse getInfo(@RequestBody MemberReqDTO.GetInfo dto) {
--
--
-- MemberResDTO.GetInfo result = MemberResDTO.GetInfo.builder()
-- .name("철수")
-- .profileUrl("https://example.com/profile.jpg")
-- .email("patrick@hansung.ac.kr")
-- .phoneNumber("010-1234-5678")
-- .point(22500)
-- .build();
--
-- return ApiResponse.onSuccess(result);
-+ public ApiResponse getInfo(
-+ @RequestBody MemberReqDTO.GetInfo dto
-+ ){
-+ BaseSuccessCode code = MemberSuccessCode.OK;
-+ return ApiResponse.onSuccess(code, memberService.getInfo(dto));
- }
- }
-\ No newline at end of file
-Index: Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
-IDEA additional info:
-Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
-<+>package com.example.umc10th.domain.member.repository;\r\n\r\npublic class MemberRepository {\r\n}\r\n
-Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
-<+>UTF-8
-===================================================================
-diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java (revision fdc37513896120be201c90f5927b89f8037b3aa3)
-+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java (date 1777524765052)
-@@ -1,4 +1,9 @@
- package com.example.umc10th.domain.member.repository;
-
--public class MemberRepository {
--}
-+import com.example.umc10th.domain.member.entity.Member;
-+import org.springframework.data.jpa.repository.JpaRepository;
-+
-+// JpaRepository<엔티티 타입, ID 타입>을 상속받아야 findById를 사용할 수 있어요!
-+public interface MemberRepository extends JpaRepository {
-+
-+}
-\ No newline at end of file
-Index: Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
-IDEA additional info:
-Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
-<+>package com.example.umc10th.domain.member.dto;\r\n\r\npublic class MemberReqDTO {\r\n\r\n public record GetInfo(\r\n Long id\r\n ) {}\r\n}
-Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
-<+>UTF-8
-===================================================================
-diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
---- a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java (revision fdc37513896120be201c90f5927b89f8037b3aa3)
-+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java (date 1777386765775)
-@@ -5,4 +5,4 @@
- public record GetInfo(
- Long id
- ) {}
--}
-\ No newline at end of file
-+}
-Index: Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java
-===================================================================
-diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java
-new file mode 100644
---- /dev/null (date 1777523463270)
-+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberErrorCode.java (date 1777523463270)
-@@ -0,0 +1,4 @@
-+package com.example.umc10th.domain.member.code;
-+
-+public enum MemberErrorCode {
-+}
-Index: .idea/workspace.xml
-IDEA additional info:
-Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
-<+>\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {\r\n "lastFilter": {\r\n "state": "OPEN",\r\n "assignee": "kjhh2605"\r\n }\r\n}\r\n {\r\n "selectedUrlAndAccountId": {\r\n "url": "https://github.com/HSU-Makeus-Challenge-10th/Spring-Boot.git",\r\n "accountId": "fcd0d66d-3517-4e5c-8ca2-d8f1db187712"\r\n }\r\n}\r\n {}\r\n {\r\n "isMigrated": true\r\n}\r\n {\r\n "associatedIndex": 5\r\n}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n 1775064304989\r\n \r\n \r\n 1775064304989\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
-Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
-<+>UTF-8
-===================================================================
-diff --git a/.idea/workspace.xml b/.idea/workspace.xml
---- a/.idea/workspace.xml (revision fdc37513896120be201c90f5927b89f8037b3aa3)
-+++ b/.idea/workspace.xml (date 1777386904627)
-@@ -5,10 +5,7 @@
-
-
-
--
--
-
--
-
-
-
-@@ -140,7 +137,7 @@
-
-
-
--
-+
-
-
-
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 98dfcc30..19f593d0 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,30 +4,25 @@
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -60,16 +55,16 @@
-
-
+
+
@@ -98,36 +93,36 @@
- {
+ "keyToString": {
+ "Gradle.Build Jinyong.executor": "Run",
+ "Gradle.Download Sources.executor": "Run",
+ "Gradle.Jinyong 빌드.executor": "Run",
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
+ "RequestMappingsPanelOrder0": "0",
+ "RequestMappingsPanelOrder1": "1",
+ "RequestMappingsPanelWidth0": "75",
+ "RequestMappingsPanelWidth1": "75",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "Spring Boot.Umc10thApplication.executor": "Run",
+ "git-widget-placeholder": "Jinyong-week7",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/umc10th_SpringBoot",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "project.structure.last.edited": "Modules",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.0",
+ "settings.editor.selected.configurable": "preferences.language.and.region",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
@@ -135,6 +130,11 @@
+
+
+
+
+
@@ -167,7 +167,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1778417464374
+
+
+
+ 1778417464374
+
+
+ 1778418177577
+
+
+
+ 1778418177577
+
+
@@ -184,6 +218,11 @@
+
+
+
+
+
diff --git a/Jinyong/build.gradle b/Jinyong/build.gradle
index fbe47215..d4404db0 100644
--- a/Jinyong/build.gradle
+++ b/Jinyong/build.gradle
@@ -27,12 +27,17 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testCompileOnly 'org.projectlombok:lombok'
+ testRuntimeOnly 'com.h2database:h2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1'
+
+ // Security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
diff --git a/Jinyong/keyword_summary/ch07.md b/Jinyong/keyword_summary/ch07.md
new file mode 100644
index 00000000..ae10a571
--- /dev/null
+++ b/Jinyong/keyword_summary/ch07.md
@@ -0,0 +1,111 @@
+- Page와 Slice
+
+ Page와 Slice는 Spring Data JPA에서 페이징 처리를 할 때 사용하는 반환 타입이다.
+
+ 여기서 **페이징**이란? → 전체 데이터를 한번에 가져오는 게 아니라, 사용자가 요청한 만큼만 나누어 가져오는 방식이다.
+
+ ex) 리뷰가 10000개인데 한번에 가져오면 무거움 → 10개씩 나눠서 조회하는 방식
+
+ Slice는 다음 또는 이전 Slice가 있는지 알 수 있는 데이터 조각이고, Page는 전체 페이지 수나 전체 데이터 수 같은 추가 정보를 포함
+
+ Page는 전체 데이터 개수, 전체 페이지 수, 현재 페이지 번호, 다음 페이지 존재 여부 등 많은 것을 한꺼번에 제공한다. → 전체 조회 시 유리
+
+ Slice는 전체 개수까진 알 필요 없고, 다음 페이지가 있는지 여부 정도만 알려준다.
+
+ ex) 무한 스크롤에서는 굳이 전체 페이지가 필요하지 않다.
+
+ → Page보다 가볍게 사용 가능하다.
+
+ 차이점 (표)
+
+ | 구분 | Page | Slice |
+ | --- | --- | --- |
+ | 전체 데이터 개수 | 알 수 있음 | 알 수 없음 |
+ | 전체 페이지 수 | 알 수 있음 | 알 수 없음 |
+ | 다음 페이지 여부 | 알 수 있음 | 알 수 있음 |
+ | count 쿼리 | 보통 실행됨 | 보통 필요 없음 |
+ | 적합한 상황 | 게시판, 관리자 페이지 | 무한 스크롤, 더보기 버튼 |
+
+ Page는 전체 페이지 수와 전체 데이터 개수가 필요한 경우에 사용하고, Slice는 다음 데이터가 있는지만 알면 되는 무한 스크롤 방식에 적합하다.
+
+- Java stream API
+
+ Java stream API는 컬렉션, 배열 같은 데이터 흐름처럼 처리할 수 있게 해주는 기능이다.
+
+ ex) List에 여러 개의 데이터가 있을 때, 반복문을 작성하지 않고도 filter, map, collect 같이 메서드를 이용해 데이터를 다룰 수 있다.
+
+ **why? 왜 사용할까?**
+
+ 1. 조건에 맞는 데이터만 걸러내기 위해서
+ 2. 객체에서 필요한 값만 뽑아낼려고
+ 3. 데이터를 다른 형태로 바꾸기 위해서
+ 4. 결과를 다시 List로 만들기 위해
+
+ 기존 for문, if문으로 쓰던 방식에서 반복문을 줄이고, 데이터 처리 의도를 더 명확하게 표현하기 위해서 쓴다
+
+ Stream API는 Spring Boot에서 **조회한 Entity 목록을 Response DTO 목록으로 바꿀 때** 많이 사용되기 때문에 Spring Boot에서 자주 사용된다.
+
+ 한줄 요약
+
+ : Stream API는 컬렉션 데이터를 반복문 없이 깔끔하게 처리하기 위한 Java 기능입니다. 특히 `filter`, `map`, `toList`를 많이 사용하며, Spring Boot에서는 Entity를 DTO로 변환할 때 자주 사용된다.
+
+- 객체 그래프 탐색
+
+ 객체 그래프 탐색이란 객체가 가진 연관관계를 따라가며 다른 객체에 접근하는 것을 말한다.
+
+ (객체 지향 언어에서 참조를 사용하여 연관된 객체를 타고 들어가 데이터를 조회하는 방식)
+ 예를 들어 `Review`가 `Member`와 연결되어 있다고 가정할 때,
+
+ ```java
+ Review review = reviewRepository.findById(1L).get();
+
+ String nickname = review.getMember().getNickname();
+ ```
+
+ 여기서 review.getMember()를 통해 Review 객체에서 연결된 Member 객체로 이동한다.
+ 이처럼 객체의 연관관계를 따라가며 접근하는 것을 객체 그래프 탐색이라고 볼 수 있다.
+
+ 객체 그래프 탐색은 JPA의 핵심 장점 중 하나이다.
+
+ JPA는 DB 테이블을 Java 객체처럼 다룰 수 있게 해주는데, SQL 직접 작성 시 필요한 조인(Join)의 제약에서 벗어나, 논리적인 도메인 모델 구조에 따라 데이터를 조회 가능하게 해준다.
+
+ 주의할 점으로는, 지연 로딩과 N + 1문제가 있는데, 연관 객체에 접근하다가 추가 쿼리가 발생할 수 있는 것을 알아야 한다.
+
+ ex) 리뷰 목록 조회 후 리뷰 작성자 닉네임을 가져올려고 하는 때에,
+
+ 리뷰 목록 조회 1번
+
+ 리뷰 10개의 작성자 조회 10번
+ = 총 11번 쿼리 ⇒ 이런 문제 발생 할 수도 있음
+
+ 해결 방향으로는, 연관된 객체를 언제 함께 가져올지 신경 써야 한다.
+
+ `@EntityGraph`를 사용하여 조회 시 연관 엔티티를 함께 가져오는 방식으로 사용할 수 있다.
+
+ 한줄요약
+
+ :객체 그래프 탐색은 객체의 연관관계를 따라 다른 객체에 접근하는 방식이다. JPA에서는 매우 자연스러운 방식이지만, 연관 객체 접근 시 추가 쿼리가 발생할 수 있으므로 N+1 문제를 주의해야 한다.
+
+- @Valid vs @Validated
+
+ `@Valid`와 `@Validated`는 모두 **요청 데이터의 유효성 검증**을 위해 사용한다.
+
+ ex) 회원가입 요청에서 이메일이 비어 있거나 형식이 잘못된 경우, 컨트롤러까지 들어온 데이터를 검증해서 잘못된 요청을 막을 수 있다.(DB까지 가지 않고 controller에서 조기 진압)
+
+ `@Valid`는 필드, 메서드 파라미터, 반환값 등에 붙여서 해당 객체와 내부 속성에 정의된 제약 조건을 검증하게 해준다.
+
+ 즉, `@Valid`는 주로 **Request Body DTO 검증**에 많이 사용된다.
+
+ `@Validated`는 Spring 기반 메서드 검증을 활성화하거나, 검증 그룹을 지정할 때 사용할 수 있다.
+
+ `@RequestParam`이나 `@PathVariable`에 붙은 `@Min`, `@NotNull` 같은 검증을 제대로 적용하려면 컨트롤러 클래스에 `@Validated`를 붙이는 경우가 많다.
+
+ @Valid와 @Validated 차이점(표)
+
+ | 구분 | @Valid | @Validated |
+ | --- | --- | --- |
+ | 주 사용 위치 | RequestBody DTO 검증 | 클래스, 메서드, 파라미터 검증 |
+ | 검증 그룹 | 기본적으로 그룹 지정 어려움 | 그룹 지정 가능 |
+ | 자주 쓰는 상황 | `@RequestBody @Valid DTO` | `@RequestParam`, `@PathVariable`, Service 메서드 검증 |
+
+ `@Valid`는 DTO 내부 필드 검증에 주로 사용되고, `@Validated`는 메서드 파라미터 검증이나 검증 그룹이 필요할 때 사용한다.
\ No newline at end of file
diff --git a/Jinyong/keyword_summary/ch08.md b/Jinyong/keyword_summary/ch08.md
new file mode 100644
index 00000000..ba23e712
--- /dev/null
+++ b/Jinyong/keyword_summary/ch08.md
@@ -0,0 +1,57 @@
+- Spring Security가 무엇인가?
+
+ 스프링 시큐리티(Spring Security)는 **자바 기반 웹 애플리케이션의 인증과 인가, 그리고 보안 위협으로부터의 보호를 처리하는 강력한 보안 프레임워크**
+
+ 즉, 사용자가 어떤 API에 접근할 수 있는지 판단하며, CSRF같은 웹 보안 위협에 대응할 수 있도록 도와주고, 인증과 인가또한 처리해주는 도구이다.
+
+ 핵심 동작은 Filter Chain으로 클라이언트의 요청이 Controller에 바로 도달하는 것이 아니라, 먼저 여러 보안 필터를 순서대로 통과한다.
+
+ 로그인 여부 확인, 인증 객체 생성, 권한 검사, 예외처리 등을 필터들이 담당한다.
+
+ 순서:
+
+ 요청 → 필터 체인 → 인증 확인 → 권한 확인 → Controller 도달 또는 예외 응답
+
+- 인증(Authentication)vs 인가(Authorization)
+
+ 인증(Authentication)은 **사용자가 누구인지 확인하는 과정**이다. 예를 들어 사용자가 메일과 비밀 번호를 입력했을 때, 실제로 가입된 사용자인지 확인하는 것이 “인증”이다. → “당신은 누구….?”
+
+ 인가(Authorization)는 **인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정**이다. 예를 들어 로그인은 성공했지만 일반 사용자가 관리자 페이지에 접근하려고 할 때 접근을 허용할지 거부할지 판단하는 것이 “인가”이다. → “이 사용자가 해당 리소스에 접근할 권한이 있는가??”
+
+ 인증: 로그인해서 신원을 확인하는 단계
+
+ 인가: 로그인한 사용자가 어디까지 접근할 수 있는지 정하는 단계
+
+- Stateful vs Stateless
+
+ Stateful은 서버가 사용자의 로그인 상태를 기억하는 방식이다.
+
+ 대표적인 예시는 세션 기반 로그인으로 사용자가 로그인하면 서버는 세션에 인증 정보를 저장하고, 클라이언트는 세션 ID를 쿠키로 들고 다닌다.
+
+ 이후 요청이 들어오면 서버는 세션 ID를 보고 사용자가 로그인한 상태인지 확인한다.
+
+ Stateless는 서버가 사용자의 로그인 상태를 저장하지 않는 방식이다. 대표적인 예시는 JWT 기반 인증이다.
+
+ 사용자가 로그인하면 서버는 Access Token 같은 토큰을 발급하고, 이후 클라이언트는 요청마다 토큰을 함께 보낸다.
+
+ 서버는 세션을 조회하는 대신, 요청에 포함된 토큰을 검증해서 사용자를 판단한다.
+
+ Stateful 방식은 서버가 로그인 상태를 기억하기 때문에 흐름이 직관적이다. 로그인 성공 후 서버 세션에 인증 정보를 저장하고, 이후 요청마다 세션을 통해 로그인 여부르 판단 할 수 있다. Spring Security의 폼 로그인 방식은 기본적으로 세션을 활용하는 Stateful 방식에 가깝다
+
+ 이와 다르게 Stateless는 서버가 로그인 상태를 기억하지 않는다. 따라서 서버 확장에 유리하지만, 클라이언트가 매 요청마다 토큰을 보내야 한다.
+
+ JWT를 사용하는 REST API 서버에서는 보통 `SessionCreationPolicy.STATELESS`를 설정하고, 서버가 세션을 만들지 않도록 구성한다.
+
+ Stateful의 장/단점
+
+ 장점 :구현 흐름이 쉽다. 서버가 세션을 직접 관리하기에 로그아웃이나 세션 만료처리가 명확함.
+
+ 단점: 사용자가 많아질수록 서버가 세션 정보를 관리해야 하므로 부담이 커질 수 있고, 서버가 여러 대일 경우 세션 공유 문제를 고려해야 함
+
+ Stateless의 장/단점
+
+ 장점: 서버가 세션을 저장하지 않기 때문에 확장성에 유리하다. 여러 서버로 트래픽을 분산해도 각 서버가 토큰만 검증하면 되므로 REST API, 모바일 앱, MSA 구조에서 자주 사용함
+
+ 단점: 토큰 탈취, 만료 시간, Refresh Token 관리, 로그아웃 처리 등을 신중하게 설계해야 함
+
+ **차이점은 결국, 서버가 로그인 상태를 기억하냐 못 하냐**
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberSuccessCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberSuccessCode.java
index f6ec9859..0f1265a6 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberSuccessCode.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/code/MemberSuccessCode.java
@@ -11,9 +11,12 @@ public enum MemberSuccessCode implements BaseSuccessCode {
OK(HttpStatus.OK,
"MEMBER200_1",
- "성공적으로 유저를 조회했습니다.");
+ "성공적으로 유저를 조회했습니다."),
+ CREATED(HttpStatus.CREATED,
+ "MEMBER201_1",
+ "회원가입이 완료되었습니다.");
private final HttpStatus status;
private final String code;
private final String message;
-}
\ No newline at end of file
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
index 144dc0e5..a9a39488 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
@@ -24,4 +24,13 @@ public ApiResponse getInfo(
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.onSuccess(code, memberService.getInfo(memberId));
}
-}
\ No newline at end of file
+
+ // 회원가입
+ @PostMapping("/v1/auth/signup")
+ public ApiResponse signUp(
+ @RequestBody MemberReqDTO.SignUp request
+ ) {
+ BaseSuccessCode code = MemberSuccessCode.CREATED;
+ return ApiResponse.onSuccess(code, memberService.signUp(request));
+ }
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
index 3b40376a..3745b129 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
@@ -1,8 +1,12 @@
package com.example.umc10th.domain.member.converter;
+import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
+import com.example.umc10th.domain.member.entity.Gender;
import com.example.umc10th.domain.member.entity.Member;
+import java.time.LocalDate;
+
public class MemberConverter {
// 마이페이지 정보 조회를 위한 DTO 변환
@@ -18,4 +22,23 @@ public static MemberResDTO.GetInfo toGetInfo(Member member) {
.phoneNumber(null)
.build();
}
+
+ public static Member toMember(MemberReqDTO.SignUp request, String encodedPassword) {
+ return Member.builder()
+ .email(request.email())
+ .password(encodedPassword)
+ .name(request.email())
+ .nickname(request.email())
+ .gender(Gender.NONE)
+ .point(0)
+ .birth(LocalDate.now())
+ .build();
+ }
+
+ public static MemberResDTO.SignUp toSignUp(Member member) {
+ return MemberResDTO.SignUp.builder()
+ .memberId(member.getId())
+ .email(member.getEmail())
+ .build();
+ }
}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
index de6ba086..1aaead74 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
@@ -5,4 +5,9 @@ public class MemberReqDTO {
public record GetInfo(
Long id
){}
+
+ public record SignUp(
+ String email,
+ String password
+ ){}
}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java
index 833e1ee3..f183d7cc 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java
@@ -12,4 +12,10 @@ public record GetInfo(
String nickname,
String gender
) {}
-}
\ No newline at end of file
+
+ @Builder
+ public record SignUp(
+ Long memberId,
+ String email
+ ) {}
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java
index ab02b778..c5a98d5f 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java
@@ -11,7 +11,10 @@ public enum MemberErrorCode implements BaseErrorCode {
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND,
"MEMBER404",
- "해당 사용자를 찾을 수 없습니다.");
+ "해당 사용자를 찾을 수 없습니다."),
+ MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT,
+ "MEMBER409_1",
+ "이미 존재하는 이메일입니다.");
private final HttpStatus httpStatus;
private final String code;
@@ -21,4 +24,4 @@ public enum MemberErrorCode implements BaseErrorCode {
public HttpStatus getStatus() {
return httpStatus;
}
-}
\ No newline at end of file
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
index e878bbb5..0b676788 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
@@ -3,5 +3,8 @@
import com.example.umc10th.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.Optional;
+
public interface MemberRepository extends JpaRepository {
-}
\ No newline at end of file
+ Optional findByEmail(String email);
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
index a049245f..e1645ddc 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
@@ -8,6 +8,7 @@
import com.example.umc10th.domain.member.exception.MemberException;
import com.example.umc10th.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@@ -15,6 +16,7 @@
public class MemberService {
private final MemberRepository memberRepository;
+ private final PasswordEncoder passwordEncoder;
public MemberResDTO.GetInfo getInfo(Long memberId) {
Member member = memberRepository.findById(memberId)
@@ -22,4 +24,17 @@ public MemberResDTO.GetInfo getInfo(Long memberId) {
return MemberConverter.toGetInfo(member);
}
+
+ public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) {
+ memberRepository.findByEmail(request.email())
+ .ifPresent(member -> {
+ throw new MemberException(MemberErrorCode.MEMBER_ALREADY_EXISTS);
+ });
+
+ String encodedPassword = passwordEncoder.encode(request.password());
+ Member member = MemberConverter.toMember(request, encodedPassword);
+ Member savedMember = memberRepository.save(member);
+
+ return MemberConverter.toSignUp(savedMember);
+ }
}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java
index bf61f50c..0413dbcb 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/code/MissionSuccessCode.java
@@ -9,8 +9,11 @@
@AllArgsConstructor
public enum MissionSuccessCode implements BaseSuccessCode {
- OK(HttpStatus.OK,
+ CREATED(HttpStatus.OK,
"MISSION200_1",
+ "성공적으로 미션을 생성했습니다."),
+ OK(HttpStatus.OK,
+ "MISSION200_2",
"성공적으로 미션을 조회했습니다.");
private final HttpStatus status;
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java
index 5c232fd2..5a4a4186 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java
@@ -1,15 +1,15 @@
package com.example.umc10th.domain.mission.controller;
import com.example.umc10th.domain.mission.code.MissionSuccessCode;
+import com.example.umc10th.domain.mission.dto.MissionReqDTO;
import com.example.umc10th.domain.mission.dto.MissionResDTO;
import com.example.umc10th.domain.mission.service.MissionService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
-import java.util.List;
-
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
@@ -17,11 +17,43 @@ public class MissionController {
private final MissionService missionService;
- @GetMapping("/v1/missions")
- public ApiResponse> getMissionList(
- @RequestParam Long storeId
+ // 가게 미션 생성(POST)
+ @PostMapping("/v1/stores/{storeId}/missions")
+ public ApiResponse createMission(
+ @PathVariable Long storeId,
+ @RequestBody @Valid MissionReqDTO.CreateMission dto
+ ) {
+ missionService.createMission(storeId, dto);
+
+ BaseSuccessCode code = MissionSuccessCode.CREATED;
+ return ApiResponse.onSuccess(code, null);
+ }
+
+ // 가게 내 미션 목록 조회 조회(GET)
+ @GetMapping("/v1/stores/{storeId}/missions")
+ public ApiResponse> getMissionList(
+ @PathVariable Long storeId,
+ @RequestParam Integer pageSize,
+ @RequestParam Integer pageNumber,
+ @RequestParam(required = false) String sort
) {
BaseSuccessCode code = MissionSuccessCode.OK;
- return ApiResponse.onSuccess(code, missionService.getMissionList(storeId));
+ return ApiResponse.onSuccess(code, missionService.getMissionList(storeId, pageSize, pageNumber, sort));
+ }
+
+ // 내가 진행 중인 미션 조회
+ @GetMapping("/v1/users/{memberId}/missions/ongoing")
+ public ApiResponse> getMyOngoingMissions(
+ @PathVariable Long memberId,
+ @RequestParam Integer pageSize,
+ @RequestParam Integer pageNumber,
+ @RequestParam(required = false) String sort
+ ) {
+ BaseSuccessCode code = MissionSuccessCode.OK;
+
+ return ApiResponse.onSuccess(
+ code,
+ missionService.getMyOngoingMissions(memberId, pageSize, pageNumber, sort)
+ );
}
-}
\ No newline at end of file
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java
index 60da9f6f..1140acd9 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java
@@ -1,17 +1,68 @@
package com.example.umc10th.domain.mission.converter;
+import com.example.umc10th.domain.mission.dto.MissionReqDTO;
import com.example.umc10th.domain.mission.dto.MissionResDTO;
import com.example.umc10th.domain.mission.entity.Mission;
+import com.example.umc10th.domain.mission.entity.mapping.MemberMission;
+import com.example.umc10th.domain.store.entity.Store;
+
+import java.util.List;
public class MissionConverter {
- public static MissionResDTO.MissionInfo toMissionInfo(Mission mission) {
- return MissionResDTO.MissionInfo.builder()
+ // 가게 미션 생성
+ public static Mission toMission(
+ Store store,
+ MissionReqDTO.CreateMission dto
+ ) {
+ return Mission.builder()
+ .store(store)
+ .title(dto.title())
+ .reward(dto.reward())
+ .deadline(dto.deadline())
+ .build();
+ }
+
+ // 가격 내 미션 조회
+ public static MissionResDTO.GetMission toGetMission(
+ Mission mission
+ ){
+ return MissionResDTO.GetMission.builder()
.id(mission.getId())
.storeId(mission.getStore().getId())
.title(mission.getTitle())
+ .reward(mission.getReward())
+ .deadline(mission.getDeadline())
+ .build();
+ }
+
+ // 내가 진행 중인 미션 조회
+ public static MissionResDTO.MyOngoingMission toMyOngoingMission(
+ MemberMission memberMission
+ ) {
+ Mission mission = memberMission.getMission();
+
+ return MissionResDTO.MyOngoingMission.builder()
+ .memberMissionId(memberMission.getId())
+ .missionId(mission.getId())
+ .storeId(mission.getStore().getId())
+ .title(mission.getTitle())
.deadline(mission.getDeadline())
.reward(mission.getReward())
+ .status(memberMission.getStatus())
+ .build();
+ }
+
+ // 페이지네이션 툴 생성
+ public static MissionResDTO.Pagination toPagination(
+ List data,
+ Integer pageNumber,
+ Integer pageSize
+ ){
+ return MissionResDTO.Pagination.builder()
+ .data(data)
+ .pageNumber(pageNumber)
+ .pageSize(pageSize)
.build();
}
-}
\ No newline at end of file
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java
index e33508b9..22959316 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java
@@ -1,4 +1,23 @@
package com.example.umc10th.domain.mission.dto;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.PositiveOrZero;
+
+import java.time.LocalDate;
+
public class MissionReqDTO {
-}
+
+ // 가게 미션 생성
+ public record CreateMission(
+ @NotBlank(message = "미션 제목은 필수입니다.")
+ String title,
+
+ @NotNull(message = "마감기한은 필수입니다.")
+ LocalDate deadline,
+
+ @NotNull(message = "보상 포인트는 필수입니다.")
+ @PositiveOrZero(message = "보상 포인트는 0 이상이어야 합니다.")
+ Integer reward
+ ) {}
+}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java
index 8d6ee79e..d81c1236 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java
@@ -1,18 +1,41 @@
package com.example.umc10th.domain.mission.dto;
+import com.example.umc10th.domain.mission.entity.MemberMissionStatus;
import lombok.Builder;
import java.time.LocalDate;
+import java.util.List;
public class MissionResDTO {
+ // 가게 내 미션 조회
@Builder
- public record MissionInfo(
+ public record GetMission(
Long id,
Long storeId,
String title,
LocalDate deadline,
Integer reward
- ) {
- }
+ ) {}
+
+ // 내가 진행 중인 미션 조회
+ @Builder
+ public record MyOngoingMission(
+ Long memberMissionId,
+ Long missionId,
+ Long storeId,
+ String title,
+ LocalDate deadline,
+ Integer reward,
+ MemberMissionStatus status
+ ) {}
+
+ // 페이지네이션 툴
+ @Builder
+ public record Pagination(
+ List data,
+ Integer pageNumber,
+ Integer pageSize
+ ){}
+
}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java
index 26230097..1b9103c2 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java
@@ -1,7 +1,21 @@
package com.example.umc10th.domain.mission.repository;
+import com.example.umc10th.domain.mission.entity.MemberMissionStatus;
import com.example.umc10th.domain.mission.entity.mapping.MemberMission;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
-public interface MemberMissionRepository extends JpaRepository {
-}
+public interface MemberMissionRepository extends JpaRepository {
+
+ @Query("SELECT mm FROM MemberMission mm " +
+ "WHERE mm.member.id = :memberId " +
+ "AND mm.status = :status")
+ Page findMyOngoingMissions(
+ @Param("memberId") Long memberId,
+ @Param("status") MemberMissionStatus status,
+ Pageable pageable
+ );
+}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java
index 5a5eceec..49df0c5d 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java
@@ -1,14 +1,14 @@
package com.example.umc10th.domain.mission.repository;
import com.example.umc10th.domain.mission.entity.Mission;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
-import java.util.List;
-
public interface MissionRepository extends JpaRepository {
@Query("SELECT m FROM Mission m WHERE m.store.id = :storeId")
- List findMissionByStoreId(@Param("storeId") Long storeId);
-}
\ No newline at end of file
+ Page findMissionByStoreId(@Param("storeId") Long storeId, Pageable pageable);
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java
index f2f1ea94..5f4e3e16 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java
@@ -1,22 +1,116 @@
package com.example.umc10th.domain.mission.service;
import com.example.umc10th.domain.mission.converter.MissionConverter;
+import com.example.umc10th.domain.mission.dto.MissionReqDTO;
import com.example.umc10th.domain.mission.dto.MissionResDTO;
+import com.example.umc10th.domain.mission.entity.MemberMissionStatus;
+import com.example.umc10th.domain.mission.entity.Mission;
+import com.example.umc10th.domain.mission.entity.mapping.MemberMission;
import com.example.umc10th.domain.mission.repository.MissionRepository;
+import com.example.umc10th.domain.store.entity.Store;
+import com.example.umc10th.domain.store.exception.StoreException;
+import com.example.umc10th.domain.store.exception.code.StoreErrorCode;
+import com.example.umc10th.domain.store.repository.StoreRepository;
+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.util.List;
+import com.example.umc10th.domain.mission.repository.MemberMissionRepository;
@Service
@RequiredArgsConstructor
public class MissionService {
private final MissionRepository missionRepository;
+ private final StoreRepository storeRepository;
+ private final MemberMissionRepository memberMissionRepository;
+
+ @Transactional
+ public void createMission(
+ Long storeId,
+ MissionReqDTO.CreateMission dto
+ ) {
+ Store store = storeRepository.findById(storeId)
+ .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND));
+
+ Mission mission = MissionConverter.toMission(store, dto);
+ missionRepository.save(mission);
+ }
+
+ @Transactional
+ public MissionResDTO.Pagination getMissionList(
+ Long storeId,
+ Integer pageNumber,
+ Integer pageSize,
+ String sort
+ ) {
+ storeRepository.findById(storeId)
+ .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND));
+
+ return getMissions(storeId, pageSize, pageNumber, sort);
+ }
+
+ // 내가 진행 중인 미션 조회
+ @Transactional
+ public MissionResDTO.Pagination getMyOngoingMissions(
+ Long memberId,
+ Integer pageSize,
+ Integer pageNumber,
+ String sort
+ ) {
+ Sort sortInfo;
+
+ if (sort != null) {
+ sortInfo = Sort.by(sort).descending();
+ } else {
+ sortInfo = Sort.by("id").descending();
+ }
+
+ PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo);
+
+ Page memberMissionPage =
+ memberMissionRepository.findMyOngoingMissions(
+ memberId,
+ MemberMissionStatus.ONGOING,
+ pageRequest
+ );
+
+ return MissionConverter.toPagination(
+ memberMissionPage.map(MissionConverter::toMyOngoingMission).toList(),
+ memberMissionPage.getNumber(),
+ memberMissionPage.getSize()
+ );
+ }
+
+ // 가게 내 미션들 조회
+ public MissionResDTO.Pagination getMissions(
+ Long storeId,
+ Integer pageSize,
+ Integer pageNumber,
+ String sort
+ ) {
+
+ // 정렬 정보 생성
+ Sort sortInfo;
+ if (sort != null) {
+ sortInfo = Sort.by(sort);
+ } else {
+ sortInfo = Sort.by("id").descending();
+ }
+
+ // 페이지 정보들을 PageRequest로 만들기
+ PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo);
+
+ // 가게 내 미션들 조회
+ Page missionList = missionRepository.findMissionByStoreId(storeId, pageRequest);
- public List getMissionList(Long storeId) {
- return missionRepository.findMissionByStoreId(storeId).stream()
- .map(MissionConverter::toMissionInfo)
- .toList();
+ // 미션들 응답 DTO로 포장하기
+ return MissionConverter.toPagination(
+ missionList.map(MissionConverter::toGetMission).toList(),
+ missionList.getNumber(),
+ missionList.getSize()
+ );
}
-}
\ No newline at end of file
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java
index dc48f9ff..62b34462 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java
@@ -17,9 +17,26 @@ public class ReviewController {
private final ReviewService reviewService;
+ // 리뷰 전체 조회 API
@GetMapping("/v1/reviews")
public ApiResponse> getReviewList() {
BaseSuccessCode code = ReviewSuccessCode.OK;
return ApiResponse.onSuccess(code, reviewService.getReviewList());
}
+
+ // 내가 작성한 리뷰 조회 API - 커서 기반 페이지네이션
+ @GetMapping("/v1/users/{memberId}/reviews")
+ public ApiResponse> getMyReviews(
+ @PathVariable Long memberId,
+ @RequestParam Integer pageSize,
+ @RequestParam String cursor,
+ @RequestParam String query
+ ) {
+ BaseSuccessCode code = ReviewSuccessCode.OK;
+
+ return ApiResponse.onSuccess(
+ code,
+ reviewService.getMyReviews(memberId, pageSize, cursor, query)
+ );
+ }
}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java
index fe2f34ac..a3a6b1fa 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java
@@ -1,8 +1,11 @@
package com.example.umc10th.domain.review.converter;
+import com.example.umc10th.domain.mission.dto.MissionResDTO;
import com.example.umc10th.domain.review.dto.ReviewResDTO;
import com.example.umc10th.domain.review.entity.Review;
+import java.util.List;
+
public class ReviewConverter {
public static ReviewResDTO.ReviewInfo toReviewInfo(Review review) {
@@ -15,4 +18,29 @@ public static ReviewResDTO.ReviewInfo toReviewInfo(Review review) {
.score(review.getScore())
.build();
}
+
+ public static ReviewResDTO.MyReview toMyReview(Review review) {
+ return ReviewResDTO.MyReview.builder()
+ .reviewId(review.getId())
+ .userId(review.getMember().getId())
+ .storeId(review.getStore().getId())
+ .userMissionId(review.getMemberMission().getId())
+ .content(review.getContent())
+ .score(review.getScore())
+ .build();
+ }
+
+ public static ReviewResDTO.CursorPagination toCursorPagination(
+ List data,
+ Boolean hasNext,
+ String nextCursor,
+ Integer pageSize
+ ) {
+ return ReviewResDTO.CursorPagination.builder()
+ .data(data)
+ .hasNext(hasNext)
+ .nextCursor(nextCursor)
+ .pageSize(pageSize)
+ .build();
+ }
}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java
index 8d5a18db..4a52b829 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java
@@ -2,6 +2,8 @@
import lombok.Builder;
+import java.util.List;
+
public class ReviewResDTO {
@Builder
@@ -13,4 +15,24 @@ public record ReviewInfo(
String content,
Integer score
) {}
+
+ // 내가 작성한 리뷰 조회
+ @Builder
+ public record MyReview(
+ Long reviewId,
+ Long userId,
+ Long storeId,
+ Long userMissionId,
+ String content,
+ Integer score
+ ) {}
+
+ // 커서 기반 페이지네이션
+ @Builder
+ public record CursorPagination(
+ List data,
+ Boolean hasNext,
+ String nextCursor,
+ Integer pageSize
+ ) {}
}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java
index d754a107..3550f3f8 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java
@@ -1,7 +1,53 @@
package com.example.umc10th.domain.review.repository;
import com.example.umc10th.domain.review.entity.Review;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
public interface ReviewRepository extends JpaRepository {
-}
+
+ // ID 순 첫 조회
+ @Query("SELECT r FROM Review r " +
+ "WHERE r.member.id = :memberId " +
+ "ORDER BY r.id DESC")
+ Slice findMyReviewsByIdFirst(
+ @Param("memberId") Long memberId,
+ Pageable pageable
+ );
+
+ // ID 순 커서 조회
+ @Query("SELECT r FROM Review r " +
+ "WHERE r.member.id = :memberId " +
+ "AND r.id < :cursorId " +
+ "ORDER BY r.id DESC")
+ Slice findMyReviewsByIdCursor(
+ @Param("memberId") Long memberId,
+ @Param("cursorId") Long cursorId,
+ Pageable pageable
+ );
+
+ // 별점 순 첫 조회
+ @Query("SELECT r FROM Review r " +
+ "WHERE r.member.id = :memberId " +
+ "ORDER BY r.score DESC, r.id DESC")
+ Slice findMyReviewsByScoreFirst(
+ @Param("memberId") Long memberId,
+ Pageable pageable
+ );
+
+ // 별점 순 커서 조회
+ @Query("SELECT r FROM Review r " +
+ "WHERE r.member.id = :memberId " +
+ "AND (r.score < :cursorScore " +
+ "OR (r.score = :cursorScore AND r.id < :cursorId)) " +
+ "ORDER BY r.score DESC, r.id DESC")
+ Slice findMyReviewsByScoreCursor(
+ @Param("memberId") Long memberId,
+ @Param("cursorScore") Integer cursorScore,
+ @Param("cursorId") Long cursorId,
+ Pageable pageable
+ );
+}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java
index 7959b683..ec4d09a6 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java
@@ -2,8 +2,12 @@
import com.example.umc10th.domain.review.converter.ReviewConverter;
import com.example.umc10th.domain.review.dto.ReviewResDTO;
+import com.example.umc10th.domain.review.entity.Review;
import com.example.umc10th.domain.review.repository.ReviewRepository;
+import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -19,4 +23,101 @@ public List getReviewList() {
.map(ReviewConverter::toReviewInfo)
.toList();
}
+
+ // 가게 내 미션들 조회
+ @Transactional
+ public ReviewResDTO.CursorPagination getMyReviews(
+ Long memberId,
+ Integer pageSize,
+ String cursor,
+ String query
+ ) {
+ // 페이지 정보들을 PageRequest로 만들기
+ PageRequest pageRequest = PageRequest.of(0, pageSize);
+
+ Slice reviewList;
+ String nextCursor;
+
+ // 커서가 있는 경우
+ if (!cursor.equals("-1")) {
+
+ String[] cursorSplit = cursor.split(":");
+
+ switch (query.toLowerCase()) {
+
+ // ID 순 조회
+ case "id" -> {
+ Long idCursor = Long.parseLong(cursorSplit[0]);
+
+ reviewList = reviewRepository.findMyReviewsByIdCursor(
+ memberId,
+ idCursor,
+ pageRequest
+ );
+ }
+
+ // 별점 순 조회
+ case "score" -> {
+ Integer scoreCursor = Integer.parseInt(cursorSplit[0]);
+ Long idCursor = Long.parseLong(cursorSplit[1]);
+
+ // 실제 DB 조회
+ reviewList = reviewRepository.findMyReviewsByScoreCursor(
+ memberId,
+ scoreCursor,
+ idCursor,
+ pageRequest
+ );
+ }
+
+ // 예외 처리
+ default -> throw new IllegalArgumentException("query는 id 또는 score만 가능합니다.");
+ }
+
+ } else { // cursor = -1인 경우의 시작 (첫 조회)
+ // 커서 없이 첫 조회
+ switch (query.toLowerCase()) {
+
+ // ID 순 첫 조회
+ case "id" -> reviewList = reviewRepository.findMyReviewsByIdFirst(
+ memberId,
+ pageRequest
+ );
+
+ // 별점 순 첫 조회
+ case "score" -> reviewList = reviewRepository.findMyReviewsByScoreFirst(
+ memberId,
+ pageRequest
+ );
+
+ default -> throw new IllegalArgumentException("query는 id 또는 score만 가능합니다.");
+ }
+ }
+
+ // 다음 커서 계산
+ if (reviewList.getContent().isEmpty()) {
+ nextCursor = null; // 조회결과가 비어 있으면 다음 커서를 만들 수 없음
+ } else {
+ Review lastReview = reviewList.getContent()
+ .get(reviewList.getContent().size() - 1); // 마지막 데이터 꺼내오기
+
+
+ // ID 기준이면 다음 커서는 마지막 리뷰의 ID
+ if (query.equals("id")) {
+ nextCursor = String.valueOf(lastReview.getId());
+ } else { // 별점 기준이면 다음 커서는 score:id 형태
+ nextCursor = lastReview.getScore() + ":" + lastReview.getId();
+ }
+ }
+
+ // 리뷰 응답 DTO로 포장하기
+ return ReviewConverter.toCursorPagination(
+ reviewList.getContent().stream()
+ .map(ReviewConverter::toMyReview)
+ .toList(),
+ reviewList.hasNext(),
+ nextCursor,
+ reviewList.getSize()
+ );
+ }
}
\ No newline at end of file
diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java
index 335dbbb8..91e8b1ec 100644
--- a/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java
+++ b/Jinyong/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java
@@ -10,10 +10,10 @@
public enum StoreErrorCode implements BaseErrorCode {
STORE_NOT_FOUND(HttpStatus.NOT_FOUND,
- "STORE404",
+ "STORE404_1",
"해당 가게를 찾을 수 없습니다."),
REGION_NOT_FOUND(HttpStatus.NOT_FOUND,
- "REGION404",
+ "REGION404_1",
"해당 지역을 찾을 수 없습니다.");
private final HttpStatus httpStatus;
diff --git a/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java b/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java
index e8fbbd3e..fe6c6723 100644
--- a/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java
+++ b/Jinyong/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java
@@ -5,9 +5,13 @@
import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
import com.example.umc10th.global.apiPayload.exception.ProjectException;
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 {
@@ -21,6 +25,23 @@ public ResponseEntity> handleMemberException(
.body(ApiResponse.onFailure(errorCode, null));
}
+ // @Valid 검증 실패 예외 처리
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity>> handleMethodArgumentNotValidException(
+ MethodArgumentNotValidException e
+ ) {
+ Map errors = new HashMap<>();
+
+ e.getBindingResult().getFieldErrors().forEach(error ->
+ errors.put(error.getField(), error.getDefaultMessage())
+ );
+
+ BaseErrorCode code = GeneralErrorCode.BAD_REQUEST;
+
+ return ResponseEntity.status(code.getStatus())
+ .body(ApiResponse.onFailure(code, errors));
+ }
+
// 그 외의 정의되지 않은 모든 예외 처리
@ExceptionHandler(Exception.class)
public ResponseEntity> handleException(
diff --git a/Jinyong/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Jinyong/src/main/java/com/example/umc10th/global/config/SecurityConfig.java
new file mode 100644
index 00000000..9d8e60dc
--- /dev/null
+++ b/Jinyong/src/main/java/com/example/umc10th/global/config/SecurityConfig.java
@@ -0,0 +1,67 @@
+package com.example.umc10th.global.config;
+
+import com.example.umc10th.global.security.handler.CustomAccessDenied;
+import com.example.umc10th.global.security.handler.CustomEntryPoint;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@EnableWebSecurity
+@Configuration
+public class SecurityConfig {
+
+ private final String[] allowUris = {
+ // Swagger
+ "/swagger-ui/**",
+ "/swagger-resources/**",
+ "/v3/api-docs/**",
+
+ // 회원가입
+ "/api/v1/auth/signup"
+ };
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(requests -> requests
+ .requestMatchers(allowUris).permitAll()
+ .anyRequest().authenticated()
+ )
+ .formLogin(form -> form
+ .defaultSuccessUrl("/swagger-ui/index.html", true)
+ .permitAll()
+ )
+ .logout(logout -> logout
+ .logoutUrl("/logout")
+ .logoutSuccessUrl("/login?logout")
+ .permitAll()
+ )
+ .exceptionHandling(exception -> exception
+ .accessDeniedHandler(customAccessDenied())
+ .authenticationEntryPoint(customEntryPoint())
+ );
+
+ return http.build();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public CustomAccessDenied customAccessDenied() {
+ return new CustomAccessDenied();
+ }
+
+ @Bean
+ public CustomEntryPoint customEntryPoint() {
+ return new CustomEntryPoint();
+ }
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java
new file mode 100644
index 00000000..61a32f48
--- /dev/null
+++ b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java
@@ -0,0 +1,33 @@
+package com.example.umc10th.global.security.entity;
+
+import com.example.umc10th.domain.member.entity.Member;
+import jakarta.annotation.Nullable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import java.awt.*;
+import java.util.Collection;
+import java.util.List;
+
+@Getter
+@RequiredArgsConstructor
+public class AuthMember implements UserDetails {
+
+ private final Member member;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of();
+ }
+
+ @Override
+ public @Nullable String getPassword() {
+ return member.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+ return member.getEmail();
+ }
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java b/Jinyong/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java
new file mode 100644
index 00000000..28ec5e20
--- /dev/null
+++ b/Jinyong/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java
@@ -0,0 +1,32 @@
+package com.example.umc10th.global.security.handler;
+
+import com.example.umc10th.global.apiPayload.ApiResponse;
+import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
+import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+
+import java.io.IOException;
+
+public class CustomAccessDenied implements AccessDeniedHandler {
+
+ @Override
+ public void handle(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AccessDeniedException accessDeniedException
+ ) throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ BaseErrorCode code = GeneralErrorCode.FORBIDDEN;
+
+ // 인증은 되었지만 권한이 부족한 요청은 403 상태와 JSON 에러 응답으로 내려준다.
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(code.getStatus().value());
+
+ ApiResponse errorResponse = ApiResponse.onFailure(code, null);
+ objectMapper.writeValue(response.getOutputStream(), errorResponse);
+ }
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java b/Jinyong/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java
new file mode 100644
index 00000000..22fb1f27
--- /dev/null
+++ b/Jinyong/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java
@@ -0,0 +1,32 @@
+package com.example.umc10th.global.security.handler;
+
+import com.example.umc10th.global.apiPayload.ApiResponse;
+import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
+import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import java.io.IOException;
+
+public class CustomEntryPoint implements AuthenticationEntryPoint {
+
+ @Override
+ public void commence(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException authException
+ ) throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;
+
+ // 인증되지 않은 요청은 401 상태와 JSON 에러 응답으로 내려준다.
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(code.getStatus().value());
+
+ ApiResponse errorResponse = ApiResponse.onFailure(code, null);
+ objectMapper.writeValue(response.getOutputStream(), errorResponse);
+ }
+}
diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java
new file mode 100644
index 00000000..7789bfc1
--- /dev/null
+++ b/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java
@@ -0,0 +1,28 @@
+package com.example.umc10th.global.security.service;
+
+import com.example.umc10th.domain.member.entity.Member;
+import com.example.umc10th.domain.member.exception.MemberException;
+import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
+import com.example.umc10th.domain.member.repository.MemberRepository;
+import com.example.umc10th.global.security.entity.AuthMember;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final MemberRepository memberRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(
+ String username
+ ) throws UsernameNotFoundException {
+ Member member = memberRepository.findByEmail(username)
+ .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
+ return new AuthMember(member);
+ }
+}
\ No newline at end of file