Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@RestControllerAdvice
public class GlobalExceptionHandler {

// InvalidLoginException (로그인 실패)
@ExceptionHandler(InvalidLoginException.class)
public ResponseEntity<ApiResponse<?>> handleInvalidLoginException(InvalidLoginException e) {
return ResponseEntity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package backend.pirocheck.User.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class SessionCheckFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {

String path = request.getRequestURI();

// 로그인/로그아웃 요청은 세션 체크 제외
if (path.startsWith("/api/login") || path.startsWith("/api/logout")) {
filterChain.doFilter(request, response); // 다음 필터나 컨트롤러로 넘기는 명령어
return; // 세션 검사 안함
}

HttpSession session = request.getSession(false); // 세션이 없으면 새로 만들지 않고 null을 리턴 (true : 새로 생성)

if (session == null || session.getAttribute("loginUser") == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 설정
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"success\":false,\"message\":\"세션이 만료되었습니다.\",\"data\":null}");
return;
}

filterChain.doFilter(request, response);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ public class UserService {

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

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

return user;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package backend.pirocheck.config;

import backend.pirocheck.User.filter.SessionCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SessionCheckFilterConfig {

@Bean
public FilterRegistrationBean<SessionCheckFilter> sessionCheckFilter() {
FilterRegistrationBean<SessionCheckFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SessionCheckFilter());
registrationBean.addUrlPatterns("/api/*");
registrationBean.setOrder(1);
return registrationBean;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package backend.pirocheck.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 백엔드 API 요청에만 CORS 허용
.allowedOrigins("http://pirocheck.org:3000") // 프론트 배포 URL
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드
.allowCredentials(true); // 세션 쿠키 주고받기 허용
}
}
11 changes: 10 additions & 1 deletion backend/pirocheck/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ spring:
properties:
hibernate:
format_sql: true
open-in-view: false
open-in-view: false
server:
servlet:
session:
cookie:
http-only: true # 세션 쿠키를 HttpOnly로 설정 (JS에서 접근 불가)
secure: false # HTTPS 전용 전송 (Https -> true로 바꿔야 함)
same-site: Lax # CSRF 방지
timeout: 30m # 세션 타임아웃 30분 (30 minutes)
address: 0.0.0.0
Binary file added frontend/public/assets/img/boom-fill-green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/img/boom-fill-red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 73 additions & 28 deletions frontend/src/Attendance.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import axios from "axios";
const Attendance = () => {
const [attendanceCode, setAttendanceCode] = useState([""]);
const [attendanceData, setAttendanceData] = useState([]);
const [todayStatuses, setTodayStatuses] = useState([
"not_started",
"not_started",
"not_started",
]);

const getSubImage = (count) => {
switch (count) {
Expand All @@ -22,6 +27,18 @@ const Attendance = () => {
}
};

// 세션별 상단 이미지 handling
const getBoomImage = (status) => {
switch (status) {
case "success":
return "/assets/img/boom-fill-green.png";
case "fail":
return "/assets/img/boom-fill-red.png";
default:
return "/assets/img/tabler--boom.png";
}
};

// 날짜 기반 주차 계산
const getWeekFromDate = (dateStr) => {
const startDate = new Date("2025-06-24"); // 세션 시작일
Expand Down Expand Up @@ -64,26 +81,57 @@ const Attendance = () => {
});
};

useEffect(() => {
const fetchAttendance = async () => {
try {
const user = JSON.parse(localStorage.getItem("user"));
const userId = user?.id;

if (!userId) return;

const res = await axios.get(`/api/attendance/user`, {
params: { userId },
});

const rawData = res.data.data;
const weekly = processWeeklyAttendance(rawData);
setAttendanceData(weekly);
} catch (error) {
console.error("출석 정보 가져오기 실패:", error);
const fetchAttendance = async () => {
try {
const user = JSON.parse(localStorage.getItem("user"));
const userId = user?.id;
if (!userId) return;

// 유저 전체 출석 데이터 불러오기
const res = await axios.get(`/api/attendance/user`, {
params: { userId },
});
const rawData = res.data.data;
const weekly = processWeeklyAttendance(rawData);
setAttendanceData(weekly);
} catch (error) {
console.error("출석 정보 가져오기 실패:", error);
}
};

// 세션별 출석체크(총 3번) 진행 정보 불러오기
const fetchTodayAttendance = async () => {
try {
const user = JSON.parse(localStorage.getItem("user"));
const userId = user?.id;
if (!userId) return;

const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const res = await axios.get(`/api/attendance/user/date`, {
params: { userId, date: today },
});

const slots = res.data.data?.[0]?.slots || [];

const statuses = slots.map((slot) => {
if (slot.status === true) return "success";
else return "fail";
});

// 출석체크 진행안된 것 처리
while (statuses.length < 3) {
statuses.push("not_started");
}
};

setTodayStatuses(statuses);
} catch (error) {
console.error("오늘 출석 정보 가져오기 실패:", error);
}
};

useEffect(() => {
fetchAttendance();
fetchTodayAttendance();
}, []);

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

// 출석체크 서버에 반영
// 유저가 입력한 출석 코드 서버에 전달(서버에서 출석코드 체크)
const res = await axios.post("/api/attendance/mark", {
userId,
code: attendanceCode[0],
Expand All @@ -109,6 +157,7 @@ const Attendance = () => {
if (res.data.success) {
alert("출석이 성공적으로 처리되었습니다!");
fetchAttendance(); // 서버 출석체크 전달 후 UI 반영
fetchTodayAttendance(); // 세션별 상단 이미지 UI 반영
} else {
alert(res.data.message);
}
Expand Down Expand Up @@ -137,15 +186,11 @@ const Attendance = () => {
</button>
)}
<div className={styles.attend_img_container}>
<div className={styles.boom_icon}>
<img src="/assets/img/tabler--boom.png" />
</div>
<div className={styles.boom_icon}>
<img src="/assets/img/tabler--boom.png" />
</div>
<div className={styles.boom_icon}>
<img src="/assets/img/tabler--boom.png" />
</div>
{todayStatuses.map((status, idx) => (
<div className={styles.boom_icon} key={idx}>
<img src={getBoomImage(status)} alt={`attendance-${idx}`} />
</div>
))}
</div>
<div className={styles.attend_week_container}>
{attendanceData.map(({ week, classes }) => (
Expand Down