Skip to content

Commit e7a6649

Browse files
authored
[fix] React & Spring Boot 연결 시도_1
[fix] React & Spring Boot 연결 시도_1
2 parents dcf7b4f + c9c3ae8 commit e7a6649

9 files changed

Lines changed: 162 additions & 31 deletions

File tree

backend/pirocheck/src/main/java/backend/pirocheck/User/exception/GlobalExceptionHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
@RestControllerAdvice
1010
public class GlobalExceptionHandler {
1111

12+
// InvalidLoginException (로그인 실패)
1213
@ExceptionHandler(InvalidLoginException.class)
1314
public ResponseEntity<ApiResponse<?>> handleInvalidLoginException(InvalidLoginException e) {
1415
return ResponseEntity
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package backend.pirocheck.User.filter;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import jakarta.servlet.http.HttpSession;
8+
import org.springframework.web.filter.OncePerRequestFilter;
9+
10+
import java.io.IOException;
11+
12+
public class SessionCheckFilter extends OncePerRequestFilter {
13+
14+
@Override
15+
protected void doFilterInternal(HttpServletRequest request,
16+
HttpServletResponse response,
17+
FilterChain filterChain)
18+
throws ServletException, IOException {
19+
20+
String path = request.getRequestURI();
21+
22+
// 로그인/로그아웃 요청은 세션 체크 제외
23+
if (path.startsWith("/api/login") || path.startsWith("/api/logout")) {
24+
filterChain.doFilter(request, response); // 다음 필터나 컨트롤러로 넘기는 명령어
25+
return; // 세션 검사 안함
26+
}
27+
28+
HttpSession session = request.getSession(false); // 세션이 없으면 새로 만들지 않고 null을 리턴 (true : 새로 생성)
29+
30+
if (session == null || session.getAttribute("loginUser") == null) {
31+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 설정
32+
response.setContentType("application/json;charset=UTF-8");
33+
response.getWriter().write("{\"success\":false,\"message\":\"세션이 만료되었습니다.\",\"data\":null}");
34+
return;
35+
}
36+
37+
filterChain.doFilter(request, response);
38+
39+
}
40+
}

backend/pirocheck/src/main/java/backend/pirocheck/User/service/UserService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ public class UserService {
1414

1515
public User login(String name, String password) {
1616
User user = userRepository.findByName(name)
17-
.orElseThrow(() -> new InvalidLoginException("해당 사용자가 존재하지 않습니다."));
17+
.orElseThrow(() -> new InvalidLoginException("해당 사용자가 존재하지 않습니다.")); //401
1818

1919
if (!user.getPassword().equals(password)) {
20-
throw new InvalidLoginException("비밀번호가 일치하지 않습니다.");
20+
throw new InvalidLoginException("비밀번호가 일치하지 않습니다."); //401
2121
}
2222

2323
return user;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package backend.pirocheck.config;
2+
3+
import backend.pirocheck.User.filter.SessionCheckFilter;
4+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
@Configuration
9+
public class SessionCheckFilterConfig {
10+
11+
@Bean
12+
public FilterRegistrationBean<SessionCheckFilter> sessionCheckFilter() {
13+
FilterRegistrationBean<SessionCheckFilter> registrationBean = new FilterRegistrationBean<>();
14+
registrationBean.setFilter(new SessionCheckFilter());
15+
registrationBean.addUrlPatterns("/api/*");
16+
registrationBean.setOrder(1);
17+
return registrationBean;
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package backend.pirocheck.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.web.servlet.config.annotation.CorsRegistry;
5+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6+
7+
@Configuration
8+
public class WebConfig implements WebMvcConfigurer {
9+
10+
@Override
11+
public void addCorsMappings(CorsRegistry registry) {
12+
registry.addMapping("/api/**") // 백엔드 API 요청에만 CORS 허용
13+
.allowedOrigins("http://pirocheck.org:3000") // 프론트 배포 URL
14+
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드
15+
.allowCredentials(true); // 세션 쿠키 주고받기 허용
16+
}
17+
}

backend/pirocheck/src/main/resources/application.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,13 @@ spring:
1111
properties:
1212
hibernate:
1313
format_sql: true
14-
open-in-view: false
14+
open-in-view: false
15+
server:
16+
servlet:
17+
session:
18+
cookie:
19+
http-only: true # 세션 쿠키를 HttpOnly로 설정 (JS에서 접근 불가)
20+
secure: false # HTTPS 전용 전송 (Https -> true로 바꿔야 함)
21+
same-site: Lax # CSRF 방지
22+
timeout: 30m # 세션 타임아웃 30분 (30 minutes)
23+
address: 0.0.0.0
15.9 KB
Loading
16.5 KB
Loading

frontend/src/Attendance.jsx

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import axios from "axios";
88
const Attendance = () => {
99
const [attendanceCode, setAttendanceCode] = useState([""]);
1010
const [attendanceData, setAttendanceData] = useState([]);
11+
const [todayStatuses, setTodayStatuses] = useState([
12+
"not_started",
13+
"not_started",
14+
"not_started",
15+
]);
1116

1217
const getSubImage = (count) => {
1318
switch (count) {
@@ -22,6 +27,18 @@ const Attendance = () => {
2227
}
2328
};
2429

30+
// 세션별 상단 이미지 handling
31+
const getBoomImage = (status) => {
32+
switch (status) {
33+
case "success":
34+
return "/assets/img/boom-fill-green.png";
35+
case "fail":
36+
return "/assets/img/boom-fill-red.png";
37+
default:
38+
return "/assets/img/tabler--boom.png";
39+
}
40+
};
41+
2542
// 날짜 기반 주차 계산
2643
const getWeekFromDate = (dateStr) => {
2744
const startDate = new Date("2025-06-24"); // 세션 시작일
@@ -64,26 +81,57 @@ const Attendance = () => {
6481
});
6582
};
6683

67-
useEffect(() => {
68-
const fetchAttendance = async () => {
69-
try {
70-
const user = JSON.parse(localStorage.getItem("user"));
71-
const userId = user?.id;
72-
73-
if (!userId) return;
74-
75-
const res = await axios.get(`/api/attendance/user`, {
76-
params: { userId },
77-
});
78-
79-
const rawData = res.data.data;
80-
const weekly = processWeeklyAttendance(rawData);
81-
setAttendanceData(weekly);
82-
} catch (error) {
83-
console.error("출석 정보 가져오기 실패:", error);
84+
const fetchAttendance = async () => {
85+
try {
86+
const user = JSON.parse(localStorage.getItem("user"));
87+
const userId = user?.id;
88+
if (!userId) return;
89+
90+
// 유저 전체 출석 데이터 불러오기
91+
const res = await axios.get(`/api/attendance/user`, {
92+
params: { userId },
93+
});
94+
const rawData = res.data.data;
95+
const weekly = processWeeklyAttendance(rawData);
96+
setAttendanceData(weekly);
97+
} catch (error) {
98+
console.error("출석 정보 가져오기 실패:", error);
99+
}
100+
};
101+
102+
// 세션별 출석체크(총 3번) 진행 정보 불러오기
103+
const fetchTodayAttendance = async () => {
104+
try {
105+
const user = JSON.parse(localStorage.getItem("user"));
106+
const userId = user?.id;
107+
if (!userId) return;
108+
109+
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
110+
const res = await axios.get(`/api/attendance/user/date`, {
111+
params: { userId, date: today },
112+
});
113+
114+
const slots = res.data.data?.[0]?.slots || [];
115+
116+
const statuses = slots.map((slot) => {
117+
if (slot.status === true) return "success";
118+
else return "fail";
119+
});
120+
121+
// 출석체크 진행안된 것 처리
122+
while (statuses.length < 3) {
123+
statuses.push("not_started");
84124
}
85-
};
125+
126+
setTodayStatuses(statuses);
127+
} catch (error) {
128+
console.error("오늘 출석 정보 가져오기 실패:", error);
129+
}
130+
};
131+
132+
useEffect(() => {
86133
fetchAttendance();
134+
fetchTodayAttendance();
87135
}, []);
88136

89137
const handleChange = (index, value) => {
@@ -100,7 +148,7 @@ const Attendance = () => {
100148
const userId = user?.id;
101149
if (!userId) return;
102150

103-
// 출석체크 서버에 반영
151+
// 유저가 입력한 출석 코드 서버에 전달(서버에서 출석코드 체크)
104152
const res = await axios.post("/api/attendance/mark", {
105153
userId,
106154
code: attendanceCode[0],
@@ -109,6 +157,7 @@ const Attendance = () => {
109157
if (res.data.success) {
110158
alert("출석이 성공적으로 처리되었습니다!");
111159
fetchAttendance(); // 서버 출석체크 전달 후 UI 반영
160+
fetchTodayAttendance(); // 세션별 상단 이미지 UI 반영
112161
} else {
113162
alert(res.data.message);
114163
}
@@ -137,15 +186,11 @@ const Attendance = () => {
137186
</button>
138187
)}
139188
<div className={styles.attend_img_container}>
140-
<div className={styles.boom_icon}>
141-
<img src="/assets/img/tabler--boom.png" />
142-
</div>
143-
<div className={styles.boom_icon}>
144-
<img src="/assets/img/tabler--boom.png" />
145-
</div>
146-
<div className={styles.boom_icon}>
147-
<img src="/assets/img/tabler--boom.png" />
148-
</div>
189+
{todayStatuses.map((status, idx) => (
190+
<div className={styles.boom_icon} key={idx}>
191+
<img src={getBoomImage(status)} alt={`attendance-${idx}`} />
192+
</div>
193+
))}
149194
</div>
150195
<div className={styles.attend_week_container}>
151196
{attendanceData.map(({ week, classes }) => (

0 commit comments

Comments
 (0)