Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2dd9e30
init: 프ë¡로젝트 초기 ì설정
CheatIsKey Apr 10, 2026
e9eb01e
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 10, 2026
ff6f98c
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 10, 2026
b77a2f2
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 11, 2026
b3a33ee
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 12, 2026
08bc5c8
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 13, 2026
2c02bbe
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 14, 2026
21755ca
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 15, 2026
12be03f
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 15, 2026
33edb5d
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 15, 2026
134f2a3
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 15, 2026
5b25401
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 16, 2026
5199462
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 16, 2026
0f586e4
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 17, 2026
ba879ef
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 17, 2026
8a95286
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 17, 2026
483bc52
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 17, 2026
e232379
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 19, 2026
67a3f77
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 20, 2026
dbbb767
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 20, 2026
2de4325
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 21, 2026
af7712f
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 21, 2026
7b8ffa0
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 21, 2026
b16d03c
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 21, 2026
aa34b0e
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 21, 2026
016e9a2
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 21, 2026
9409ce8
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 22, 2026
dccbc69
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 23, 2026
6337b02
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 23, 2026
55ca628
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 23, 2026
983d756
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 23, 2026
7131980
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 23, 2026
e9b8173
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 24, 2026
f2af60f
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 24, 2026
a8a68b6
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 24, 2026
238b86f
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 24, 2026
f601aa4
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 24, 2026
4707d98
Merge branch 'develop' of https://github.com/Allday-Project/Allday-Pr…
CheatIsKey Apr 25, 2026
d058f4a
feat: Index 적용 및 N+1문ì  œ 해결
CheatIsKey Apr 26, 2026
b450928
Refactor: 프론트 엔드 구현 및 여러 기능 수정
CheatIsKey Apr 27, 2026
52eb605
fix
CheatIsKey Apr 27, 2026
9b85c20
fix
CheatIsKey Apr 27, 2026
be59de1
fix
CheatIsKey Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ public void run(String... args) {
return;
}

// 기존 더미데이터 3개 + 티켓
create("볼캡", 36000L, 100, "발렌타인 코듀로이 볼캡", ProductStatus.ON_SALE, Category.MERCHANDISE, "cap.png");
// 기존 더미데이터 3개 + 이벤트
create("볼캡", 36000L, 100, "발렌타인 코듀로이 볼캡", ProductStatus.ON_SALE, Category.MERCH, "cap.png");
create("바이닐", 70000L, 50, "한정판 LP", ProductStatus.ON_SALE, Category.ALBUM,"vinyl.png");
create("슬로건", 15000L, 200, "공식 슬로건", ProductStatus.SOLD_OUT, Category.MERCHANDISE,"slogan.png");
create("티켓", 0L, 100, "티켓 구매권", ProductStatus.ON_SALE, Category.TICKET,"ticket.png");
create("슬로건", 15000L, 200, "공식 슬로건", ProductStatus.SOLD_OUT, Category.MERCH,"slogan.png");
create("이벤트 티켓", 0L, 100, "이벤트 티켓 구매권", ProductStatus.ON_SALE, Category.EVENT,"ticket.png");

// 더미데이터 50,000건 추가
Category[] categories = Category.values();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package jpa.basic.alldayprojectcommerce.common.config;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jpa.basic.alldayprojectcommerce.common.security.auth.AuthConstants;
import jpa.basic.alldayprojectcommerce.common.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtHandshakeInterceptor implements HandshakeInterceptor {

private final JwtTokenProvider jwtTokenProvider;

@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest servletRequest) {
HttpServletRequest req = servletRequest.getServletRequest();
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (AuthConstants.ACCESS_TOKEN.equals(cookie.getName())) {
String token = cookie.getValue();
if (jwtTokenProvider.validateToken(token)) {
attributes.put("jwt_token", token);
}
break;
}
}
}
}
return true;
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,19 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
return message;
}

// Authorization 헤더에서 Bearer 토큰 추출
// Authorization 헤더 및 Session Attributes에서 Bearer 토큰 추출
private String extractToken(StompHeaderAccessor accessor) {
String authHeader = accessor.getFirstNativeHeader("Authorization");

if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}

