Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
97cfb2d
refactor(member): AdminMemberController의 멤버 조회 예외 처리를 서비스로 위임(step1 수…
ianlee2 Mar 5, 2026
ad2ecce
refactor(common): BaseException 기반 글로벌 예외 처리 도입
ianlee2 Mar 5, 2026
f68f506
refactor(auth,member): 도메인 예외 표준화 및 핸들러 이관
ianlee2 Mar 5, 2026
38cbdfa
refactor(product): product 도메인 예외 표준화
ianlee2 Mar 5, 2026
4e1c51b
refactor(category): category 도메인 예외 표준화
ianlee2 Mar 5, 2026
34a0bea
refactor(order, option): order, option 도메인 예외 표준화
ianlee2 Mar 5, 2026
cea6ca1
refactor(wish): wish 도메인 예외 표준화
ianlee2 Mar 5, 2026
76618a8
refactor(entities): 엔티티에 Lombok 적용
ianlee2 Mar 5, 2026
465a5d2
refactor(lombok): controller, service에 Lombok 적용
ianlee2 Mar 5, 2026
a1b54f0
refactor(service): transaction 적용
ianlee2 Mar 5, 2026
f39f2da
test(auth): KakaoAuthService/AuthenticationResolver 단위 테스트 추가
ianlee2 Mar 5, 2026
506bb5b
test(member): Member/MemberService/AdminMemberService 단위 테스트 추가
ianlee2 Mar 5, 2026
9e30582
test(product): Product/ProductService 단위 테스트 추가
ianlee2 Mar 5, 2026
af9dda7
test(category): Category/CategoryService 단위 테스트 추가
ianlee2 Mar 5, 2026
d62a74e
test(option): Option/OptionService 단위 테스트 추가
ianlee2 Mar 5, 2026
bc85cd6
test(order): OrderService 단위 테스트 추가
ianlee2 Mar 5, 2026
ac04b1e
test(wish): WishService 단위 테스트 추가
ianlee2 Mar 5, 2026
4f91205
docs(test): 전체 도메인 단위 테스트 작성 내용 programming-log-step2에 정리
ianlee2 Mar 5, 2026
d27586c
refactor(product): AdminProductController에서 AdminProductService 분리 및 …
ianlee2 Mar 5, 2026
9327de7
refactor(kakao): Kakao 클라이언트를 OAuth/Message 인터페이스 어댑터로 분리
ianlee2 Mar 5, 2026
6d2c920
refactor(all): 디렉토리 패키지 분리
ianlee2 Mar 5, 2026
3998ae6
feat(order,wish): 미구현 동작(구매 후 위시 제거) 완성, 검증 코드 반영
ianlee2 Mar 5, 2026
b20a4e3
refactor(wish): 소유권 검증을 Wish 도메인 메서드로 이동
ianlee2 Mar 5, 2026
0679672
refactor(product): AdminProductController 폼 에러 모델 세팅 공통화
ianlee2 Mar 5, 2026
7feda3e
refactor(option): 삭제 도메인 검증을 Option 엔티티로 이동
ianlee2 Mar 5, 2026
144e828
refactor(service): 특정 패키지의 service에서 타 패키지 Repository 의존을 해당 Service …
ianlee2 Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ dependencies {
runtimeOnly("io.jsonwebtoken:jjwt-jackson")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("io.rest-assured:rest-assured")
testCompileOnly("org.projectlombok:lombok")
testAnnotationProcessor("org.projectlombok:lombok")
}

kotlin {
Expand Down
282 changes: 282 additions & 0 deletions programming-log-step2.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions programming-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ private void validateName(String name) {
`ProductNameValidator`에 `validateOrThrow(String name)` 메서드를 추가하고 Controller의 private `validateName()`을 제거했다. `OptionController`는 DTO가 검증을 담당하므로 `validateName()` 제거 후 별도 호출 없음.

```
gift.product.ProductNameValidator
gift.product.validation.ProductNameValidator
└── validateOrThrow(name) ← throw 로직 포함, Controller private 메서드 대체

gift.option.OptionController
gift.option.controller.OptionController
└── validateName() 제거 ← OptionRequest @Valid가 대체
```

Expand Down
7 changes: 0 additions & 7 deletions src/main/java/gift/auth/AuthenticationException.java

This file was deleted.

7 changes: 0 additions & 7 deletions src/main/java/gift/auth/ForbiddenException.java

This file was deleted.

37 changes: 0 additions & 37 deletions src/main/java/gift/auth/KakaoAuthController.java

This file was deleted.

54 changes: 0 additions & 54 deletions src/main/java/gift/auth/KakaoAuthService.java

This file was deleted.

47 changes: 47 additions & 0 deletions src/main/java/gift/auth/controller/OAuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package gift.auth.controller;

import gift.auth.exception.InvalidOAuthProviderException;
import gift.auth.service.OAuthService;
import gift.auth.dto.TokenResponse;
import gift.external.ExternalProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/api/auth")
@RequiredArgsConstructor
public class OAuthController {
private final OAuthService oAuthService;

@GetMapping(path = "/{provider}/login")
public ResponseEntity<Void> login(@PathVariable String provider) {
ExternalProvider externalProvider = parseProvider(provider);
return ResponseEntity.status(HttpStatus.FOUND)
.header(HttpHeaders.LOCATION, oAuthService.getLoginUri(externalProvider).toString())
.build();
}

@GetMapping(path = "/{provider}/callback")
public ResponseEntity<TokenResponse> callback(
@PathVariable String provider,
@RequestParam("code") String code
) {
ExternalProvider externalProvider = parseProvider(provider);
return ResponseEntity.ok(oAuthService.handleCallback(externalProvider, code));
}

private ExternalProvider parseProvider(String provider) {
try {
return ExternalProvider.valueOf(provider.toUpperCase());
} catch (IllegalArgumentException e) {
throw new InvalidOAuthProviderException();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package gift.auth;
package gift.auth.dto;

/**
* Response containing a JWT access token.
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/gift/auth/exception/AuthErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gift.auth.exception;

import gift.common.BaseErrorCode;
import org.springframework.http.HttpStatus;

public enum AuthErrorCode implements BaseErrorCode {
AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "인증에 실패했습니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."),
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "지원하지 않는 OAuth Provider입니다.");

private final HttpStatus httpStatus;
private final String message;

AuthErrorCode(HttpStatus httpStatus, String message) {
this.httpStatus = httpStatus;
this.message = message;
}

@Override
public HttpStatus getHttpStatus() {
return httpStatus;
}

@Override
public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gift.auth.exception;

import gift.common.BaseException;

public class AuthenticationException extends BaseException {
public AuthenticationException() {
super(AuthErrorCode.AUTHENTICATION_FAILED);
}
}
9 changes: 9 additions & 0 deletions src/main/java/gift/auth/exception/ForbiddenException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gift.auth.exception;

import gift.common.BaseException;

public class ForbiddenException extends BaseException {
public ForbiddenException() {
super(AuthErrorCode.ACCESS_DENIED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gift.auth.exception;

import gift.common.BaseException;

public class InvalidOAuthProviderException extends BaseException {
public InvalidOAuthProviderException() {
super(AuthErrorCode.INVALID_OAUTH_PROVIDER);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package gift.auth;
package gift.auth.jwt;

import gift.member.Member;
import gift.member.MemberRepository;
import gift.member.entity.Member;
import gift.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;

import org.springframework.stereotype.Component;

Expand All @@ -12,15 +13,11 @@
* @since 1.0
*/
@Component
Comment on lines 13 to 15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

structural-change-plan.md에서 "Javadoc 삭제 — 나머지 엔티티에도 없으므로 통일한다"고 명시했는데 여기에는 Javadoc이 남아 있네요?

@RequiredArgsConstructor
public class AuthenticationResolver {
private final JwtProvider jwtProvider;
private final MemberRepository memberRepository;

public AuthenticationResolver(JwtProvider jwtProvider, MemberRepository memberRepository) {
this.jwtProvider = jwtProvider;
this.memberRepository = memberRepository;
}

public Member extractMember(String authorization) {
try {
String token = authorization.replace("Bearer ", "");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package gift.auth;
package gift.auth.jwt;

import org.springframework.boot.context.properties.ConfigurationProperties;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package gift.auth;
package gift.auth.jwt;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/gift/auth/oauth/OAuthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gift.auth.oauth;

import gift.external.ExternalProvider;

import java.net.URI;

public interface OAuthClient {
ExternalProvider provider();

URI getLoginUri();

OAuthUserInfo getUserInfo(String code);
}
27 changes: 27 additions & 0 deletions src/main/java/gift/auth/oauth/OAuthClientRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package gift.auth.oauth;

import gift.external.ExternalProvider;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
public class OAuthClientRegistry {
private final Map<ExternalProvider, OAuthClient> clients;

public OAuthClientRegistry(List<OAuthClient> clients) {
this.clients = clients.stream()
.collect(Collectors.toMap(OAuthClient::provider, Function.identity()));
}

public OAuthClient get(ExternalProvider provider) {
OAuthClient client = clients.get(provider);
if (client == null) {
throw new IllegalStateException("OAuth client not found. provider=" + provider);
}
return client;
}
}
4 changes: 4 additions & 0 deletions src/main/java/gift/auth/oauth/OAuthUserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package gift.auth.oauth;

public record OAuthUserInfo(String email, String accessToken) {
}
46 changes: 46 additions & 0 deletions src/main/java/gift/auth/service/OAuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package gift.auth.service;

import gift.auth.dto.TokenResponse;
import gift.auth.jwt.JwtProvider;
import gift.auth.oauth.OAuthClient;
import gift.auth.oauth.OAuthClientRegistry;
import gift.auth.oauth.OAuthUserInfo;
import gift.external.ExternalProvider;
import gift.member.entity.Member;
import gift.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.URI;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OAuthService {
private final OAuthClientRegistry oAuthClientRegistry;
private final MemberRepository memberRepository;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

144e828 커밋에서 서비스 간 교차 Repository 의존을 제거했는데
OAuthService에서는 MemberRepository를 직접 사용하고 있네요?
OAuthService만 MemberRepository를 직접 사용하는 이유가 있을까요?
의도적인 예외인지 누락인지 궁금합니다.

private final JwtProvider jwtProvider;

public URI getLoginUri(ExternalProvider provider) {
OAuthClient client = oAuthClientRegistry.get(provider);
return client.getLoginUri();
}

@Transactional
public TokenResponse handleCallback(ExternalProvider provider, String code) {
OAuthClient client = oAuthClientRegistry.get(provider);
OAuthUserInfo userInfo = client.getUserInfo(code);
String email = userInfo.email();

Member member = memberRepository.findByEmail(email)
.orElseGet(() -> new Member(email));
if (provider == ExternalProvider.KAKAO) {
member.updateKakaoAccessToken(userInfo.accessToken());
}
memberRepository.save(member);

String token = jwtProvider.createToken(member.getEmail());
return new TokenResponse(token);
}
}
Loading