Skip to content
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,9 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요
</tr>
</table>

<br><br />
# 4. API 명세
<img width="1485" alt="image" src="https://github.com/user-attachments/assets/c63821ac-4334-4b76-b7f9-0e9741874d60" />
<img width="1476" alt="image" src="https://github.com/user-attachments/assets/e8ff2953-8a5a-49d3-ab06-6b552e5d01de" />
<img width="1456" alt="image" src="https://github.com/user-attachments/assets/5f1a7836-d0ec-4ded-b7ca-725dffb7e7ed" />
<img width="1475" alt="image" src="https://github.com/user-attachments/assets/833f9546-9db9-42a4-8a0c-bdb47244ec30" />
<img width="1468" alt="image" src="https://github.com/user-attachments/assets/e9efc1c1-45fe-482a-8db0-ab40696ede33" />
<img width="1469" alt="image" src="https://github.com/user-attachments/assets/f4b7523b-d359-4b68-8709-f8c5ee3c2ecc" />
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/b75c8a2c-2d64-4cc2-8581-aa853137fdad" />
<br><br />
# 4. 기술 스택 (Tech Stack)

# 5. 기술 스택 (Tech Stack)

### 5.1 백엔드
### 4.1 백엔드

| 구분 | 기술 |
|------|------|
Expand All @@ -119,29 +108,29 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요
| 테스트 | ![JUnit5](https://img.shields.io/badge/JUnit%205-25A162?style=flat&logo=jest&logoColor=white)<br>![Mockito](https://img.shields.io/badge/Mockito-5A6268?style=flat&logo=mockito&logoColor=white) |
| DB 마이그레이션 | ![Liquibase](https://img.shields.io/badge/Liquibase-2962FF?style=flat&logo=liquibase&logoColor=white) |

### 5.2 프론트엔드
### 4.2 프론트엔드

| 구분 | 기술 |
|------|------|
| 라이브러리 | ![React](https://img.shields.io/badge/React-61DAFB?style=flat&logo=react&logoColor=black) |
| HTTP 클라이언트 | ![Axios](https://img.shields.io/badge/Axios-5A29E4?style=flat&logo=axios&logoColor=white) |

### 5.3 AI
### 4.3 AI

| 구분 | 기술 |
|------|------|
| 백엔드 연동 | ![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=flat&logo=fastapi&logoColor=white) |
| AI 플랫폼 | ![OpenAI](https://img.shields.io/badge/OpenAI-412991?style=flat&logo=openai&logoColor=white)<br>![Hugging Face](https://img.shields.io/badge/Hugging%20Face-FFD21F?style=flat&logo=huggingface&logoColor=black) |
| LLM 프레임워크 | ![LangChain](https://img.shields.io/badge/LangChain-000000?style=flat&logo=python&logoColor=white) |

### 5.4 데이터베이스
### 4.4 데이터베이스

| 구분 | 기술 |
|------|------|
| RDBMS | ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?style=flat&logo=postgresql&logoColor=white) |
| 벡터DB 확장 | ![pgvector](https://img.shields.io/badge/pgvector-000000?style=flat&logo=postgresql&logoColor=white) |

### 5.5 협업 & 형상 관리
### 4.5 협업 & 형상 관리

| 구분 | 기술 |
|------|------|
Expand All @@ -152,14 +141,26 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요



## 5.6 프로젝트 구조
## 4.6 프로젝트 구조
- 시스템 아키텍처
<br>![Image](https://github.com/user-attachments/assets/dfb7d2f7-f93f-46d5-a135-7e75038697d5)

- ERD
<br>![Image](https://github.com/user-attachments/assets/271fbd65-50ba-4528-a96c-3ae0b16a7d34)
<br><br />

<br><br />

# 5. API 명세
<img width="1485" alt="image" src="https://github.com/user-attachments/assets/c63821ac-4334-4b76-b7f9-0e9741874d60" />
<img width="1476" alt="image" src="https://github.com/user-attachments/assets/e8ff2953-8a5a-49d3-ab06-6b552e5d01de" />
<img width="1456" alt="image" src="https://github.com/user-attachments/assets/5f1a7836-d0ec-4ded-b7ca-725dffb7e7ed" />
<img width="1475" alt="image" src="https://github.com/user-attachments/assets/833f9546-9db9-42a4-8a0c-bdb47244ec30" />
<img width="1468" alt="image" src="https://github.com/user-attachments/assets/e9efc1c1-45fe-482a-8db0-ab40696ede33" />
<img width="1469" alt="image" src="https://github.com/user-attachments/assets/f4b7523b-d359-4b68-8709-f8c5ee3c2ecc" />
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/b75c8a2c-2d64-4cc2-8581-aa853137fdad" />
<br><br />

# 6. 기대 효과

### 사용자 측면
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ public User handleKakaoLogin(String code, HttpServletResponse response){

tokenRepository.findByUser(user)
.ifPresentOrElse(
token -> token.updateRefreshToken(refreshToken, expiryTime),
token -> {
token.updateRefreshToken(refreshToken, expiryTime);
tokenRepository.save(token);
},
() -> tokenRepository.save(Token.builder()
.user(user)
.refreshToken(refreshToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public ReviewCreateResponse createReview(User user, ReviewCreateRequest request)

@Transactional
public ReviewUpdateResponse updateReview(User user, ReviewUpdateRequest request) {
validateBanWords(request.getTitle());
validateBanWords(request.getContent());

Review review = reviewRepository.findById(request.getReviewId()).get();

review.updateReview(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.ureca.uplait.domain.user.repository.UserRepository;
import com.ureca.uplait.global.security.jwt.JwtValidator;
import com.ureca.uplait.global.security.jwt.entrypoint.JwtAuthenticationEntryPoint;
import com.ureca.uplait.global.security.jwt.filter.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -40,12 +41,17 @@ public SecurityFilterChain filterChain(HttpSecurity http, UserRepository userRep
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.formLogin(form -> form.disable())
.exceptionHandling(e -> e
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/auth/reissue", "/auth/logout").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll()
.requestMatchers("/health").permitAll()
.requestMatchers("/plan/**").permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers(HttpMethod.POST, "/user/extra-info").hasRole("TMP_USER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtValidator, userRepository), UsernamePasswordAuthenticationFilter.class)
Expand Down
68 changes: 41 additions & 27 deletions src/main/java/com/ureca/uplait/global/security/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Date;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

import com.ureca.uplait.domain.user.entity.User;
Expand Down Expand Up @@ -57,43 +58,56 @@ public String createRefreshToken(User user) {
}

public void addAccessTokenCookie(HttpServletResponse response, String token){
String cookieString = String.format(
"accessToken=%s; Path=/; Max-Age=%d; HttpOnly; Secure=%s; SameSite=%s; Domain=%s",
token,
ACCESS_TOKEN_VALIDITY_SECONDS / 1000,
isSecure ? "Secure" : "",
sameSite,
cookieDomain
);
response.addHeader("Set-Cookie", cookieString);
ResponseCookie cookie = ResponseCookie.from("accessToken", token)
.path("/")
.httpOnly(true)
.secure(isSecure)
.maxAge(ACCESS_TOKEN_VALIDITY_SECONDS / 1000)
.sameSite(sameSite)
.domain(cookieDomain.isBlank() ? null : cookieDomain)
.build();

response.addHeader("Set-Cookie", cookie.toString());

}

public void addRefreshTokenCookie(HttpServletResponse response, String token){
Cookie cookie = new Cookie("refreshToken", token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge((int) (REFRESH_TOKEN_VALIDITY_SECONDS / 1000));
response.addCookie(cookie);
ResponseCookie cookie = ResponseCookie.from("refreshToken", token)
.path("/")
.httpOnly(true)
.secure(isSecure)
.maxAge(REFRESH_TOKEN_VALIDITY_SECONDS / 1000)
.sameSite(sameSite)
.domain(cookieDomain.isBlank() ? null : cookieDomain)
.build();

response.addHeader("Set-Cookie", cookie.toString());
}

public void deleteAccessTokenCookie(HttpServletResponse response){
String cookieString = String.format(
"accessToken=; Path=/; Max-Age=0; HttpOnly; Secure=%s; SameSite=%s; Domain=%s",
isSecure ? "Secure" : "",
sameSite,
cookieDomain
);
response.addHeader("Set-Cookie", cookieString);
ResponseCookie cookie = ResponseCookie.from("accessToken", "")
.path("/")
.httpOnly(true)
.secure(isSecure)
.maxAge(0)
.sameSite(sameSite)
.domain(cookieDomain.isBlank() ? null : cookieDomain)
.build();

response.addHeader("Set-Cookie", cookie.toString());
}

public void deleteRefreshTokenCookie(HttpServletResponse response){
Cookie cookie = new Cookie("refreshToken", null);
cookie.setMaxAge(0);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
ResponseCookie cookie = ResponseCookie.from("refreshToken", "")
.path("/")
.httpOnly(true)
.secure(isSecure)
.maxAge(0)
.sameSite(sameSite)
.domain(cookieDomain.isBlank() ? null : cookieDomain)
.build();

response.addHeader("Set-Cookie", cookie.toString());
}

public LocalDateTime getRefreshTokenExpiry(String token){
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.ureca.uplait.global.security.jwt.entrypoint;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.ureca.uplait.global.security.jwt.JwtProvider;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public JwtAuthenticationFilter(JwtValidator jwtValidator, UserRepository userRep
this.userRepository = userRepository;
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getRequestURI().equals("/auth/reissue");
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ kakao:
client-id: ${KAKAO_CLIENT_ID}
redirect-uri: ${KAKAO_REDIRECT_URI}
jwt:
secret-key: ${JWT_SECRET}
secret: ${JWT_SECRET}
access-token-validity: 1800000
refresh-token-validity: 604800000
vector:
Expand Down
Loading