java.util.Map<String, Object> sessionAttributes = accessor.getSessionAttributes();
if (sessionAttributes != null && sessionAttributes.containsKey("jwt_token")) {
return (String) sessionAttributes.get("jwt_token");
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final StompChannelInterceptor stompChannelInterceptor;
private final JwtHandshakeInterceptor jwtHandshakeInterceptor;

@Value("${websocket.allowed-origins}")
private String allowedOrigins;
Expand Down Expand Up @@ -43,6 +44,7 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-chat")
.setAllowedOriginPatterns(allowedOrigins.split(",")) // 운영할 때는 도메인 지정
.addInterceptors(jwtHandshakeInterceptor)
.withSockJS(); // WebSocket 미지원 브라우저 fallback 처리
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package jpa.basic.alldayprojectcommerce.common.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jpa.basic.alldayprojectcommerce.common.exception.CustomException;
import jpa.basic.alldayprojectcommerce.common.exception.ErrorCode;
import jpa.basic.alldayprojectcommerce.common.security.auth.LoginUserInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
@RequiredArgsConstructor
public class AdminInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || "anonymousUser".equals(authentication.getPrincipal())) {
response.sendRedirect("/login");
return false;
}

LoginUserInfo userInfo = (LoginUserInfo) authentication.getPrincipal();

if (!"ADMIN".equals(userInfo.role())) {
if (request.getRequestURI().startsWith("/api/")) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
return false;
}
response.sendRedirect("/");
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package jpa.basic.alldayprojectcommerce.common.interceptor;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jpa.basic.alldayprojectcommerce.common.security.auth.LoginUserInfo;
import jpa.basic.alldayprojectcommerce.common.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import java.util.Arrays;

/**
* JWT 쿠키에서 인증 정보를 추출하여 Thymeleaf 모델에 주입하는 인터셉터.
* STATELESS 세션 환경에서 ${user} 모델 어트리뷰트로 로그인 상태를 전달합니다.
* API 스펙에는 영향을 주지 않습니다.
*/
@Component
@RequiredArgsConstructor
public class AuthModelInterceptor implements HandlerInterceptor {

private final JwtTokenProvider jwtTokenProvider;

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {

// API 요청이거나 ModelAndView가 없으면 스킵
if (modelAndView == null || request.getRequestURI().startsWith("/api/")) {
return;
}

String token = resolveToken(request);

if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
Long userId = jwtTokenProvider.getMemberId(token);
String role = jwtTokenProvider.getRole(token);

LoginUserInfo userInfo = LoginUserInfo.builder()
.id(userId)
.role(role)
.build();

modelAndView.addObject("user", userInfo);
}
}

private String resolveToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) return null;

return Arrays.stream(cookies)
.filter(cookie -> "access_token".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jpa.basic.alldayprojectcommerce.domain.user.service.UserQueryService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -21,6 +26,13 @@
public class AuthController {

private final AuthService authService;
private final UserQueryService userQueryService;

@GetMapping("/check-duplicate")
public ResponseEntity<ApiResponse<Boolean>> checkDuplicate(@RequestParam String email) {
boolean duplicate = userQueryService.getByEmail(email).isPresent();
return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK, duplicate));
}

