Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion .github/workflows/CI-CD_Pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion backend/.env.default
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__KAKAO__CLIENT_SECRET=NEED_TO_SET
SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__NAVER__CLIENT_ID=NEED_TO_SET
SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__NAVER__CLIENT_SECRET=NEED_TO_SET

CUSTOM__JWT__SECRET_KEY=NEED_TO_SET
CUSTOM_JWT_SECRET_KEY=NEED_TO_SET
CUSTOM_JWT_ACCESS_TOKEN_EXPIRATION_SECONDS=NEED_TO_SET

PROD_DATASOURCE_URL=NEED_TO_SET
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ai.lawyer.domain.law.repository;

import com.ai.lawyer.domain.law.entity.Hang;
import com.ai.lawyer.domain.law.entity.Jo;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand All @@ -13,4 +14,6 @@ public interface HangRepository extends JpaRepository<Hang, Long> {
// Hang + Ho만 페치
@EntityGraph(attributePaths = "hoList")
List<Hang> findByJoId(Long joId);

List<Hang> findByJo(Jo jo);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.ai.lawyer.domain.law.repository;

import com.ai.lawyer.domain.law.entity.Hang;
import com.ai.lawyer.domain.law.entity.Ho;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface HoRepository extends JpaRepository<Ho, Long> {
List<Ho> findByHang(Hang hang);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ai.lawyer.domain.law.repository;

import com.ai.lawyer.domain.law.entity.Jang;
import com.ai.lawyer.domain.law.entity.Law;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand All @@ -12,4 +13,6 @@ public interface JangRepository extends JpaRepository<Jang, Long> {
// Jang + Jo만 페치
@EntityGraph(attributePaths = "joList")
List<Jang> findByLawId(Long lawId);

List<Jang> findByLaw(Law law);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ai.lawyer.domain.law.repository;

import com.ai.lawyer.domain.law.entity.Jang;
import com.ai.lawyer.domain.law.entity.Jo;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -13,4 +14,6 @@ public interface JoRepository extends JpaRepository<Jo, Long> {
// Jo + Hang만 페치
@EntityGraph(attributePaths = "hangList")
List<Jo> findByJangId(Long jangId);

List<Jo> findByJang(Jang jang);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.ai.lawyer.domain.member.dto.MemberLoginRequest;
import com.ai.lawyer.domain.member.dto.MemberResponse;
import com.ai.lawyer.domain.member.dto.MemberSignupRequest;
import com.ai.lawyer.domain.member.entity.Member;
import com.ai.lawyer.domain.member.service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand Down Expand Up @@ -37,14 +36,9 @@ public class MemberController {
public ResponseEntity<MemberResponse> signup(@Valid @RequestBody MemberSignupRequest request) {
log.info("회원가입 요청: email={}, name={}", request.getLoginId(), request.getName());

try {
MemberResponse response = memberService.signup(request);
log.info("회원가입 성공: memberId={}", response.getMemberId());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (IllegalArgumentException e) {
log.warn("회원가입 실패: {}", e.getMessage());
return ResponseEntity.badRequest().build();
}
MemberResponse response = memberService.signup(request);
log.info("회원가입 성공: memberId={}", response.getMemberId());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@PostMapping("/login")
Expand All @@ -57,14 +51,9 @@ public ResponseEntity<MemberResponse> login(@Valid @RequestBody MemberLoginReque
HttpServletResponse response) {
log.info("로그인 요청: email={}", request.getLoginId());

try {
MemberResponse memberResponse = memberService.login(request, response);
log.info("로그인 성공: memberId={}", memberResponse.getMemberId());
return ResponseEntity.ok(memberResponse);
} catch (IllegalArgumentException e) {
log.warn("로그인 실패: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
MemberResponse memberResponse = memberService.login(request, response);
log.info("로그인 성공: memberId={}", memberResponse.getMemberId());
return ResponseEntity.ok(memberResponse);
}

@PostMapping("/logout")
Expand All @@ -78,9 +67,9 @@ public ResponseEntity<Void> logout(Authentication authentication, HttpServletRes
if (authentication != null && authentication.getName() != null) {
String loginId = authentication.getName();
memberService.logout(loginId, response);
log.info("로그아웃 완료: email={}", loginId);
log.info("로그아웃 완료: memberId={}", loginId);
} else {
// 인증 정보가 없어도 쿠키는 클리어
// 인증되지 않은 상태에서도 클라이언트 쿠키 클리어 처리
memberService.logout("", response);
log.info("인증 정보 없이 로그아웃 완료");
}
Expand All @@ -98,22 +87,16 @@ public ResponseEntity<MemberResponse> refreshToken(HttpServletRequest request,
HttpServletResponse response) {
log.info("토큰 재발급 요청");

// 쿠키에서 리프레시 토큰 추출 (간단한 방법)
// HTTP 쿠키에서 리프레시 토큰 추출
String refreshToken = extractRefreshTokenFromCookies(request);

if (refreshToken == null) {
log.warn("리프레시 토큰이 없음");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
throw new com.ai.lawyer.domain.member.exception.MemberAuthenticationException("리프레시 토큰이 없습니다.");
}

try {
MemberResponse memberResponse = memberService.refreshToken(refreshToken, response);
log.info("토큰 재발급 성공: memberId={}", memberResponse.getMemberId());
return ResponseEntity.ok(memberResponse);
} catch (IllegalArgumentException e) {
log.warn("토큰 재발급 실패: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
MemberResponse memberResponse = memberService.refreshToken(refreshToken, response);
log.info("토큰 재발급 성공: memberId={}", memberResponse.getMemberId());
return ResponseEntity.ok(memberResponse);
}

@DeleteMapping("/withdraw")
Expand All @@ -124,25 +107,18 @@ public ResponseEntity<MemberResponse> refreshToken(HttpServletRequest request,
@ApiResponse(responseCode = "404", description = "존재하지 않는 회원")
})
public ResponseEntity<Void> withdraw(Authentication authentication, HttpServletResponse response) {
if (authentication == null || authentication.getName() == null) {
log.warn("인증되지 않은 회원탈퇴 요청");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
if (authentication == null || authentication.getPrincipal() == null) {
throw new com.ai.lawyer.domain.member.exception.MemberAuthenticationException("인증이 필요합니다.");
}

String loginId = authentication.getName();
log.info("회원탈퇴 요청: email={}", loginId);

try {
// loginId로 Member를 조회하여 실제 memberId 사용
Member member = memberService.findByLoginId(loginId);
memberService.withdraw(member.getMemberId());
memberService.logout(loginId, response); // 탈퇴 후 로그아웃 처리
log.info("회원탈퇴 성공: email={}, memberId={}", loginId, member.getMemberId());
return ResponseEntity.ok().build();
} catch (IllegalArgumentException e) {
log.warn("회원탈퇴 실패: {}", e.getMessage());
return ResponseEntity.notFound().build();
}
Long memberId = (Long) authentication.getPrincipal();
String loginId = (String) authentication.getDetails();
log.info("회원탈퇴 요청: memberId={}, email={}", memberId, loginId);

memberService.withdraw(memberId);
memberService.logout(loginId, response); // 회원 탈퇴 후 세션 및 토큰 정리
log.info("회원탈퇴 성공: memberId={}, email={}", memberId, loginId);
return ResponseEntity.ok().build();
}

@GetMapping("/me")
Expand All @@ -152,26 +128,23 @@ public ResponseEntity<Void> withdraw(Authentication authentication, HttpServletR
@ApiResponse(responseCode = "401", description = "인증되지 않은 사용자")
})
public ResponseEntity<MemberResponse> getMyInfo(Authentication authentication) {
if (authentication == null || authentication.getName() == null) {
log.warn("인증되지 않은 정보 조회 요청");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
if (authentication == null || authentication.getPrincipal() == null) {
throw new com.ai.lawyer.domain.member.exception.MemberAuthenticationException("인증이 필요합니다.");
}

String loginId = authentication.getName();
log.info("내 정보 조회 요청: email={}", loginId);

try {
// loginId로 Member를 조회하여 실제 memberId 사용
Member member = memberService.findByLoginId(loginId);
MemberResponse response = memberService.getMemberById(member.getMemberId());
log.info("내 정보 조회 성공: memberId={}", response.getMemberId());
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
log.warn("내 정보 조회 실패: {}", e.getMessage());
return ResponseEntity.notFound().build();
}
Long memberId = (Long) authentication.getPrincipal();
log.info("내 정보 조회 요청: memberId={}", memberId);

MemberResponse response = memberService.getMemberById(memberId);
log.info("내 정보 조회 성공: memberId={}", response.getMemberId());
return ResponseEntity.ok(response);
}

/**
* HTTP 쿠키에서 리프레시 토큰을 추출합니다.
* @param request HTTP 요청 객체
* @return 리프레시 토큰 값 또는 null
*/
private String extractRefreshTokenFromCookies(HttpServletRequest request) {
if (request.getCookies() != null) {
for (jakarta.servlet.http.Cookie cookie : request.getCookies()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ai.lawyer.domain.member.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@AllArgsConstructor
public class MemberErrorResponse {
private final String message;
private final int status;
private final String error;
private final LocalDateTime timestamp;

public static MemberErrorResponse of(String message, int status, String error) {
return new MemberErrorResponse(message, status, error, LocalDateTime.now());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ai.lawyer.domain.member.exception;

public class MemberAuthenticationException extends RuntimeException {
public MemberAuthenticationException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.ai.lawyer.domain.member.exception;

import com.ai.lawyer.domain.member.dto.MemberErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
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;

@RestControllerAdvice(basePackages = "com.ai.lawyer.domain.member")
@Slf4j
public class MemberExceptionHandler {

/**
* IllegalArgumentException 고도화 처리
* 메시지에 따라 HTTP 상태코드와 에러 메시지 다르게 반환
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<MemberErrorResponse> handleMemberIllegalArgumentException(IllegalArgumentException e) {
log.warn("Member 도메인 IllegalArgumentException: {}", e.getMessage());

String msg = e.getMessage();
HttpStatus status;
String error = switch (msg) {
case "이미 존재하는 이메일입니다.", "잘못된 입력입니다." -> {
status = HttpStatus.BAD_REQUEST;
yield "잘못된 요청";
}
case "존재하지 않는 회원입니다.", "비밀번호가 일치하지 않습니다." -> {
status = HttpStatus.UNAUTHORIZED;
yield "인증 실패";
}
default -> {
status = HttpStatus.BAD_REQUEST;
yield "오류 발생";
}
};

// 메시지 기반으로 상태코드 결정

MemberErrorResponse errorResponse = MemberErrorResponse.of(msg, status.value(), error);
return ResponseEntity.status(status).body(errorResponse);
}

/**
* 인증 관련 예외 처리
*/
@ExceptionHandler(MemberAuthenticationException.class)
public ResponseEntity<MemberErrorResponse> handleMemberAuthenticationException(MemberAuthenticationException e) {
log.warn("Member 도메인 AuthenticationException: {}", e.getMessage());
MemberErrorResponse errorResponse = MemberErrorResponse.of(
e.getMessage(),
HttpStatus.UNAUTHORIZED.value(),
"인증 실패"
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}

/**
* 유효성 검증 실패 처리
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<MemberErrorResponse> handleMemberValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getAllErrors().getFirst().getDefaultMessage();
log.warn("Member 도메인 유효성 검증 실패: {}", message);
MemberErrorResponse errorResponse = MemberErrorResponse.of(
message,
HttpStatus.BAD_REQUEST.value(),
"유효성 검증 실패"
);
return ResponseEntity.badRequest().body(errorResponse);
}
}
Loading
Loading