From d43bd5c245eabf035a02ac8b53aecacfffa9efaf Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Fri, 18 Jul 2025 13:14:27 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refacotr=20:=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/Centralthon/domain/member/entity/Member.java | 8 ++++---- .../domain/member/service/MemberServiceImpl.java | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/Centralthon/domain/member/entity/Member.java b/src/main/java/com/example/Centralthon/domain/member/entity/Member.java index ef80f01..7e82abc 100644 --- a/src/main/java/com/example/Centralthon/domain/member/entity/Member.java +++ b/src/main/java/com/example/Centralthon/domain/member/entity/Member.java @@ -31,12 +31,12 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private MemberRole role; - public static Member toEntity(SignUpReq signUpReq, PasswordEncoder passwordEncoder) { + public static Member toEntity(String email, String encoded, String nickName) { return Member.builder() - .email(signUpReq.getEmail()) - .password(passwordEncoder.encode(signUpReq.getPassword())) - .nickName(signUpReq.getNickName()) + .email(email) + .password(encoded) + .nickName(nickName) .role(MemberRole.USER) .build(); } diff --git a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java b/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java index 73f65ea..09b2a10 100644 --- a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java @@ -26,8 +26,9 @@ public void signUp(SignUpReq signUpReq) { if (memberRepository .findByEmailOrNickName(signUpReq.getEmail(), signUpReq.getNickName()) .isPresent()) { throw new MemberAlreadyExistException(); } - Member newMember = Member.toEntity(signUpReq, passwordEncoder); + String encoded = passwordEncoder.encode(signUpReq.getPassword()); + Member member = Member.toEntity(signUpReq.getEmail(),encoded,signUpReq.getNickName()); - memberRepository.save(newMember); + memberRepository.save(member); } } From 6d6e3a6a15637a72de4b5f8f0326b6855c076d1c Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Sun, 20 Jul 2025 14:07:35 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8=20=20feat=20:=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/entity/enums/MemberRole.java | 3 + .../InvalidCredentialsException.java | 7 ++ .../member/exception/MemberErrorCode.java | 4 +- .../exception/MemberNotFoundException.java | 7 ++ .../member/repository/MemberRepository.java | 2 + .../domain/member/service/MemberService.java | 4 + .../member/service/MemberServiceImpl.java | 23 +++++ .../web/controller/MemberController.java | 27 +++++- .../domain/member/web/dto/LoginReq.java | 15 ++++ .../domain/member/web/dto/LoginRes.java | 6 ++ .../domain/member/web/dto/ProfileRes.java | 7 ++ .../global/config/SecurityConfig.java | 16 +++- .../exception/CustomAccessDeniedHandler.java | 35 ++++++++ .../global/jwt/JwtTokenFilter.java | 46 ++++++++++ .../global/jwt/JwtTokenProvider.java | 87 +++++++++++++++++++ .../global/security/CustomMemberDetails.java | 34 ++++++++ src/main/resources/application.properties | 4 + 17 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java create mode 100644 src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java create mode 100644 src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java create mode 100644 src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java create mode 100644 src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java create mode 100644 src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java create mode 100644 src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java create mode 100644 src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java create mode 100644 src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java diff --git a/src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java b/src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java index 1d29f02..206b1eb 100644 --- a/src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java +++ b/src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java @@ -12,4 +12,7 @@ public enum MemberRole { @Getter private final String memberRole; + public String getStringRole() { + return memberRole; + } } diff --git a/src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java b/src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java new file mode 100644 index 0000000..3d06aea --- /dev/null +++ b/src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java @@ -0,0 +1,7 @@ +package com.example.Centralthon.domain.member.exception; + +import com.example.Centralthon.global.exception.BaseException; + +public class InvalidCredentialsException extends BaseException { + public InvalidCredentialsException( ) {super(MemberErrorCode.INVALID_CREDENTIALS);} +} diff --git a/src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java b/src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java index d35dd86..b83c403 100644 --- a/src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java +++ b/src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java @@ -7,7 +7,9 @@ @Getter @AllArgsConstructor public enum MemberErrorCode implements BaseResponseCode { - MEMBER_ALREADY_EXIST("MEMBER_409_1", 409, "이미 존재하는 사용자입니다."); + MEMBER_ALREADY_EXIST("MEMBER_409_1", 409, "이미 존재하는 사용자입니다."), + INVALID_CREDENTIALS("MEMBER_401_1", 401, "아이디 또는 비밀번호를 다시 확인해주세요"), + MEMBER_NOT_FOUND("MEMBER_404_1",404,"존재하지 않는 사용자입니다."); private final String code; private final int httpStatus; diff --git a/src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java b/src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java new file mode 100644 index 0000000..4fcdac3 --- /dev/null +++ b/src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.Centralthon.domain.member.exception; + +import com.example.Centralthon.global.exception.BaseException; + +public class MemberNotFoundException extends BaseException { + public MemberNotFoundException() {super(MemberErrorCode.MEMBER_NOT_FOUND);} +} diff --git a/src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java b/src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java index e408aa4..5872243 100644 --- a/src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -13,4 +14,5 @@ public interface MemberRepository extends JpaRepository { Optional findByEmailOrNickName( String email, String nickName); + Optional findByEmail(String email); } diff --git a/src/main/java/com/example/Centralthon/domain/member/service/MemberService.java b/src/main/java/com/example/Centralthon/domain/member/service/MemberService.java index fe46b48..b57da9a 100644 --- a/src/main/java/com/example/Centralthon/domain/member/service/MemberService.java +++ b/src/main/java/com/example/Centralthon/domain/member/service/MemberService.java @@ -1,5 +1,7 @@ package com.example.Centralthon.domain.member.service; +import com.example.Centralthon.domain.member.web.dto.LoginReq; +import com.example.Centralthon.domain.member.web.dto.LoginRes; import com.example.Centralthon.domain.member.web.dto.SignUpReq; import com.example.Centralthon.global.response.SuccessResponse; import jakarta.validation.Valid; @@ -8,4 +10,6 @@ public interface MemberService { void signUp( SignUpReq signupReq); + + LoginRes login( LoginReq loginReq); } diff --git a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java b/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java index 09b2a10..ba20994 100644 --- a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java @@ -2,10 +2,14 @@ import com.example.Centralthon.domain.member.entity.Member; import com.example.Centralthon.domain.member.entity.enums.MemberRole; +import com.example.Centralthon.domain.member.exception.InvalidCredentialsException; import com.example.Centralthon.domain.member.exception.MemberAlreadyExistException; import com.example.Centralthon.domain.member.exception.MemberErrorCode; import com.example.Centralthon.domain.member.repository.MemberRepository; +import com.example.Centralthon.domain.member.web.dto.LoginReq; +import com.example.Centralthon.domain.member.web.dto.LoginRes; import com.example.Centralthon.domain.member.web.dto.SignUpReq; +import com.example.Centralthon.global.jwt.JwtTokenProvider; import com.example.Centralthon.global.response.SuccessResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -18,6 +22,7 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; @Override @@ -31,4 +36,22 @@ public void signUp(SignUpReq signUpReq) { memberRepository.save(member); } + + @Override + public LoginRes login(LoginReq loginReq) { + // 아이디 확인 + Member member = memberRepository.findByEmail(loginReq.getEmail()) + .orElseThrow(InvalidCredentialsException::new); + + // 비밀번호 검증 + if (!passwordEncoder.matches(loginReq.getPassword(), member.getPassword())) { + throw new InvalidCredentialsException(); + } + + // 토큰 생성 + String token = jwtTokenProvider.createToken(member); + + // 반환 + return new LoginRes(token); + } } diff --git a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java b/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java index 3703ed8..0761e87 100644 --- a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java +++ b/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java @@ -1,16 +1,19 @@ package com.example.Centralthon.domain.member.web.controller; +import com.example.Centralthon.domain.member.entity.Member; import com.example.Centralthon.domain.member.service.MemberService; +import com.example.Centralthon.domain.member.web.dto.LoginReq; +import com.example.Centralthon.domain.member.web.dto.LoginRes; +import com.example.Centralthon.domain.member.web.dto.ProfileRes; import com.example.Centralthon.domain.member.web.dto.SignUpReq; import com.example.Centralthon.global.response.SuccessResponse; +import com.example.Centralthon.global.security.CustomMemberDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/members") @@ -25,4 +28,20 @@ public ResponseEntity> signup(@RequestBody @Valid SignUpReq s return ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.created(null)); } + + //로그인 + @PostMapping("/login") + public ResponseEntity> login(@RequestBody @Valid LoginReq loginReq){ + LoginRes loginRes = memberService.login(loginReq); + + return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.created(loginRes)); + } + + // 인가 테스트용 API + @GetMapping("/profile") + public ResponseEntity getProfile(@AuthenticationPrincipal CustomMemberDetails memberDetails) { + Member member = memberDetails.getMember(); + return ResponseEntity + .ok(new ProfileRes(member.getNickName(), member.getRole().getStringRole())); + } } diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java new file mode 100644 index 0000000..94bfc6b --- /dev/null +++ b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java @@ -0,0 +1,15 @@ +package com.example.Centralthon.domain.member.web.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; + +@Getter +public class LoginReq { + @NotBlank(message = "이메일을 입력해주세요.") + private String email; + + @NotBlank(message = "비밀번호를 입력해주세요.") + private String password; +} diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java new file mode 100644 index 0000000..0a2a897 --- /dev/null +++ b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java @@ -0,0 +1,6 @@ +package com.example.Centralthon.domain.member.web.dto; + +public record LoginRes( + String token +) { +} diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java new file mode 100644 index 0000000..e79e7f5 --- /dev/null +++ b/src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java @@ -0,0 +1,7 @@ +package com.example.Centralthon.domain.member.web.dto; + +public record ProfileRes( + String nickName, + String role +) { +} diff --git a/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java b/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java index 492e276..41e7884 100644 --- a/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java +++ b/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java @@ -1,8 +1,12 @@ package com.example.Centralthon.global.config; +import com.example.Centralthon.global.exception.CustomAccessDeniedHandler; +import com.example.Centralthon.global.jwt.JwtTokenFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -12,21 +16,31 @@ @Configuration @RequiredArgsConstructor public class SecurityConfig { + private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final JwtTokenFilter jwtTokenFilter; + // 비밀번호 인코더 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + // 필터 체인 설정 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/members/signup", "/api/members/login").permitAll() //누구나 접근 허용 - .requestMatchers("/api/members/profile").hasRole("ADMIN") // 관리자만 프로필 열람 가능 + .requestMatchers("/api/members/profile").hasRole("ADMIN") // 관리자 권한 필요한 API .anyRequest().authenticated() //로그인한 사용자만 접근 허용 + ) + + .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(ex -> ex + .accessDeniedHandler(customAccessDeniedHandler) ); + return http.build(); } } diff --git a/src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java b/src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..dc8a2c6 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java @@ -0,0 +1,35 @@ +package com.example.Centralthon.global.exception; + +import com.example.Centralthon.global.response.BaseResponse; +import com.example.Centralthon.global.response.code.ErrorResponseCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; // 혹은 빈 주입 가능 + + /** + * 인증은 되었지만, 인가가 부족한 요청이 들어올 때 처리하는 핸들러 + */ + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + + response.setStatus(ErrorResponseCode.ACCESS_DENIED_REQUEST.getHttpStatus()); + response.setContentType("application/json;charset=UTF-8"); + + BaseResponse body = BaseResponse.of(false, ErrorResponseCode.ACCESS_DENIED_REQUEST); + response.getWriter().write(objectMapper.writeValueAsString(body)); + } +} diff --git a/src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java b/src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java new file mode 100644 index 0000000..9f41bf0 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java @@ -0,0 +1,46 @@ +package com.example.Centralthon.global.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtTokenFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = extractToken(request); + + // 토큰 유효성 검사 + if(token != null && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + private String extractToken(HttpServletRequest request) { + String bearer = request.getHeader("Authorization"); + if (bearer != null && bearer.startsWith("Bearer ")) { + return bearer.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java b/src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..f93a4a0 --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java @@ -0,0 +1,87 @@ +package com.example.Centralthon.global.jwt; + +import com.example.Centralthon.domain.member.entity.Member; +import com.example.Centralthon.domain.member.exception.InvalidCredentialsException; +import com.example.Centralthon.domain.member.exception.MemberNotFoundException; +import com.example.Centralthon.domain.member.repository.MemberRepository; +import com.example.Centralthon.global.security.CustomMemberDetails; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + private final MemberRepository memberRepository; + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration}") + private long expiration; + + private Key key; + + @PostConstruct + public void init() { this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); } + + public String createToken(Member member) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + expiration); + + return Jwts.builder() + .subject(member.getEmail()) + .claim("nickname", member.getNickName()) + .claim("role", member.getRole()) + .issuedAt(now) + .expiration(expiry) + .signWith(key) + .compact(); + } + + // 토큰 유효성 검사 + 만료 검사 + public boolean validateToken(String token) { + try{ + getClaims(token); + return true; + }catch (JwtException | IllegalArgumentException e){ + return false; + } + } + + // Claim 파싱 + public Claims getClaims(String token) { + return Jwts.parser() + .verifyWith((SecretKey) key) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public Authentication getAuthentication(String token) { + Claims claims = getClaims(token); + String email = claims.getSubject(); + + Member member = memberRepository.findByEmail(email) + .orElseThrow(MemberNotFoundException::new); + + CustomMemberDetails memberDetails = new CustomMemberDetails(member); + + return new UsernamePasswordAuthenticationToken( + memberDetails, + null, + memberDetails.getAuthorities() + ); + } +} diff --git a/src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java b/src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java new file mode 100644 index 0000000..4d76d4a --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java @@ -0,0 +1,34 @@ +package com.example.Centralthon.global.security; + +import com.example.Centralthon.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class CustomMemberDetails implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(member.getRole().getStringRole())); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3289bd9..f856f6c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,6 +3,10 @@ spring.application.name=Centralthon # application-secret.properties spring.config.import=optional:application-secret.properties +#JWT +jwt.secret=${jwt.secret} +jwt.expiration=${jwt.expiration} + # Datasource spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USERNAME} From 5bd0de19f3d672303e6d483c4e5b9b074197d5bc Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Sun, 20 Jul 2025 14:28:55 +0900 Subject: [PATCH 03/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/web/controller/MemberController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java b/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java index 0761e87..3410c5f 100644 --- a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java +++ b/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java @@ -34,7 +34,7 @@ public ResponseEntity> signup(@RequestBody @Valid SignUpReq s public ResponseEntity> login(@RequestBody @Valid LoginReq loginReq){ LoginRes loginRes = memberService.login(loginReq); - return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.created(loginRes)); + return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(loginRes)); } // 인가 테스트용 API From fc1a79e3d46aa8c1845945feec0a675bd24b6dad Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Tue, 5 Aug 2025 12:25:13 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20JW?= =?UTF-8?q?T=20=EA=B4=80=EB=A0=A8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/entity/Member.java | 4 +- .../member/service/MemberServiceImpl.java | 10 +-- .../web/controller/MemberController.java | 15 +--- .../domain/member/web/dto/LoginRes.java | 2 +- .../domain/member/web/dto/ProfileRes.java | 7 -- .../domain/member/web/dto/SignUpReq.java | 10 ++- .../global/config/SecurityConfig.java | 12 --- .../exception/CustomAccessDeniedHandler.java | 35 -------- .../exception/GlobalExceptionHandler.java | 10 +++ .../global/jwt/JwtTokenFilter.java | 46 ---------- .../global/jwt/JwtTokenProvider.java | 87 ------------------- .../global/response/BaseResponse.java | 2 +- src/main/resources/application.properties | 4 - 13 files changed, 24 insertions(+), 220 deletions(-) delete mode 100644 src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java delete mode 100644 src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java delete mode 100644 src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java delete mode 100644 src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java diff --git a/src/main/java/com/example/Centralthon/domain/member/entity/Member.java b/src/main/java/com/example/Centralthon/domain/member/entity/Member.java index 7e82abc..b4bee43 100644 --- a/src/main/java/com/example/Centralthon/domain/member/entity/Member.java +++ b/src/main/java/com/example/Centralthon/domain/member/entity/Member.java @@ -31,13 +31,13 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private MemberRole role; - public static Member toEntity(String email, String encoded, String nickName) { + public static Member toEntity(String email, String encoded, String nickName, MemberRole role) { return Member.builder() .email(email) .password(encoded) .nickName(nickName) - .role(MemberRole.USER) + .role(role) .build(); } } diff --git a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java b/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java index ba20994..10d7a3b 100644 --- a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java @@ -9,7 +9,6 @@ import com.example.Centralthon.domain.member.web.dto.LoginReq; import com.example.Centralthon.domain.member.web.dto.LoginRes; import com.example.Centralthon.domain.member.web.dto.SignUpReq; -import com.example.Centralthon.global.jwt.JwtTokenProvider; import com.example.Centralthon.global.response.SuccessResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -22,8 +21,6 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; - private final JwtTokenProvider jwtTokenProvider; - @Override @Transactional @@ -32,7 +29,7 @@ public void signUp(SignUpReq signUpReq) { throw new MemberAlreadyExistException(); } String encoded = passwordEncoder.encode(signUpReq.getPassword()); - Member member = Member.toEntity(signUpReq.getEmail(),encoded,signUpReq.getNickName()); + Member member = Member.toEntity(signUpReq.getEmail(),encoded,signUpReq.getNickName(),signUpReq.getRole()); memberRepository.save(member); } @@ -48,10 +45,7 @@ public LoginRes login(LoginReq loginReq) { throw new InvalidCredentialsException(); } - // 토큰 생성 - String token = jwtTokenProvider.createToken(member); - // 반환 - return new LoginRes(token); + return new LoginRes(member.getNickName()); } } diff --git a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java b/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java index 3410c5f..eb013ca 100644 --- a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java +++ b/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java @@ -1,18 +1,14 @@ package com.example.Centralthon.domain.member.web.controller; -import com.example.Centralthon.domain.member.entity.Member; import com.example.Centralthon.domain.member.service.MemberService; import com.example.Centralthon.domain.member.web.dto.LoginReq; import com.example.Centralthon.domain.member.web.dto.LoginRes; -import com.example.Centralthon.domain.member.web.dto.ProfileRes; import com.example.Centralthon.domain.member.web.dto.SignUpReq; import com.example.Centralthon.global.response.SuccessResponse; -import com.example.Centralthon.global.security.CustomMemberDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -23,7 +19,7 @@ public class MemberController { //회원 가입 @PostMapping("/signup") - public ResponseEntity> signup(@RequestBody @Valid SignUpReq signupReq) { + public ResponseEntity> signup(@RequestBody @Valid SignUpReq signupReq) { memberService.signUp(signupReq); return ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.created(null)); @@ -31,17 +27,10 @@ public ResponseEntity> signup(@RequestBody @Valid SignUpReq s //로그인 @PostMapping("/login") - public ResponseEntity> login(@RequestBody @Valid LoginReq loginReq){ + public ResponseEntity> login(@RequestBody @Valid LoginReq loginReq){ LoginRes loginRes = memberService.login(loginReq); return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(loginRes)); } - // 인가 테스트용 API - @GetMapping("/profile") - public ResponseEntity getProfile(@AuthenticationPrincipal CustomMemberDetails memberDetails) { - Member member = memberDetails.getMember(); - return ResponseEntity - .ok(new ProfileRes(member.getNickName(), member.getRole().getStringRole())); - } } diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java index 0a2a897..c06794e 100644 --- a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java +++ b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java @@ -1,6 +1,6 @@ package com.example.Centralthon.domain.member.web.dto; public record LoginRes( - String token + String nickname ) { } diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java deleted file mode 100644 index e79e7f5..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/web/dto/ProfileRes.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.Centralthon.domain.member.web.dto; - -public record ProfileRes( - String nickName, - String role -) { -} diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java index eb29923..be963e6 100644 --- a/src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java +++ b/src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java @@ -1,11 +1,9 @@ package com.example.Centralthon.domain.member.web.dto; -import jakarta.validation.constraints.AssertTrue; +import com.example.Centralthon.domain.member.entity.enums.MemberRole; +import jakarta.validation.constraints.*; import lombok.Data; import lombok.Getter; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; @Getter public class SignUpReq { @@ -23,6 +21,10 @@ public class SignUpReq { @NotBlank(message = "이름을 입력해주세요.") private String nickName; + @NotNull(message = "회원 권한을 입력해주세요.") + private MemberRole role; + + @AssertTrue(message = "비밀번호가 일치하지 않습니다.") public boolean isPasswordMatching() { if (password == null || passwordCheck == null) return false; diff --git a/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java b/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java index 41e7884..3e7e499 100644 --- a/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java +++ b/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java @@ -1,23 +1,16 @@ package com.example.Centralthon.global.config; -import com.example.Centralthon.global.exception.CustomAccessDeniedHandler; -import com.example.Centralthon.global.jwt.JwtTokenFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @RequiredArgsConstructor public class SecurityConfig { - private final CustomAccessDeniedHandler customAccessDeniedHandler; - private final JwtTokenFilter jwtTokenFilter; // 비밀번호 인코더 @Bean @@ -34,11 +27,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/members/signup", "/api/members/login").permitAll() //누구나 접근 허용 .requestMatchers("/api/members/profile").hasRole("ADMIN") // 관리자 권한 필요한 API .anyRequest().authenticated() //로그인한 사용자만 접근 허용 - ) - - .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling(ex -> ex - .accessDeniedHandler(customAccessDeniedHandler) ); return http.build(); diff --git a/src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java b/src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java deleted file mode 100644 index dc8a2c6..0000000 --- a/src/main/java/com/example/Centralthon/global/exception/CustomAccessDeniedHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.Centralthon.global.exception; - -import com.example.Centralthon.global.response.BaseResponse; -import com.example.Centralthon.global.response.code.ErrorResponseCode; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Component -@RequiredArgsConstructor -public class CustomAccessDeniedHandler implements AccessDeniedHandler { - - private final ObjectMapper objectMapper; // 혹은 빈 주입 가능 - - /** - * 인증은 되었지만, 인가가 부족한 요청이 들어올 때 처리하는 핸들러 - */ - @Override - public void handle(HttpServletRequest request, - HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException { - - response.setStatus(ErrorResponseCode.ACCESS_DENIED_REQUEST.getHttpStatus()); - response.setContentType("application/json;charset=UTF-8"); - - BaseResponse body = BaseResponse.of(false, ErrorResponseCode.ACCESS_DENIED_REQUEST); - response.getWriter().write(objectMapper.writeValueAsString(body)); - } -} diff --git a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java index d6fa5c5..1dc1edd 100644 --- a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java @@ -43,6 +43,16 @@ public ResponseEntity> handleBindException(BindException e) { @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { log.error("HttpMessageNotReadableException : {}", e.getMessage(), e); + + // 회원 권한 Enum 파싱 실패 했을 때 + if (e.getMessage() != null && e.getMessage().contains("MemberRole")) { + ErrorResponse errorResponse = ErrorResponse.of( + ErrorResponseCode.INVALID_HTTP_MESSAGE_BODY, + "회원 권한은 USER 또는 ADMIN 중 하나여야 합니다."); + return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); + } + + // 일반적인 파싱 실패 처리 ErrorResponse errorResponse = ErrorResponse.from(ErrorResponseCode.INVALID_HTTP_MESSAGE_BODY); return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); } diff --git a/src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java b/src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java deleted file mode 100644 index 9f41bf0..0000000 --- a/src/main/java/com/example/Centralthon/global/jwt/JwtTokenFilter.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.Centralthon.global.jwt; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -@RequiredArgsConstructor -public class JwtTokenFilter extends OncePerRequestFilter { - - private final JwtTokenProvider jwtTokenProvider; - @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain - ) throws ServletException, IOException { - String token = extractToken(request); - - // 토큰 유효성 검사 - if(token != null && jwtTokenProvider.validateToken(token)) { - Authentication authentication = jwtTokenProvider.getAuthentication(token); - - SecurityContextHolder.getContext().setAuthentication(authentication); - } - - filterChain.doFilter(request, response); - } - - private String extractToken(HttpServletRequest request) { - String bearer = request.getHeader("Authorization"); - if (bearer != null && bearer.startsWith("Bearer ")) { - return bearer.substring(7); - } - return null; - } -} diff --git a/src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java b/src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java deleted file mode 100644 index f93a4a0..0000000 --- a/src/main/java/com/example/Centralthon/global/jwt/JwtTokenProvider.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.example.Centralthon.global.jwt; - -import com.example.Centralthon.domain.member.entity.Member; -import com.example.Centralthon.domain.member.exception.InvalidCredentialsException; -import com.example.Centralthon.domain.member.exception.MemberNotFoundException; -import com.example.Centralthon.domain.member.repository.MemberRepository; -import com.example.Centralthon.global.security.CustomMemberDetails; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import java.security.Key; -import java.util.Date; - -@Component -@RequiredArgsConstructor -public class JwtTokenProvider { - private final MemberRepository memberRepository; - - @Value("${jwt.secret}") - private String secretKey; - - @Value("${jwt.expiration}") - private long expiration; - - private Key key; - - @PostConstruct - public void init() { this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); } - - public String createToken(Member member) { - Date now = new Date(); - Date expiry = new Date(now.getTime() + expiration); - - return Jwts.builder() - .subject(member.getEmail()) - .claim("nickname", member.getNickName()) - .claim("role", member.getRole()) - .issuedAt(now) - .expiration(expiry) - .signWith(key) - .compact(); - } - - // 토큰 유효성 검사 + 만료 검사 - public boolean validateToken(String token) { - try{ - getClaims(token); - return true; - }catch (JwtException | IllegalArgumentException e){ - return false; - } - } - - // Claim 파싱 - public Claims getClaims(String token) { - return Jwts.parser() - .verifyWith((SecretKey) key) - .build() - .parseSignedClaims(token) - .getPayload(); - } - - public Authentication getAuthentication(String token) { - Claims claims = getClaims(token); - String email = claims.getSubject(); - - Member member = memberRepository.findByEmail(email) - .orElseThrow(MemberNotFoundException::new); - - CustomMemberDetails memberDetails = new CustomMemberDetails(member); - - return new UsernamePasswordAuthenticationToken( - memberDetails, - null, - memberDetails.getAuthorities() - ); - } -} diff --git a/src/main/java/com/example/Centralthon/global/response/BaseResponse.java b/src/main/java/com/example/Centralthon/global/response/BaseResponse.java index 680e3ed..eb85b66 100644 --- a/src/main/java/com/example/Centralthon/global/response/BaseResponse.java +++ b/src/main/java/com/example/Centralthon/global/response/BaseResponse.java @@ -12,7 +12,7 @@ @ToString @RequiredArgsConstructor public class BaseResponse { - private final Boolean inSuccess; + private final Boolean isSuccess; private final String code; private final String message; private final String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f856f6c..3289bd9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,10 +3,6 @@ spring.application.name=Centralthon # application-secret.properties spring.config.import=optional:application-secret.properties -#JWT -jwt.secret=${jwt.secret} -jwt.expiration=${jwt.expiration} - # Datasource spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USERNAME} From 31fc2c426b6e86de125af5706fa41f16ea954e7c Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Sun, 10 Aug 2025 18:56:42 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20me?= =?UTF-8?q?mber=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/entity/Member.java | 43 ---------------- .../member/entity/enums/MemberRole.java | 18 ------- .../InvalidCredentialsException.java | 7 --- .../MemberAlreadyExistException.java | 8 --- .../member/exception/MemberErrorCode.java | 17 ------- .../exception/MemberNotFoundException.java | 7 --- .../member/repository/MemberRepository.java | 18 ------- .../domain/member/service/MemberService.java | 15 ------ .../member/service/MemberServiceImpl.java | 51 ------------------- .../web/controller/MemberController.java | 36 ------------- .../domain/member/web/dto/LoginReq.java | 15 ------ .../domain/member/web/dto/LoginRes.java | 6 --- .../domain/member/web/dto/SignUpReq.java | 33 ------------ .../global/config/SecurityConfig.java | 34 ------------- .../exception/GlobalExceptionHandler.java | 10 ---- 15 files changed, 318 deletions(-) delete mode 100644 src/main/java/com/example/Centralthon/domain/member/entity/Member.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/exception/MemberAlreadyExistException.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/service/MemberService.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java delete mode 100644 src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java delete mode 100644 src/main/java/com/example/Centralthon/global/config/SecurityConfig.java diff --git a/src/main/java/com/example/Centralthon/domain/member/entity/Member.java b/src/main/java/com/example/Centralthon/domain/member/entity/Member.java deleted file mode 100644 index b4bee43..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/entity/Member.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.Centralthon.domain.member.entity; - -import com.example.Centralthon.domain.member.entity.enums.MemberRole; -import com.example.Centralthon.domain.member.web.dto.SignUpReq; -import com.example.Centralthon.global.entity.BaseEntity; -import jakarta.persistence.*; -import lombok.*; -import org.springframework.security.crypto.password.PasswordEncoder; - -@Entity -@Getter -@Setter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "MEMBERS") -public class Member extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String email; - - @Column(nullable = false) - private String password; - - @Column(nullable = false, unique = true) - private String nickName; - - @Enumerated(EnumType.STRING) - private MemberRole role; - - public static Member toEntity(String email, String encoded, String nickName, MemberRole role) { - - return Member.builder() - .email(email) - .password(encoded) - .nickName(nickName) - .role(role) - .build(); - } -} diff --git a/src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java b/src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java deleted file mode 100644 index 206b1eb..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/entity/enums/MemberRole.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.Centralthon.domain.member.entity.enums; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum MemberRole { - USER("ROLE_USER"), - ADMIN("ROLE_ADMIN"); - - @Getter - private final String memberRole; - - public String getStringRole() { - return memberRole; - } -} diff --git a/src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java b/src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java deleted file mode 100644 index 3d06aea..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/exception/InvalidCredentialsException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.Centralthon.domain.member.exception; - -import com.example.Centralthon.global.exception.BaseException; - -public class InvalidCredentialsException extends BaseException { - public InvalidCredentialsException( ) {super(MemberErrorCode.INVALID_CREDENTIALS);} -} diff --git a/src/main/java/com/example/Centralthon/domain/member/exception/MemberAlreadyExistException.java b/src/main/java/com/example/Centralthon/domain/member/exception/MemberAlreadyExistException.java deleted file mode 100644 index efa1891..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/exception/MemberAlreadyExistException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.Centralthon.domain.member.exception; - -import com.example.Centralthon.global.exception.BaseException; -import com.example.Centralthon.global.response.code.BaseResponseCode; - -public class MemberAlreadyExistException extends BaseException { - public MemberAlreadyExistException() {super(MemberErrorCode.MEMBER_ALREADY_EXIST);} -} diff --git a/src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java b/src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java deleted file mode 100644 index b83c403..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/exception/MemberErrorCode.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.Centralthon.domain.member.exception; - -import com.example.Centralthon.global.response.code.BaseResponseCode; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum MemberErrorCode implements BaseResponseCode { - MEMBER_ALREADY_EXIST("MEMBER_409_1", 409, "이미 존재하는 사용자입니다."), - INVALID_CREDENTIALS("MEMBER_401_1", 401, "아이디 또는 비밀번호를 다시 확인해주세요"), - MEMBER_NOT_FOUND("MEMBER_404_1",404,"존재하지 않는 사용자입니다."); - - private final String code; - private final int httpStatus; - private final String message; -} diff --git a/src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java b/src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java deleted file mode 100644 index 4fcdac3..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/exception/MemberNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.Centralthon.domain.member.exception; - -import com.example.Centralthon.global.exception.BaseException; - -public class MemberNotFoundException extends BaseException { - public MemberNotFoundException() {super(MemberErrorCode.MEMBER_NOT_FOUND);} -} diff --git a/src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java b/src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java deleted file mode 100644 index 5872243..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/repository/MemberRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.Centralthon.domain.member.repository; - -import com.example.Centralthon.domain.member.entity.Member; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface MemberRepository extends JpaRepository { - - Optional findByEmailOrNickName( String email, String nickName); - - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/Centralthon/domain/member/service/MemberService.java b/src/main/java/com/example/Centralthon/domain/member/service/MemberService.java deleted file mode 100644 index b57da9a..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/service/MemberService.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.Centralthon.domain.member.service; - -import com.example.Centralthon.domain.member.web.dto.LoginReq; -import com.example.Centralthon.domain.member.web.dto.LoginRes; -import com.example.Centralthon.domain.member.web.dto.SignUpReq; -import com.example.Centralthon.global.response.SuccessResponse; -import jakarta.validation.Valid; -import org.springframework.http.ResponseEntity; - -public interface MemberService { - - void signUp( SignUpReq signupReq); - - LoginRes login( LoginReq loginReq); -} diff --git a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java b/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java deleted file mode 100644 index 10d7a3b..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/service/MemberServiceImpl.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.Centralthon.domain.member.service; - -import com.example.Centralthon.domain.member.entity.Member; -import com.example.Centralthon.domain.member.entity.enums.MemberRole; -import com.example.Centralthon.domain.member.exception.InvalidCredentialsException; -import com.example.Centralthon.domain.member.exception.MemberAlreadyExistException; -import com.example.Centralthon.domain.member.exception.MemberErrorCode; -import com.example.Centralthon.domain.member.repository.MemberRepository; -import com.example.Centralthon.domain.member.web.dto.LoginReq; -import com.example.Centralthon.domain.member.web.dto.LoginRes; -import com.example.Centralthon.domain.member.web.dto.SignUpReq; -import com.example.Centralthon.global.response.SuccessResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class MemberServiceImpl implements MemberService { - private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; - - @Override - @Transactional - public void signUp(SignUpReq signUpReq) { - if (memberRepository .findByEmailOrNickName(signUpReq.getEmail(), signUpReq.getNickName()) .isPresent()) { - throw new MemberAlreadyExistException(); } - - String encoded = passwordEncoder.encode(signUpReq.getPassword()); - Member member = Member.toEntity(signUpReq.getEmail(),encoded,signUpReq.getNickName(),signUpReq.getRole()); - - memberRepository.save(member); - } - - @Override - public LoginRes login(LoginReq loginReq) { - // 아이디 확인 - Member member = memberRepository.findByEmail(loginReq.getEmail()) - .orElseThrow(InvalidCredentialsException::new); - - // 비밀번호 검증 - if (!passwordEncoder.matches(loginReq.getPassword(), member.getPassword())) { - throw new InvalidCredentialsException(); - } - - // 반환 - return new LoginRes(member.getNickName()); - } -} diff --git a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java b/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java deleted file mode 100644 index eb013ca..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/web/controller/MemberController.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.Centralthon.domain.member.web.controller; - -import com.example.Centralthon.domain.member.service.MemberService; -import com.example.Centralthon.domain.member.web.dto.LoginReq; -import com.example.Centralthon.domain.member.web.dto.LoginRes; -import com.example.Centralthon.domain.member.web.dto.SignUpReq; -import com.example.Centralthon.global.response.SuccessResponse; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/members") -@RequiredArgsConstructor -public class MemberController { - private final MemberService memberService; - - //회원 가입 - @PostMapping("/signup") - public ResponseEntity> signup(@RequestBody @Valid SignUpReq signupReq) { - memberService.signUp(signupReq); - - return ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.created(null)); - } - - //로그인 - @PostMapping("/login") - public ResponseEntity> login(@RequestBody @Valid LoginReq loginReq){ - LoginRes loginRes = memberService.login(loginReq); - - return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(loginRes)); - } - -} diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java deleted file mode 100644 index 94bfc6b..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginReq.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.Centralthon.domain.member.web.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.Getter; - -@Getter -public class LoginReq { - @NotBlank(message = "이메일을 입력해주세요.") - private String email; - - @NotBlank(message = "비밀번호를 입력해주세요.") - private String password; -} diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java deleted file mode 100644 index c06794e..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/web/dto/LoginRes.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.Centralthon.domain.member.web.dto; - -public record LoginRes( - String nickname -) { -} diff --git a/src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java b/src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java deleted file mode 100644 index be963e6..0000000 --- a/src/main/java/com/example/Centralthon/domain/member/web/dto/SignUpReq.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.Centralthon.domain.member.web.dto; - -import com.example.Centralthon.domain.member.entity.enums.MemberRole; -import jakarta.validation.constraints.*; -import lombok.Data; -import lombok.Getter; - -@Getter -public class SignUpReq { - @NotBlank(message = "이메일을 입력해주세요.") - @Email(message = "올바른 이메일 형식으로 작성해주세요.") - private String email; - - @NotBlank(message = "비밀번호를 입력해주세요.") - @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d\\S]{8,20}$", message = "영문, 숫자를 포함한 8~20자리 이내로 입력해주세요.") - private String password; - - @NotBlank(message = "비밀번호를 한 번 더 입력해주세요.") - private String passwordCheck; - - @NotBlank(message = "이름을 입력해주세요.") - private String nickName; - - @NotNull(message = "회원 권한을 입력해주세요.") - private MemberRole role; - - - @AssertTrue(message = "비밀번호가 일치하지 않습니다.") - public boolean isPasswordMatching() { - if (password == null || passwordCheck == null) return false; - return password.equals(passwordCheck); - } -} diff --git a/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java b/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java deleted file mode 100644 index 3e7e499..0000000 --- a/src/main/java/com/example/Centralthon/global/config/SecurityConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.Centralthon.global.config; - -import lombok.RequiredArgsConstructor; -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.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; - -@Configuration -@RequiredArgsConstructor -public class SecurityConfig { - - // 비밀번호 인코더 - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - // 필터 체인 설정 - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/members/signup", "/api/members/login").permitAll() //누구나 접근 허용 - .requestMatchers("/api/members/profile").hasRole("ADMIN") // 관리자 권한 필요한 API - .anyRequest().authenticated() //로그인한 사용자만 접근 허용 - ); - - return http.build(); - } -} diff --git a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java index 1dc1edd..d6fa5c5 100644 --- a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java @@ -43,16 +43,6 @@ public ResponseEntity> handleBindException(BindException e) { @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { log.error("HttpMessageNotReadableException : {}", e.getMessage(), e); - - // 회원 권한 Enum 파싱 실패 했을 때 - if (e.getMessage() != null && e.getMessage().contains("MemberRole")) { - ErrorResponse errorResponse = ErrorResponse.of( - ErrorResponseCode.INVALID_HTTP_MESSAGE_BODY, - "회원 권한은 USER 또는 ADMIN 중 하나여야 합니다."); - return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); - } - - // 일반적인 파싱 실패 처리 ErrorResponse errorResponse = ErrorResponse.from(ErrorResponseCode.INVALID_HTTP_MESSAGE_BODY); return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); } From 019a582d80d811989af33b15071fa9e440b7fbbb Mon Sep 17 00:00:00 2001 From: frombunny Date: Mon, 11 Aug 2025 19:03:45 +0900 Subject: [PATCH 06/10] =?UTF-8?q?:recycle:=20refactor:=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20CustomMember?= =?UTF-8?q?Details=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/CustomMemberDetails.java | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java diff --git a/src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java b/src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java deleted file mode 100644 index 4d76d4a..0000000 --- a/src/main/java/com/example/Centralthon/global/security/CustomMemberDetails.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.Centralthon.global.security; - -import com.example.Centralthon.domain.member.entity.Member; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.List; - -@Getter -@RequiredArgsConstructor -public class CustomMemberDetails implements UserDetails { - - private final Member member; - - @Override - public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority(member.getRole().getStringRole())); - } - - @Override - public String getPassword() { - return member.getPassword(); - } - - @Override - public String getUsername() { - return member.getEmail(); - } - -} From d6ed155d86357326d8a580aad4a1d7d89ee2c1bf Mon Sep 17 00:00:00 2001 From: frombunny Date: Tue, 12 Aug 2025 03:19:39 +0900 Subject: [PATCH 07/10] =?UTF-8?q?:construction=5Fworker:=20CI:=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cicd.yml | 86 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/cicd.yml diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..e1ececa --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,86 @@ +name: cicd.yml +on: + push: + branches: [ "main" ] # main 브랜치에 push가 발생하면 실행한다는 조건 + pull_request: + branches: [ "main" ] # main 브랜치에 pull_request가 발생하면 실행한다는 조건 + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build -x test + + - name: Docker build + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker build -t ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest + + - name: Deploy to dev + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ubuntu + key: ${{ secrets.PRIVATE_KEY }} + script: | + echo "${{ secrets.ENV_FILE }}" > .env + export $(grep -v '^#' .env | xargs) + + sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest + sudo docker stop leftOversFlirting || true + sudo docker rm leftOversFlirting || true + set -a + source .env + set +a + sudo docker run -d \ + --name leftOversFlirting \ + --restart always \ + --log-driver=syslog \ + -p 8080:8080 \ + --env-file .env \ + ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest + sudo docker image prune -a -f + + + - name: Check logs + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ubuntu + key: ${{ secrets.PRIVATE_KEY }} + script: | + if sudo docker ps -a --format '{{.Names}}' | grep -q '^leftOversFlirting$'; then + sudo docker logs leftOversFlirting + else + echo "!!! no container !!!" + exit 1 + fi \ No newline at end of file From 79ab459025b431e1ef74fc905e993cc002bccaa9 Mon Sep 17 00:00:00 2001 From: frombunny Date: Wed, 13 Aug 2025 06:18:45 +0900 Subject: [PATCH 08/10] =?UTF-8?q?:construction=5Fworker:=20CI:=20ci.yml=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 68 ++++++++++++++++++++++++++++++ .github/workflows/cicd.yml | 86 -------------------------------------- 2 files changed, 68 insertions(+), 86 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/cicd.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1d0d5c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI with Gradle + +on: + pull_request: + branches: ["main"] + push: + branches: ["main"] + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + JAVA_VERSION: '21' + IMAGE: ${{ secrets.DOCKER_USERNAME }}/leftoversflirting + +jobs: + build: # 1) 빌드 전용 Job — PR과 push 모두 실행 + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Temurin JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build JAR + run: ./gradlew bootJar -x test --no-daemon + + docker-push: + name: Docker Build & Push + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build & Push image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ env.IMAGE }}:latest + ${{ env.IMAGE }}:${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml deleted file mode 100644 index e1ececa..0000000 --- a/.github/workflows/cicd.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: cicd.yml -on: - push: - branches: [ "main" ] # main 브랜치에 push가 발생하면 실행한다는 조건 - pull_request: - branches: [ "main" ] # main 브랜치에 pull_request가 발생하면 실행한다는 조건 - workflow_dispatch: - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - - name: Gradle Caching - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with Gradle - run: ./gradlew build -x test - - - name: Docker build - run: | - docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker build -t ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest - - - name: Deploy to dev - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ubuntu - key: ${{ secrets.PRIVATE_KEY }} - script: | - echo "${{ secrets.ENV_FILE }}" > .env - export $(grep -v '^#' .env | xargs) - - sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - sudo docker pull ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest - sudo docker stop leftOversFlirting || true - sudo docker rm leftOversFlirting || true - set -a - source .env - set +a - sudo docker run -d \ - --name leftOversFlirting \ - --restart always \ - --log-driver=syslog \ - -p 8080:8080 \ - --env-file .env \ - ${{ secrets.DOCKER_USERNAME }}/leftOversFlirting:latest - sudo docker image prune -a -f - - - - name: Check logs - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ubuntu - key: ${{ secrets.PRIVATE_KEY }} - script: | - if sudo docker ps -a --format '{{.Names}}' | grep -q '^leftOversFlirting$'; then - sudo docker logs leftOversFlirting - else - echo "!!! no container !!!" - exit 1 - fi \ No newline at end of file From 84f100457c8a79226597fa22ffe89c3698739f70 Mon Sep 17 00:00:00 2001 From: frombunny Date: Wed, 13 Aug 2025 06:25:48 +0900 Subject: [PATCH 09/10] =?UTF-8?q?:construction=5Fworker:=20CI:=20cd.yml=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..d089d69 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,43 @@ +name: CD + +on: + push: + branches: + - main # main 브랜치 push 시 자동 배포 + workflow_dispatch: # 수동 실행 + +permissions: + contents: read + +env: + IMAGE: ${{ secrets.DOCKER_USERNAME }}/leftoversflirting + +jobs: + deploy: + name: Deploy to Server + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + # SSH로 서버에 접속해 배포 + - name: Deploy over SSH + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + port: 22 + script: | + echo "[1/3] Pull latest Docker image..." + docker pull ${{ env.IMAGE }}:latest + + echo "[2/3] Restart container with new image..." + docker stop leftoversflirting || true + docker rm leftoversflirting || true + docker run -d --name leftoversflirting \ + -p 8080:8080 \ + ${{ env.IMAGE }}:latest + + echo "[3/3] Cleanup unused images..." + docker image prune -f \ No newline at end of file From 0d92dee4b1bc8be64f2476d4a99f9bfee92b992d Mon Sep 17 00:00:00 2001 From: frombunny Date: Wed, 13 Aug 2025 06:40:50 +0900 Subject: [PATCH 10/10] =?UTF-8?q?:construction=5Fworker:=20CI:=20Dockerfil?= =?UTF-8?q?e=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..71d8cc1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY build/libs/*.jar /app/app.jar +ENTRYPOINT ["java","-jar","/app/app.jar"] \ No newline at end of file