@PostMapping("/signup")
public ResponseEntity<ApiResponse<Void>> signup(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/", "/login", "/signup",
"/cart", "/mypage", "/orders", "/orders/**", "/checkout",
"/css/**", "/js/**", "/images/**",
"/assets/**", "/img/**", "/error", "/favicon.ico",
"/h2-console/**"
Expand All @@ -50,7 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers(
"/actuator/health",
"/api/events/**",
"/ws-chat/**"
"/ws/**", "/ws-chat/**"
).permitAll()
.requestMatchers(
"/api/chat/admin/**"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package jpa.basic.alldayprojectcommerce.common.security.config;

import jpa.basic.alldayprojectcommerce.common.interceptor.AuthModelInterceptor;
import jpa.basic.alldayprojectcommerce.common.interceptor.AdminInterceptor;
import jpa.basic.alldayprojectcommerce.common.security.LoginUserArgumentResolver;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;
Expand All @@ -14,10 +17,21 @@
public class WebMvcConfig implements WebMvcConfigurer {

private final LoginUserArgumentResolver loginUserArgumentResolver;
// 수정 포인트: 다른 ArgumentResolver가 있으면 여기에 함께 추가
private final AuthModelInterceptor authModelInterceptor;
private final AdminInterceptor adminInterceptor;

@Override
public void addArgumentResolvers(@NonNull List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
}

@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
registry.addInterceptor(authModelInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/api/**", "/css/**", "/js/**", "/images/**", "/favicon.ico");

registry.addInterceptor(adminInterceptor)
.addPathPatterns("/admin/**");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
public record ChatRoomResponse(
Long id,
Long userId,
String userEmail,
String title,
ChatRoomStatus chatRoomStatus,
LocalDateTime lastMessageAt,
Expand All @@ -18,6 +19,19 @@ public static ChatRoomResponse from(ChatRoom chatRoom) {
return new ChatRoomResponse(
chatRoom.getId(),
chatRoom.getUserId(),
null,
chatRoom.getTitle(),
chatRoom.getChatRoomStatus(),
chatRoom.getLastMessageAt(),
chatRoom.getCreatedAt()
);
}

public static ChatRoomResponse from(ChatRoom chatRoom, String userEmail) {
return new ChatRoomResponse(
chatRoom.getId(),
chatRoom.getUserId(),
userEmail,
chatRoom.getTitle(),
chatRoom.getChatRoomStatus(),
chatRoom.getLastMessageAt(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public void onMessage(Message message, @Nullable byte[] pattern) {
// 구독자 전체에게 브로드캐스트
simpMessagingTemplate.convertAndSend("/sub/chat/" + roomId, response);

// 관리자 상담 목록 페이지에도 업데이트 알림 전송
simpMessagingTemplate.convertAndSend("/sub/admin/consultations", response);

log.debug("[Redis Sub] roomId: {}, messageId: {}", roomId, response.id());

} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.List;

import static jpa.basic.alldayprojectcommerce.domain.chat.entity.QChatRoom.chatRoom;
import static jpa.basic.alldayprojectcommerce.domain.user.entity.QUser.user;

@Repository
@RequiredArgsConstructor
Expand All @@ -39,12 +40,14 @@ public Page<ChatRoomResponse> findAllWithFilter(ChatRoomStatus status, Pageable
ChatRoomResponse.class,
chatRoom.id,
chatRoom.userId,
user.email,
chatRoom.title,
chatRoom.chatRoomStatus,
chatRoom.lastMessageAt,
chatRoom.createdAt
))
.from(chatRoom)
.leftJoin(user).on(user.id.eq(chatRoom.userId))
.where(statusEq(status))
.orderBy(chatRoom.createdAt.desc())
.offset(pageable.getOffset())
Expand Down Expand Up @@ -84,7 +87,13 @@ public void bulkCompleteRooms(List<Long> roomIds) {
.update(chatRoom)
.set(chatRoom.chatRoomStatus, ChatRoomStatus.COMPLETED)
.setNull(chatRoom.activeFlag)
.where(chatRoom.id.in(roomIds))
.where(
chatRoom.id.in(roomIds),
chatRoom.chatRoomStatus.in(
ChatRoomStatus.WAITING,
ChatRoomStatus.IN_PROGRESS
)
)
.execute();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import jpa.basic.alldayprojectcommerce.common.lock.repository.RedisLockRepository;
import jpa.basic.alldayprojectcommerce.domain.chat.dto.response.ChatMessageResponse;
import jpa.basic.alldayprojectcommerce.domain.chat.entity.ChatMessage;
import jpa.basic.alldayprojectcommerce.domain.chat.entity.ChatRoom;
import jpa.basic.alldayprojectcommerce.domain.chat.entity.MessageType;
import jpa.basic.alldayprojectcommerce.domain.chat.entity.SenderType;
Expand Down Expand Up @@ -85,18 +84,9 @@ public void closeInactiveRooms() {
.collect(Collectors.toList());

/**
* Bulk Update
*
* 분산락이 스케쥴러 진입 자체를 1대만 허용하므로
* 비관적 락 없이 Bulk Update 사용
* 상태 변경 + 메시지 저장을 하나의 트랜잭션으로 처리
*/
chatRoomRepository.bulkCompleteRooms(roomIds);

List<ChatMessage> messages = roomIds.stream()
.map(id -> ChatMessage.systemMessage(id, buildCloseMessage()))
.collect(Collectors.toList());

chatMessageRepository.saveAll(messages);
chatRoomCommandService.batchAutoCloseRooms(roomIds, buildCloseMessage());

for (Long roomId : roomIds) {
try {
Expand Down
Loading
Loading