From 37140baea1b5e42eff7d5eb5a2df1e35a6c73452 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 15:39:53 +0900 Subject: [PATCH 01/14] =?UTF-8?q?refactor:=20=EC=9B=B9=EC=86=8C=EC=BC=93?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=83=81=20=EC=93=B0=EB=A0=88=EB=93=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/PachinkoWebSocketHandler.java | 137 ++++++++---------- 1 file changed, 62 insertions(+), 75 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java index 35945d6..fe506ab 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java +++ b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java @@ -9,7 +9,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; @@ -24,43 +25,38 @@ public class PachinkoWebSocketHandler extends TextWebSocketHandler { private final PachinkoService pachinkoService; private final UserService userService; private final JwtTokenUtils jwtTokenUtils; - - // Json 데이터를 처리(파싱)하는 객체 private final ObjectMapper objectMapper = new ObjectMapper(); - - // 현재 연결된 모든 WebSocket 세션을 저장하는 리스트 private final List sessions = new ArrayList<>(); + private final ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); @Override public void afterConnectionEstablished(WebSocketSession session) { sessions.add(session); - System.out.println("새로운 사용자 접속"); - System.out.println("session 안의 요소 개수: " + sessions.size()); - for (WebSocketSession webSocketSession : sessions) { - System.out.println(webSocketSession.getId()); - } + logSessionConnected(); } @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { - String payload = message.getPayload(); - JsonNode node = objectMapper.readTree(payload); - - // JWT 토큰이 없는 경우 처리 (첫 번째 메시지에만 JWT 필수) - if (!session.getAttributes().containsKey("user") && node.has("token")) { - String token = node.get("token").asText(); - if (jwtTokenUtils.validateToken(token)) { - String username = jwtTokenUtils.getUsernameFromToken(token); - User user = userService.findByUserName(username); - session.getAttributes().put("user", user); - } else { - sendMessage(session, "JWT 검증 실패하여 연결 종료합니다."); - session.close(); - return; + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + virtualThreadExecutor.submit(() -> { + try { + processIncomingMessage(session, message); + } catch (Exception e) { + e.printStackTrace(); } - } + }); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + sessions.remove(session); + } + + private void processIncomingMessage(WebSocketSession session, TextMessage message) throws IOException { + JsonNode node = objectMapper.readTree(message.getPayload()); - // 칸 선택 처리 (JWT 검증이 완료된 후) + if (!validateJwt(session, node)) { + return; + } if (!node.has("square")) { sendMessage(session, "Invalid message format: 'square' 필드에 값이 없습니다."); return; @@ -68,14 +64,12 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) int selectedSquare = node.get("square").asInt(); User user = (User) session.getAttributes().get("user"); - if (user == null) { sendMessage(session, "유저가 인증되지 않았습니다."); return; } long currentRound = pachinkoService.getCurrentRound(); - if (!validateUserState(session, user, currentRound)) { return; } @@ -83,28 +77,20 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) processSquareSelection(session, user, currentRound, selectedSquare); } - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { - System.out.println("해당 세션 제거"); - sessions.remove(session); - } - - private void processSquareSelection(WebSocketSession session, User user, long currentRound, int selectedSquare) { - System.out.println(pachinkoService.viewSelectedSquares() + "핸들러에서 processSquareSelection 시작지점"); - - String result = pachinkoService.selectSquare(user, currentRound, selectedSquare); - System.out.println(pachinkoService.viewSelectedSquares() + "핸들러에서 processSquareSelection 시작지점"); - - if (Objects.equals(result, "정상적으로 선택 완료되었습니다.")) { - broadcastMessage(user.getNickname() + "가 " + selectedSquare + "을 선택했습니다."); - checkGameStatusAndCloseSessionsIfNeeded(); - } else if (Objects.equals(result, "다른 사용자가 이전에 선택한 칸입니다.")) { - sendMessage(session, selectedSquare + "번째 칸은 이미 다른 사용자에 의해 선택되었습니다."); - } else if (Objects.equals(result, "본인이 이전에 선택한 칸입니다.")) { - sendMessage(session, selectedSquare + "번째 칸은 본인이 이전에 선택한 칸입니다."); - } else { - sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); + private boolean validateJwt(WebSocketSession session, JsonNode node) throws IOException { + if (!session.getAttributes().containsKey("user") && node.has("token")) { + String token = node.get("token").asText(); + if (jwtTokenUtils.validateToken(token)) { + String username = jwtTokenUtils.getUsernameFromToken(token); + User user = userService.findByUserName(username); + session.getAttributes().put("user", user); + } else { + sendMessage(session, "JWT 검증 실패하여 연결 종료합니다."); + session.close(); + return false; + } } + return true; } private boolean validateUserState(WebSocketSession session, User user, long currentRound) { @@ -112,29 +98,31 @@ private boolean validateUserState(WebSocketSession session, User user, long curr sendMessage(session, "칸을 선택할때 필요한 보석이 부족합니다."); return false; } - if (!pachinkoService.canSelectMore(user, currentRound)) { sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); return false; } - return true; } - private void sendMessage(WebSocketSession session, String message) { - try { - session.sendMessage(new TextMessage(message)); - } catch (IOException e) { - e.printStackTrace(); + private void processSquareSelection(WebSocketSession session, User user, long currentRound, int selectedSquare) { + String result = pachinkoService.selectSquare(user, currentRound, selectedSquare); + switch (result) { + case "정상적으로 선택 완료되었습니다." -> { + broadcastMessage(user.getNickname() + "가 " + selectedSquare + "을 선택했습니다."); + checkGameStatusAndCloseSessionsIfNeeded(); + } + case "다른 사용자가 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 이미 다른 사용자에 의해 선택되었습니다."); + case "본인이 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 본인이 이전에 선택한 칸입니다."); + case null, default -> sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); } } private void checkGameStatusAndCloseSessionsIfNeeded() { if (pachinkoService.isGameOver()) { - System.out.println("게임 끝남 확인. 보상 전달 시작"); broadcastMessage("해당 판이 종료되었습니다. 10초 후 새로운 판이 시작됩니다."); - Thread rewardThread = new Thread(() -> { + Thread.startVirtualThread(() -> { try { pachinkoService.giveRewards(); broadcastMessage("보상 전달이 완료되었습니다."); @@ -143,7 +131,7 @@ private void checkGameStatusAndCloseSessionsIfNeeded() { } }); - Thread countdownThread = new Thread(() -> { + new Thread(() -> { try { countdownAndNotifyPlayers(10); startNewRoundAndNotifyPlayers(); @@ -151,10 +139,6 @@ private void checkGameStatusAndCloseSessionsIfNeeded() { e.printStackTrace(); } }); - - // 두 작업을 비동기로 병렬 실행 - rewardThread.start(); - countdownThread.start(); } } @@ -176,16 +160,19 @@ private void broadcastMessage(String message) { } } - /*public void endGameForAll() { - List sessionsCopy = new ArrayList<>(sessions); - for (WebSocketSession session : sessionsCopy) { - sendMessage(session, "해당 게임의 세션을 모두 종료합니다."); - try { - session.close(CloseStatus.NORMAL); - } catch (IOException e) { - e.printStackTrace(); - } + private void logSessionConnected() { + System.out.println("새로운 사용자 접속"); + System.out.println("session 안의 요소 개수: " + sessions.size()); + for (WebSocketSession webSocketSession : sessions) { + System.out.println(webSocketSession.getId()); } - sessions.clear(); - }*/ + } + + private void sendMessage(WebSocketSession session, String message) { + try { + session.sendMessage(new TextMessage(message)); + } catch (IOException e) { + e.printStackTrace(); + } + } } From dc949189f40007475b29a0996fe89c8a6a2b09f2 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 15:41:11 +0900 Subject: [PATCH 02/14] =?UTF-8?q?refactor:=20synchronized=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EB=B3=B4=EA=B8=B0=20(=EC=84=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EC=B9=B8=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=B4=EB=9D=BC=EB=8F=84=20=EB=9D=BD=20?= =?UTF-8?q?=EA=B0=9C=EC=B2=B4=EA=B0=80=20=EA=B0=99=EC=9C=BC=EB=A9=B4=20?= =?UTF-8?q?=EC=A7=81=EB=A0=AC=ED=99=94=EB=90=98=EC=96=B4=20=EC=84=B1?= =?UTF-8?q?=EB=8A=A5=20=EC=A0=80=ED=95=98=20=EB=AC=B8=EC=A0=9C=20=EC=9E=88?= =?UTF-8?q?=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pachinko/service/PachinkoService.java | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index 54a9da5..679f64c 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -139,40 +139,44 @@ public String selectSquare(User user, long currentRound, int squareNumber) { // 1. 칸 번호 유효성 검증 validateSquareNumber(squareNumber); - // 2. 이미 선택된 칸인지 확인 - if (selectedSquares.contains(squareNumber)) { - log.info("{} 이미 {}가 존재합니다.", selectedSquares, squareNumber); - if (isUserSelected(user, currentRound, squareNumber)) { - log.info("본인이 이전에 선택한 칸입니다."); - return "본인이 이전에 선택한 칸입니다."; - } else { - log.info("다른 사용자가 이전에 선택한 칸입니다."); - return "다른 사용자가 이전에 선택한 칸입니다."; + synchronized (this) { + + // 2. 이미 선택된 칸인지 확인 + if (selectedSquares.contains(squareNumber)) { + log.info("{} 이미 {}가 존재합니다.", selectedSquares, squareNumber); + if (isUserSelected(user, currentRound, squareNumber)) { + log.info("본인이 이전에 선택한 칸입니다."); + return "본인이 이전에 선택한 칸입니다."; + } else { + log.info("다른 사용자가 이전에 선택한 칸입니다."); + return "다른 사용자가 이전에 선택한 칸입니다."; + } } - } - // 3. 사용자 Pachinko 상태 조회 및 초기화 - UserPachinko userPachinko = userpachinkoRepository.findByUserAndRoundForUpdate(user, currentRound) - .orElseGet(() -> initializeUserPachinko(user, currentRound)); + // 3. 사용자 Pachinko 상태 조회 및 초기화 + UserPachinko userPachinko = userpachinkoRepository.findByUserAndRoundForUpdate(user, currentRound) + .orElseGet(() -> initializeUserPachinko(user, currentRound)); - // 4. 칸 추가 로직 & 더 이상 선택할 수 없는 경우 처리 - if (!userPachinko.addSquare(squareNumber)) { - log.info("이미 세 칸을 선택하셨습니다."); - return "이미 세 개의 칸을 선택하셨습니다."; - } + // 4. 칸 추가 로직 & 더 이상 선택할 수 없는 경우 처리 + if (!userPachinko.addSquare(squareNumber)) { + log.info("이미 세 칸을 선택하셨습니다."); + return "이미 세 개의 칸을 선택하셨습니다."; + } + + // 7. 선택한 칸을 set에 추가 + addSelectedSquare(squareNumber); + log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - // 5. 사용자 Pachinko 상태 저장 - userpachinkoRepository.save(userPachinko); - log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); + // 5. 사용자 Pachinko 상태 저장 + userpachinkoRepository.save(userPachinko); + log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); + + } // 6. 보석 차감 로직 deductUserJewel(user); log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); - // 7. 선택한 칸을 set에 추가 - addSelectedSquare(squareNumber); - log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - return "정상적으로 선택 완료되었습니다."; } @@ -351,7 +355,7 @@ public List getRewards(User user) { Pachinko pa = pachinkoRepository.findByRoundAndSquare(round, sq) .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); if (pa.getJewelType() == JewelType.S) { - jewelsNum.set(0, jewelsNum.get(0) + pa.getJewelNum()); + jewelsNum.set(0, jewelsNum.getFirst() + pa.getJewelNum()); } else if (pa.getJewelType() == JewelType.A) { jewelsNum.set(1, jewelsNum.get(1) + pa.getJewelNum()); } else if (pa.getJewelType() == JewelType.B) { From 84494245345137fd955c2bcedd8963189c226da4 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 20:55:09 +0900 Subject: [PATCH 03/14] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=BD=98=EC=86=94=EC=97=90=EB=8F=84=20=EB=9C=A8=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 089b8fd..d4c0124 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -12,7 +12,15 @@ + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + From f6c6b18326592e1b28d8689e7610a556e5a14e11 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 20:56:06 +0900 Subject: [PATCH 04/14] =?UTF-8?q?build:=20=EC=9E=90=EB=B0=94=2017=20->=202?= =?UTF-8?q?1=20upgrade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7fb6464..7718342 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ version = '0.0.1-SNAPSHOT' java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } @@ -64,6 +64,11 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.glassfish.tyrus:tyrus-client:2.1.3' + implementation 'org.glassfish.tyrus:tyrus-container-grizzly-client:2.1.3' + implementation 'org.glassfish.tyrus.bundles:tyrus-standalone-client:2.1.3' + implementation 'jakarta.websocket:jakarta.websocket-api:2.1.1' + } configurations.all { From adcf8d851f800bf068d0318b01210d77e9fc6ff6 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 21:00:11 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor:=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=B9=B8=20=EA=B0=9C=EC=88=98?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=9C=A0=EC=97=B0=EC=84=B1?= =?UTF-8?q?=EA=B3=BC=20square=20&=20round=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=9C=A0=EC=9D=BC=EC=84=B1=20=EC=A0=9C=EC=95=BD=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20userPachinkoRepository=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pachinko/converter/PachinkoConverter.java | 6 ++-- .../backend/pachinko/domain/UserPachinko.java | 33 ++----------------- .../repository/UserPachinkoRepository.java | 12 +++---- 3 files changed, 9 insertions(+), 42 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/converter/PachinkoConverter.java b/src/main/java/LuckyVicky/backend/pachinko/converter/PachinkoConverter.java index 568ec46..a1fa9df 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/converter/PachinkoConverter.java +++ b/src/main/java/LuckyVicky/backend/pachinko/converter/PachinkoConverter.java @@ -74,13 +74,11 @@ public static Pachinko savePachinko(Long currentRound, Integer squareNum, JewelT .build(); } - public static UserPachinko saveUserPachinko(Long currentRound, User user) { + public static UserPachinko saveUserPachinko(User user, Long currentRound, Integer squareNumber) { return UserPachinko.builder() .round(currentRound) .user(user) - .square1(0) - .square2(0) - .square3(0) + .square(squareNumber) .build(); } diff --git a/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java b/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java index 3008e70..504ae88 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java +++ b/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java @@ -23,46 +23,19 @@ @AllArgsConstructor @Table( name = "user_pachinko", - uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "round"}) + uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "round", "square"}) ) public class UserPachinko { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Long round; // 게임 번호 + private Long round; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; - private Integer square1; + private Integer square; - private Integer square2; - - private Integer square3; - - public void setSquares(int square1, int square2, int square3) { - this.square1 = square1; - this.square2 = square2; - this.square3 = square3; - } - - public boolean addSquare(int squareNumber) { - if (square1 == null || square1 == 0) { - square1 = squareNumber; - return true; - } else if (square2 == null || square2 == 0) { - square2 = squareNumber; - return true; - } else if (square3 == null || square3 == 0) { - square3 = squareNumber; - return true; - } - return false; - } - - public boolean canSelectMore() { - return square3 == null || square3 == 0; // 세 번째 칸이 비어 있으면 선택 가능 - } } diff --git a/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java b/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java index c7300c9..ef43b53 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java +++ b/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java @@ -2,23 +2,19 @@ import LuckyVicky.backend.pachinko.domain.UserPachinko; import LuckyVicky.backend.user.domain.User; -import jakarta.persistence.LockModeType; import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface UserPachinkoRepository extends JpaRepository { - Optional findByUserAndRound(User user, Long round); + List findByUserAndRound(User user, Long round); - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT u FROM UserPachinko u WHERE u.user = :user AND u.round = :round") - Optional findByUserAndRoundForUpdate(@Param("user") User user, @Param("round") Long round); + long countByUserAndRound(User user, Long round); + + boolean existsByUserAndRoundAndSquare(User user, Long round, Integer square); List findByRound(Long round); From b1791e6e3160c70ecb2553b92ac1963855830c17 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 21:07:13 +0900 Subject: [PATCH 06/14] =?UTF-8?q?refactor:=20=EA=B0=80=EC=83=81=20?= =?UTF-8?q?=EC=93=B0=EB=A0=88=EB=93=9C=EB=A5=BC=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=20=EB=B3=B4=EC=83=81=20=EC=A7=80=EA=B8=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EB=B3=91=EB=A0=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pachinko/service/PachinkoService.java | 295 ++++++++---------- 1 file changed, 134 insertions(+), 161 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index 679f64c..5df263b 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -27,15 +27,17 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @@ -45,6 +47,7 @@ public class PachinkoService { private static final int TOTAL_PACHINKO_SQUARE_COUNT = 36; private static final int MIN_PACHINKO_SQUARE_NUMBER = 1; + public static final int MAX_SQUARES = 3; private static final String REWARD_S1 = "S1"; private static final String REWARD_A1 = "A1"; private static final String REWARD_B2 = "B2"; @@ -54,7 +57,7 @@ public class PachinkoService { private static final int PACHINKO_NEED_JEWEL_COUNT = 1; private final PachinkoRepository pachinkoRepository; - private final UserPachinkoRepository userpachinkoRepository; + private final UserPachinkoRepository userPachinkoRepository; private final PachinkoRewardRepository pachinkoRewardRepository; private final UserJewelRepository userJewelRepository; private final UserRepository userRepository; @@ -62,7 +65,7 @@ public class PachinkoService { private final FcmService fcmService; @Getter - private final Set selectedSquares = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set selectedSquares = ConcurrentHashMap.newKeySet(); public Set viewSelectedSquares() { // 읽기 전용 뷰 반환 return Collections.unmodifiableSet(selectedSquares); @@ -78,27 +81,11 @@ public void startFirstRound() { @Transactional public List getMeChosen(User user) { - UserPachinko userPachinko = userpachinkoRepository.findByUserAndRound(user, currentRound) - .orElse(UserPachinko.builder() - .round(currentRound) - .user(user) - .square1(0) - .square2(0) - .square3(0) - .build()); - if (userPachinko.getSquare1() == 0) { - return Collections.nCopies(3, 0); - } else { - List meChosen = new ArrayList<>(Collections.nCopies(3, 0)); - meChosen.set(0, userPachinko.getSquare1()); - if (userPachinko.getSquare2() != 0) { - meChosen.set(1, userPachinko.getSquare2()); - } - if (userPachinko.getSquare3() != 0) { - meChosen.set(2, userPachinko.getSquare3()); - } - return meChosen; - } + List selected = userPachinkoRepository.findByUserAndRound(user, currentRound); + return selected.stream() + .map(UserPachinko::getSquare) + .sorted() + .collect(Collectors.toList()); } @Transactional @@ -123,78 +110,49 @@ public boolean noMoreJewel(User user) { @Transactional public boolean canSelectMore(User user, Long round) { - // Optional로 조회하여 값이 없으면 true 반환, 있으면 조건에 맞게 처리 - return userpachinkoRepository.findByUserAndRound(user, round) - .map(UserPachinko::canSelectMore) // 존재할 때 조건에 맞게 처리 - .orElse(true); // 존재하지 않으면 true 반환 + return userPachinkoRepository.countByUserAndRound(user, round) < MAX_SQUARES; } @Transactional @Retryable( value = DataIntegrityViolationException.class, - maxAttempts = 3, backoff = @Backoff(delay = 100, multiplier = 2) ) public String selectSquare(User user, long currentRound, int squareNumber) { - // 1. 칸 번호 유효성 검증 + // 칸 번호 유효성 검증 validateSquareNumber(squareNumber); - synchronized (this) { - - // 2. 이미 선택된 칸인지 확인 - if (selectedSquares.contains(squareNumber)) { - log.info("{} 이미 {}가 존재합니다.", selectedSquares, squareNumber); - if (isUserSelected(user, currentRound, squareNumber)) { - log.info("본인이 이전에 선택한 칸입니다."); - return "본인이 이전에 선택한 칸입니다."; - } else { - log.info("다른 사용자가 이전에 선택한 칸입니다."); - return "다른 사용자가 이전에 선택한 칸입니다."; - } - } + // 이미 선택된 칸인지 확인 + if (selectedSquares.contains(squareNumber)) { + log.info("{} 이미 {}가 선택되었습니다.", selectedSquares, squareNumber); - // 3. 사용자 Pachinko 상태 조회 및 초기화 - UserPachinko userPachinko = userpachinkoRepository.findByUserAndRoundForUpdate(user, currentRound) - .orElseGet(() -> initializeUserPachinko(user, currentRound)); + boolean userAlreadySelected = userPachinkoRepository.existsByUserAndRoundAndSquare(user, currentRound, + squareNumber); - // 4. 칸 추가 로직 & 더 이상 선택할 수 없는 경우 처리 - if (!userPachinko.addSquare(squareNumber)) { - log.info("이미 세 칸을 선택하셨습니다."); - return "이미 세 개의 칸을 선택하셨습니다."; + if (userAlreadySelected) { + log.info("본인이 이전에 선택한 칸입니다."); + return "본인이 이전에 선택한 칸입니다."; + } else { + log.info("다른 사용자가 이전에 선택한 칸입니다."); + return "다른 사용자가 이전에 선택한 칸입니다."; } + } - // 7. 선택한 칸을 set에 추가 - addSelectedSquare(squareNumber); - log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - - // 5. 사용자 Pachinko 상태 저장 - userpachinkoRepository.save(userPachinko); - log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); + // 선택한 칸을 set에 추가 + addSelectedSquare(squareNumber); + log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - } + // 사용자 Pachinko 상태 저장 + userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); + log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); - // 6. 보석 차감 로직 + // 보석 차감 deductUserJewel(user); log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); return "정상적으로 선택 완료되었습니다."; } - @Recover - public String recover(DataIntegrityViolationException e, User user, long currentRound, int squareNumber) { - log.warn("재시도 후에도 UserPachinko 엔티티 중복 생성 시도: {}", e.getMessage()); - return "이미 다른 트랜잭션에서 생성된 사용자 데이터입니다."; - } - - private boolean isUserSelected(User user, long currentRound, int squareNumber) { - UserPachinko userPachinko = userpachinkoRepository.findByUserAndRound(user, currentRound) - .orElseGet(() -> initializeUserPachinko(user, currentRound)); - Integer s1 = userPachinko.getSquare1(); - Integer s2 = userPachinko.getSquare2(); - Integer s3 = userPachinko.getSquare3(); - return (squareNumber == s1 || squareNumber == s2 || squareNumber == s3); - } - public synchronized void addSelectedSquare(int square) { selectedSquares.add(square); } @@ -206,11 +164,6 @@ private void deductUserJewel(User user) { userJewelRepository.save(userJewel); } - private UserPachinko initializeUserPachinko(User user, long currentRound) { - UserPachinko newUserPachinko = PachinkoConverter.saveUserPachinko(currentRound, user); - return userpachinkoRepository.save(newUserPachinko); - } - private void validateSquareNumber(int squareNumber) { if (squareNumber < MIN_PACHINKO_SQUARE_NUMBER || squareNumber > TOTAL_PACHINKO_SQUARE_COUNT) { throw new GeneralException(ErrorCode.PACHINKO_OUT_OF_BOUND); @@ -223,52 +176,71 @@ public boolean isGameOver() { } @Transactional - public void giveRewards() throws IOException { + public void giveRewards() { System.out.println("보상 전달 시작"); - List userPachinkoList = userpachinkoRepository.findByRound(currentRound); + List userPachinkoList = userPachinkoRepository.findByRound(currentRound); + + ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); + List> futures = new ArrayList<>(); for (UserPachinko userPachinko : userPachinkoList) { - User user = userPachinko.getUser(); - user.updatePreviousPachinkoRound(currentRound); - userRepository.save(user); - - List squares = new ArrayList<>(); - squares.add(userPachinko.getSquare1()); - squares.add(userPachinko.getSquare2()); - squares.add(userPachinko.getSquare3()); - - for (int i = 0; i < 3; i++) { - if (squares.get(i) > 0) { - int sq = squares.get(i); - // 해당 칸에 대한 보상 알아내기 - System.out.println("보상찾기 - squares.get(i): " + squares.get(i)); - Pachinko pa = pachinkoRepository.findByRoundAndSquare(currentRound, sq) - .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); - - if (pa.getJewelType() != JewelType.F) { - UserJewel uj = userJewelRepository.findByUserAndJewelType(user, pa.getJewelType()) - .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); - uj.setCount(pa.getJewelNum()); - userJewelRepository.save(uj); - System.out.println("user jewel 보상에 따라 갱신완료"); - } - - if (pa.getJewelType() == JewelType.S) { - displayBoardService.addPachinkoSJewelMessage(user); - } - - List userDeviceTokens = user.getDeviceTokenList(); - for (UserDeviceToken userDeviceToken : userDeviceTokens) { - FcmSimpleReqDto requestDTO = FcmConverter.toFcmSimpleReqDto(userDeviceToken.getDeviceToken(), - Constant.FCM_PACHINKO_GAME_FINISH_TITLE, Constant.FCM_PACHINKO_GAME_FINISH_BODY); - fcmService.sendMessageTo(requestDTO); - } + futures.add(virtualThreadExecutor.submit(() -> { + try { + handleReward(userPachinko); + } catch (Exception e) { + log.error("보상 작업 처리 중 예외 발생", e); } + })); + } + + // 모든 작업 완료 대기 + for (Future future : futures) { + try { + future.get(); + } catch (Exception e) { + log.error("보상 작업 처리 중 예외 발생", e); } - System.out.println("보상 전달 완료"); } + + virtualThreadExecutor.shutdown(); + System.out.println("모든 보상 전달 완료"); } + private void handleReward(UserPachinko userPachinko) throws IOException { + User user = userPachinko.getUser(); + user.updatePreviousPachinkoRound(currentRound); + userRepository.save(user); + + int sq = userPachinko.getSquare(); + + Pachinko pa = pachinkoRepository.findByRoundAndSquare(currentRound, sq) + .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); + + if (pa.getJewelType() != JewelType.F) { + UserJewel uj = userJewelRepository.findByUserAndJewelType(user, pa.getJewelType()) + .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); + uj.setCount(pa.getJewelNum()); + userJewelRepository.save(uj); + System.out.println("user jewel 보상에 따라 갱신완료"); + } + + if (pa.getJewelType() == JewelType.S) { + displayBoardService.addPachinkoSJewelMessage(user); + } + + for (UserDeviceToken token : user.getDeviceTokenList()) { + FcmSimpleReqDto dto = FcmConverter.toFcmSimpleReqDto( + token.getDeviceToken(), + Constant.FCM_PACHINKO_GAME_FINISH_TITLE, + Constant.FCM_PACHINKO_GAME_FINISH_BODY + ); + fcmService.sendMessageTo(dto); + } + + System.out.println("보상 전달 완료"); + } + + public void assignRewardsToSquares(Long currentRound) { // 보상 항목들을 리스트에 추가 List rewards = new ArrayList<>(); @@ -309,21 +281,27 @@ public void assignRewardsToSquares(Long currentRound) { for (int i = 0; i < TOTAL_PACHINKO_SQUARE_COUNT; i++) { JewelType jewelType; int jewelNum; - if (Objects.equals(rewards.get(i), REWARD_S1)) { - jewelType = JewelType.S; - jewelNum = 1; - } else if (Objects.equals(rewards.get(i), REWARD_A1)) { - jewelType = JewelType.A; - jewelNum = 1; - } else if (Objects.equals(rewards.get(i), REWARD_B2)) { - jewelType = JewelType.B; - jewelNum = 2; - } else if (Objects.equals(rewards.get(i), REWARD_B1)) { - jewelType = JewelType.B; - jewelNum = 1; - } else { - jewelType = JewelType.F; - jewelNum = 0; + switch (rewards.get(i)) { + case REWARD_S1 -> { + jewelType = JewelType.S; + jewelNum = 1; + } + case REWARD_A1 -> { + jewelType = JewelType.A; + jewelNum = 1; + } + case REWARD_B2 -> { + jewelType = JewelType.B; + jewelNum = 2; + } + case REWARD_B1 -> { + jewelType = JewelType.B; + jewelNum = 1; + } + case null, default -> { + jewelType = JewelType.F; + jewelNum = 0; + } } Pachinko newPachinco = PachinkoConverter.savePachinko(currentRound, i + 1, jewelType, jewelNum); @@ -339,34 +317,31 @@ public List getRewards(User user) { throw new GeneralException(ErrorCode.PACHINKO_NO_PREVIOUS_ROUND); } - UserPachinko userPachinko = userpachinkoRepository.findByUserAndRound(user, round) - .orElseThrow(() -> new GeneralException(ErrorCode.USER_PACHINKO_NOT_FOUND)); - - List squares = new ArrayList<>(); - squares.add(userPachinko.getSquare1()); - squares.add(userPachinko.getSquare2()); - squares.add(userPachinko.getSquare3()); - - List jewelsNum = new ArrayList<>(Collections.nCopies(3, 0L)); - - for (int i = 0; i < 3; i++) { - if (squares.get(i) > 0) { - int sq = squares.get(i); - Pachinko pa = pachinkoRepository.findByRoundAndSquare(round, sq) - .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); - if (pa.getJewelType() == JewelType.S) { - jewelsNum.set(0, jewelsNum.getFirst() + pa.getJewelNum()); - } else if (pa.getJewelType() == JewelType.A) { - jewelsNum.set(1, jewelsNum.get(1) + pa.getJewelNum()); - } else if (pa.getJewelType() == JewelType.B) { - jewelsNum.set(2, jewelsNum.get(2) + pa.getJewelNum()); - } + // 유저가 해당 라운드에 선택한 모든 square 조회 + List selections = userPachinkoRepository.findByUserAndRound(user, round); + if (selections.isEmpty()) { + throw new GeneralException(ErrorCode.USER_PACHINKO_NOT_FOUND); + } + + // A, B, S 보석 수를 담을 리스트 + List jewelsNum = new ArrayList<>(List.of(0L, 0L, 0L)); // [S, A, B] + + for (UserPachinko selection : selections) { + int sq = selection.getSquare(); + Pachinko pa = pachinkoRepository.findByRoundAndSquare(round, sq) + .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); + + switch (pa.getJewelType()) { + case S -> jewelsNum.set(0, jewelsNum.get(0) + pa.getJewelNum()); + case A -> jewelsNum.set(1, jewelsNum.get(1) + pa.getJewelNum()); + case B -> jewelsNum.set(2, jewelsNum.get(2) + pa.getJewelNum()); } } return jewelsNum; } + public List getPreviousPachinkoRewards(Long round) { return pachinkoRepository.findByRound(round); } @@ -375,17 +350,15 @@ public List getPreviousPachinkoRewards(Long round) { public Long updateSelectedSquaresSet() { if (selectedSquares.size() == TOTAL_PACHINKO_SQUARE_COUNT) { - currentRound = userpachinkoRepository.findCurrentRound() + 1; + currentRound = userPachinkoRepository.findCurrentRound() + 1; selectedSquares.clear(); } else { selectedSquares.clear(); - currentRound = userpachinkoRepository.findCurrentRound(); + currentRound = userPachinkoRepository.findCurrentRound(); - List userPachinkoList = userpachinkoRepository.findByRound(currentRound); + List userPachinkoList = userPachinkoRepository.findByRound(currentRound); for (UserPachinko userPachinko : userPachinkoList) { - selectedSquares.add(userPachinko.getSquare1()); - selectedSquares.add(userPachinko.getSquare2()); - selectedSquares.add(userPachinko.getSquare3()); + selectedSquares.add(userPachinko.getSquare()); } selectedSquares.remove(0); } From 86d2d88c4862f6344f51e7c1a5da680cfbf4fe55 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 23:18:58 +0900 Subject: [PATCH 07/14] =?UTF-8?q?refactor:=20=EC=96=B8=EB=A7=88=EC=9A=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=9C=84=ED=95=B4=20=EA=B0=80=EC=83=81=EC=8A=A4?= =?UTF-8?q?=EB=A0=88=EB=93=9C=EB=A1=9C=20=EC=B9=B4=EC=9A=B4=ED=8A=B8?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EC=A7=84=ED=96=89=ED=95=98=EB=A9=B0=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=ED=8C=90=20=EC=A4=80=EB=B9=84.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/PachinkoWebSocketHandler.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java index fe506ab..81cdef8 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java +++ b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java @@ -1,5 +1,7 @@ package LuckyVicky.backend.pachinko.handler; +import static LuckyVicky.backend.pachinko.service.PachinkoService.PACHINKO_USER_MAX_SQUARES; + import LuckyVicky.backend.pachinko.service.PachinkoService; import LuckyVicky.backend.user.domain.User; import LuckyVicky.backend.user.jwt.JwtTokenUtils; @@ -11,13 +13,16 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; +@Slf4j @Component @RequiredArgsConstructor public class PachinkoWebSocketHandler extends TextWebSocketHandler { @@ -28,6 +33,7 @@ public class PachinkoWebSocketHandler extends TextWebSocketHandler { private final ObjectMapper objectMapper = new ObjectMapper(); private final List sessions = new ArrayList<>(); private final ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); + private final Semaphore messageLimiter = new Semaphore(200); // 동시에 200개만 처리 @Override public void afterConnectionEstablished(WebSocketSession session) { @@ -99,7 +105,7 @@ private boolean validateUserState(WebSocketSession session, User user, long curr return false; } if (!pachinkoService.canSelectMore(user, currentRound)) { - sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); + sendMessage(session, "이미 " + PACHINKO_USER_MAX_SQUARES + "칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); return false; } return true; @@ -126,17 +132,19 @@ private void checkGameStatusAndCloseSessionsIfNeeded() { try { pachinkoService.giveRewards(); broadcastMessage("보상 전달이 완료되었습니다."); + pachinkoService.startNewRound(); + log.info("새로운 판 준비가 완료되었습니다."); } catch (Exception e) { - e.printStackTrace(); + log.error("보상 처리 중 예외 발생", e); } }); - new Thread(() -> { + Thread.startVirtualThread(() -> { try { countdownAndNotifyPlayers(10); - startNewRoundAndNotifyPlayers(); + broadcastMessage("새로운 판이 시작됩니다."); } catch (InterruptedException e) { - e.printStackTrace(); + log.error("카운트다운 중 예외 발생", e); } }); } @@ -145,15 +153,10 @@ private void checkGameStatusAndCloseSessionsIfNeeded() { private void countdownAndNotifyPlayers(int seconds) throws InterruptedException { for (int i = seconds; i > 0; i--) { broadcastMessage(i + "초 후에 새로운 게임이 시작됩니다."); - Thread.sleep(1000); + Thread.sleep(1000); // 가상 쓰레드라 문제 없음 } } - private void startNewRoundAndNotifyPlayers() { - broadcastMessage("새로운 판이 시작됩니다."); - pachinkoService.startNewRound(); - } - private void broadcastMessage(String message) { for (WebSocketSession session : sessions) { sendMessage(session, message); From 02415496ca37ecc36abba8131d662fed3e70238a Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 23:36:41 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20UserPachinko=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=8B=9C=20=EC=B6=94=ED=9B=84=20=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EC=97=94=ED=8B=B0=ED=8B=B0=EB=A5=BC=20JOIN=20FETCH=EB=A1=9C?= =?UTF-8?q?=20=EB=AF=B8=EB=A6=AC=20=EB=A1=9C=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/UserPachinkoRepository.java | 9 ++ .../pachinko/service/PachinkoService.java | 87 +++++++++---------- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java b/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java index ef43b53..b6dd497 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java +++ b/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java @@ -5,6 +5,7 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -16,6 +17,14 @@ public interface UserPachinkoRepository extends JpaRepository findByRoundWithUserAndDeviceTokens(@Param("round") Long round); + List findByRound(Long round); @Query("SELECT COALESCE(MAX(u.round), 0) FROM UserPachinko u") diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index 5df263b..6bd26d1 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -22,7 +22,6 @@ import LuckyVicky.backend.user.repository.UserRepository; import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; -import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; @@ -45,9 +44,9 @@ @Service @RequiredArgsConstructor public class PachinkoService { - private static final int TOTAL_PACHINKO_SQUARE_COUNT = 36; - private static final int MIN_PACHINKO_SQUARE_NUMBER = 1; - public static final int MAX_SQUARES = 3; + private static final int PACHINKO_TOTAL_SQUARE_COUNT = 36; + private static final int PACHINKO_MIN_SQUARE_NUMBER = 1; + public static final int PACHINKO_USER_MAX_SQUARES = 10; private static final String REWARD_S1 = "S1"; private static final String REWARD_A1 = "A1"; private static final String REWARD_B2 = "B2"; @@ -110,7 +109,7 @@ public boolean noMoreJewel(User user) { @Transactional public boolean canSelectMore(User user, Long round) { - return userPachinkoRepository.countByUserAndRound(user, round) < MAX_SQUARES; + return userPachinkoRepository.countByUserAndRound(user, round) < PACHINKO_USER_MAX_SQUARES; } @Transactional @@ -165,20 +164,20 @@ private void deductUserJewel(User user) { } private void validateSquareNumber(int squareNumber) { - if (squareNumber < MIN_PACHINKO_SQUARE_NUMBER || squareNumber > TOTAL_PACHINKO_SQUARE_COUNT) { + if (squareNumber < PACHINKO_MIN_SQUARE_NUMBER || squareNumber > PACHINKO_TOTAL_SQUARE_COUNT) { throw new GeneralException(ErrorCode.PACHINKO_OUT_OF_BOUND); } } public boolean isGameOver() { System.out.println("모든 칸 선택 되었나 확인중"); - return (selectedSquares.size() == TOTAL_PACHINKO_SQUARE_COUNT); + return (selectedSquares.size() == PACHINKO_TOTAL_SQUARE_COUNT); } @Transactional public void giveRewards() { System.out.println("보상 전달 시작"); - List userPachinkoList = userPachinkoRepository.findByRound(currentRound); + List userPachinkoList = userPachinkoRepository.findByRoundWithUserAndDeviceTokens(currentRound); ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); List> futures = new ArrayList<>(); @@ -186,7 +185,37 @@ public void giveRewards() { for (UserPachinko userPachinko : userPachinkoList) { futures.add(virtualThreadExecutor.submit(() -> { try { - handleReward(userPachinko); + User user = userPachinko.getUser(); + user.updatePreviousPachinkoRound(currentRound); + userRepository.save(user); + + int sq = userPachinko.getSquare(); + + Pachinko pa = pachinkoRepository.findByRoundAndSquare(currentRound, sq) + .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); + + if (pa.getJewelType() != JewelType.F) { + UserJewel uj = userJewelRepository.findByUserAndJewelType(user, pa.getJewelType()) + .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); + uj.setCount(pa.getJewelNum()); + userJewelRepository.save(uj); + System.out.println("user jewel 보상에 따라 갱신완료"); + } + + if (pa.getJewelType() == JewelType.S) { + displayBoardService.addPachinkoSJewelMessage(user); + } + + for (UserDeviceToken token : user.getDeviceTokenList()) { + FcmSimpleReqDto dto = FcmConverter.toFcmSimpleReqDto( + token.getDeviceToken(), + Constant.FCM_PACHINKO_GAME_FINISH_TITLE, + Constant.FCM_PACHINKO_GAME_FINISH_BODY + ); + fcmService.sendMessageTo(dto); + } + + System.out.println("보상 전달 완료"); } catch (Exception e) { log.error("보상 작업 처리 중 예외 발생", e); } @@ -206,40 +235,6 @@ public void giveRewards() { System.out.println("모든 보상 전달 완료"); } - private void handleReward(UserPachinko userPachinko) throws IOException { - User user = userPachinko.getUser(); - user.updatePreviousPachinkoRound(currentRound); - userRepository.save(user); - - int sq = userPachinko.getSquare(); - - Pachinko pa = pachinkoRepository.findByRoundAndSquare(currentRound, sq) - .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); - - if (pa.getJewelType() != JewelType.F) { - UserJewel uj = userJewelRepository.findByUserAndJewelType(user, pa.getJewelType()) - .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); - uj.setCount(pa.getJewelNum()); - userJewelRepository.save(uj); - System.out.println("user jewel 보상에 따라 갱신완료"); - } - - if (pa.getJewelType() == JewelType.S) { - displayBoardService.addPachinkoSJewelMessage(user); - } - - for (UserDeviceToken token : user.getDeviceTokenList()) { - FcmSimpleReqDto dto = FcmConverter.toFcmSimpleReqDto( - token.getDeviceToken(), - Constant.FCM_PACHINKO_GAME_FINISH_TITLE, - Constant.FCM_PACHINKO_GAME_FINISH_BODY - ); - fcmService.sendMessageTo(dto); - } - - System.out.println("보상 전달 완료"); - } - public void assignRewardsToSquares(Long currentRound) { // 보상 항목들을 리스트에 추가 @@ -255,7 +250,7 @@ public void assignRewardsToSquares(Long currentRound) { .orElseThrow(() -> new GeneralException(ErrorCode.BAD_REQUEST)); int fSquareCount = - TOTAL_PACHINKO_SQUARE_COUNT - (s1.getSquareCount() + a1.getSquareCount() + b2.getSquareCount() + PACHINKO_TOTAL_SQUARE_COUNT - (s1.getSquareCount() + a1.getSquareCount() + b2.getSquareCount() + b1.getSquareCount()); for (int i = 0; i < s1.getSquareCount(); i++) { @@ -278,7 +273,7 @@ public void assignRewardsToSquares(Long currentRound) { Collections.shuffle(rewards, secureRandom); // db에 넣기 - for (int i = 0; i < TOTAL_PACHINKO_SQUARE_COUNT; i++) { + for (int i = 0; i < PACHINKO_TOTAL_SQUARE_COUNT; i++) { JewelType jewelType; int jewelNum; switch (rewards.get(i)) { @@ -349,7 +344,7 @@ public List getPreviousPachinkoRewards(Long round) { @PostConstruct public Long updateSelectedSquaresSet() { - if (selectedSquares.size() == TOTAL_PACHINKO_SQUARE_COUNT) { + if (selectedSquares.size() == PACHINKO_TOTAL_SQUARE_COUNT) { currentRound = userPachinkoRepository.findCurrentRound() + 1; selectedSquares.clear(); } else { From f6f27e11b2f331de215178d7b6b55fdb21d41138 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 11 May 2025 23:40:00 +0900 Subject: [PATCH 09/14] =?UTF-8?q?test:=20=EB=B9=A0=EC=B9=AD=EC=BD=94=20?= =?UTF-8?q?=EB=B6=80=ED=95=98=20=ED=85=8C=EC=8A=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PachinkoController.java | 14 +- .../PachinkoLoadTestWithVerification.java | 150 ++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java diff --git a/src/main/java/LuckyVicky/backend/pachinko/controller/PachinkoController.java b/src/main/java/LuckyVicky/backend/pachinko/controller/PachinkoController.java index f789d5d..2942dfe 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/controller/PachinkoController.java +++ b/src/main/java/LuckyVicky/backend/pachinko/controller/PachinkoController.java @@ -37,7 +37,7 @@ public class PachinkoController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PACHINKO_2001", description = "빠칭코 선택완료 칸 확인 성공"), }) @GetMapping("/chosen-squares") - public ApiResponse SelectedSquares( + public ApiResponse ChosenSquares( @AuthenticationPrincipal CustomUserDetails customUserDetails ) { User user = userService.findByUserName(customUserDetails.getUsername()); @@ -57,6 +57,18 @@ public ApiResponse SelectedSquares( PachinkoConverter.pachinkoChosenResDto(jewelsNumber, currentRound, meChosenSet, chosenSquares)); } + @Operation(summary = "빠칭코 선택된 칸 반환", description = "빠칭코에서 선택 완료된 칸 반환하는 메서드입니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PACHINKO_T_2000", description = "빠칭코 선택완료 칸 확인 성공"), + }) + @GetMapping("/selected-squares") + public ApiResponse> SelectedSquares() { + + Set chosenSquares = pachinkoService.viewSelectedSquares(); + + return ApiResponse.onSuccess(SuccessCode.PACHINKO_GET_SQUARES_SUCCESS, chosenSquares); + } + @Operation(summary = "빠칭코 첫 게임 시작", description = "빠칭코 첫 게임의 각 칸에 대한 보상을 정하는 메서드입니다.") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PACHINKO_2002", description = "빠칭코 첫 게임 시작 성공"), diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java new file mode 100644 index 0000000..b0b1afe --- /dev/null +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java @@ -0,0 +1,150 @@ +package LuckyVicky.backend.pachinko.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.util.Scanner; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.glassfish.tyrus.client.ClientManager; + +public class PachinkoLoadTestWithVerification { + + private static final int USERS_PER_SQUARE = 1000; + private static final int TOTAL_SQUARES = 5; + private static final String TOKEN_URL = "http://localhost:8080/token/generate"; + private static final String WS_URL = "ws://localhost:8080/pachinko"; + private static final String VERIFY_URL = "http://localhost:8080/game/pachinko/selected-squares"; + + private static final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + + public static void main(String[] args) throws Exception { + WebSocketContainer container = ClientManager.createClient(); + //WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + System.out.println("사용 중인 WebSocketContainer 구현체: " + container.getClass().getName()); + + CountDownLatch latch = new CountDownLatch(USERS_PER_SQUARE * TOTAL_SQUARES); + + for (int square = 1; square <= TOTAL_SQUARES; square++) { + final int finalSquare = square; + for (int i = 0; i < USERS_PER_SQUARE; i++) { + final int userIndex = (square - 1) * USERS_PER_SQUARE + i; + executor.submit(() -> { + try { + String token = getTokenForUser(userIndex); + connectAndSendWebSocket(token, finalSquare); + } catch (Exception e) { + System.err.println("에러: " + e.getMessage()); + } finally { + latch.countDown(); + } + }); + } + } + + latch.await(); + executor.shutdown(); + System.out.println("make user finish"); + + // 결과 검증 + Thread.sleep(2000); // 데이터 반영 대기 + verifySelectedSquares(); + } + + private static String getTokenForUser(int userNum) throws Exception { + System.out.println(userNum); + URL url = new URL(TOKEN_URL); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/json"); + con.setDoOutput(true); + + String payload = String.format(""" + { + "email": "testuser%d@example.com", + "username": "user%d", + "provider": "test", + "deviceToken": "test" + } + """, userNum, userNum); + + try (OutputStream os = con.getOutputStream()) { + os.write(payload.getBytes()); + } + + if (con.getResponseCode() == 200) { + try (Scanner scanner = new Scanner(con.getInputStream()).useDelimiter("\\A")) { + String response = scanner.hasNext() ? scanner.next() : ""; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(response); + String token = root.get("result").get("accessToken").asText(); + System.out.println(token); + return token; + + } + } else { + throw new RuntimeException("토큰 요청 실패: " + con.getResponseCode()); + } + } + + private static void connectAndSendWebSocket(String token, int square) throws Exception { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + Session session = container.connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig config) { + try { + String json = String.format(""" + { + "token": "%s", + "square": %d + } + """, token, square); + session.getBasicRemote().sendText(json); + } catch (IOException e) { + e.printStackTrace(); + } + } + }, URI.create(WS_URL)); + Thread.sleep(200); // 메시지 전송 후 약간 대기 + session.close(); + } + + private static void verifySelectedSquares() throws Exception { + URL url = new URL(VERIFY_URL); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + + if (con.getResponseCode() == 200) { + try (Scanner scanner = new Scanner(con.getInputStream()).useDelimiter("\\A")) { + String response = scanner.hasNext() ? scanner.next() : ""; + System.out.println("선택된 칸 결과 확인 응답:"); + System.out.println(response); + + int resultStart = response.indexOf("["); + int resultEnd = response.indexOf("]", resultStart) + 1; + String resultJsonArray = response.substring(resultStart, resultEnd); + String[] squares = resultJsonArray.replaceAll("[\\[\\]\\s]", "").split(","); + + System.out.printf("최종 선택된 칸 개수: %d개%n", squares.length); + if (squares.length == TOTAL_SQUARES) { + System.out.printf("테스트 성공: %d개 칸이 정확히 채워졌습니다.", squares.length); + } else { + System.err.println("테스트 실패: 선택된 칸 수 = " + squares.length); + } + } + } else { + System.err.println(" 확인 API 호출 실패: " + con.getResponseCode()); + } + } +} + From a7906c72e0909c6904ebb5ebf08262431eb39206 Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 12 May 2025 17:04:37 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=20DB=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=97=90=20=EC=84=B1=EA=B3=B5=ED=95=9C=20=ED=9B=84=EC=97=90?= =?UTF-8?q?=EB=A7=8C=20=EC=BA=90=EC=8B=9C=EC=97=90=20=EC=82=BD=EC=9E=85?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/pachinko/service/PachinkoService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index 6bd26d1..c533103 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -137,14 +137,14 @@ public String selectSquare(User user, long currentRound, int squareNumber) { } } - // 선택한 칸을 set에 추가 - addSelectedSquare(squareNumber); - log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - // 사용자 Pachinko 상태 저장 userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); + // 선택한 칸을 set에 추가 + addSelectedSquare(squareNumber); + log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); + // 보석 차감 deductUserJewel(user); log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); From 0a3769f71e8502f5575f05379d203e825cd50ddd Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 12 May 2025 17:08:47 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20save=20=EC=9D=B4=ED=9B=84=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=EB=90=98=EB=8A=94=20=EC=BA=90=EC=8B=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=A1=9C=EC=A7=81=EC=9D=98=20synchronized?= =?UTF-8?q?=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LuckyVicky/backend/pachinko/service/PachinkoService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index c533103..d72236d 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -152,7 +152,7 @@ public String selectSquare(User user, long currentRound, int squareNumber) { return "정상적으로 선택 완료되었습니다."; } - public synchronized void addSelectedSquare(int square) { + public void addSelectedSquare(int square) { selectedSquares.add(square); } From 78ab01167e4a7d2ca5aa80225ad2b6768f21355a Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 12 May 2025 17:09:39 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=9D=BC=EC=84=B1?= =?UTF-8?q?=20=EC=A0=9C=EC=95=BD=20=EC=A1=B0=EA=B1=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?user=5Fid=20=EC=A0=9C=EA=B1=B0=ED=95=98=EC=97=AC=20=EC=B9=B8=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=EC=A4=91=EB=B3=B5=EB=A7=8C=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/LuckyVicky/backend/pachinko/domain/UserPachinko.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java b/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java index 504ae88..c562591 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java +++ b/src/main/java/LuckyVicky/backend/pachinko/domain/UserPachinko.java @@ -23,7 +23,7 @@ @AllArgsConstructor @Table( name = "user_pachinko", - uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "round", "square"}) + uniqueConstraints = @UniqueConstraint(columnNames = {"round", "square"}) ) public class UserPachinko { @Id From dc972a11b7c2c0b07c895a4651aa5062952cc890 Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 12 May 2025 17:28:37 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20=EC=9E=AC=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=ED=9A=9F=EC=88=98=203=EB=B2=88(default)=EA=B9=8C=EC=A7=80?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=20=EC=97=86=EC=96=B4=EC=84=9C=202?= =?UTF-8?q?=EB=B2=88=EC=9C=BC=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LuckyVicky/backend/pachinko/service/PachinkoService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index d72236d..b1c204d 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -115,6 +115,7 @@ public boolean canSelectMore(User user, Long round) { @Transactional @Retryable( value = DataIntegrityViolationException.class, + maxAttempts = 2, backoff = @Backoff(delay = 100, multiplier = 2) ) public String selectSquare(User user, long currentRound, int squareNumber) { From 9a10a2cc605060569d1e3ddfa33aa134a2859fd3 Mon Sep 17 00:00:00 2001 From: persi Date: Thu, 24 Jul 2025 10:47:06 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=96=B4=ED=81=AC?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev_deploy.yml | 138 +++++++++++++++---------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml index 92ac213..6943b01 100644 --- a/.github/workflows/dev_deploy.yml +++ b/.github/workflows/dev_deploy.yml @@ -1,69 +1,69 @@ -name: LuckyVicky CI/CD - -on: - pull_request: - types: - - closed - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop' - - steps: - # 1. GitHub 저장소에서 코드 체크아웃 - - name: Checkout - uses: actions/checkout@v4 - - # 2. JDK 17 설정 - - name: Set up JDK 17 - uses: actions/setup-java@v4.0.0 - with: - java-version: '17' - distribution: 'adopt' - - # 3. gradlew 파일 실행 권한 추가 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - shell: bash - - # 4. Gradle 빌드 (테스트는 제외) - - name: Build with Gradle - run: ./gradlew clean build -x test - shell: bash - - # 5. 현재 시간 가져오기 (배포 버전 레이블에 사용) - - name: Get current time - uses: josStorer/get-current-time@v2 - id: current-time - with: - format: 'YYYY-MM-DDTHH:mm:ss' - utcOffset: '+09:00' - - - name: Show current time - run: echo "${{ steps.current-time.outputs.formattedTime }}" - shell: bash - - # 6. 배포 패키지 생성 - - name: Generate deployment package - run: | - mkdir -p deploy - cp build/libs/*.jar deploy/application.jar - cp Procfile deploy/Procfile - cp -r .ebextensions_dev deploy/.ebextensions - cp -r .platform deploy/.platform - cd deploy && zip -r deploy.zip . - - # 7. Elastic Beanstalk으로 배포 - - name: Deploy to Elastic Beanstalk - uses: einaregilsson/beanstalk-deploy@v20 - with: - aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} - aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} - application_name: 'persi-dev' - environment_name: 'Persi-dev-env' - version_label: github-action-${{ steps.current-time.outputs.formattedTime }} - region: 'ap-northeast-2' - deployment_package: 'deploy/deploy.zip' - wait_for_deployment: false \ No newline at end of file +#name: LuckyVicky CI/CD +# +#on: +# pull_request: +# types: +# - closed +# workflow_dispatch: +# +#jobs: +# build: +# runs-on: ubuntu-latest +# if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop' +# +# steps: +# # 1. GitHub 저장소에서 코드 체크아웃 +# - name: Checkout +# uses: actions/checkout@v4 +# +# # 2. JDK 17 설정 +# - name: Set up JDK 17 +# uses: actions/setup-java@v4.0.0 +# with: +# java-version: '17' +# distribution: 'adopt' +# +# # 3. gradlew 파일 실행 권한 추가 +# - name: Grant execute permission for gradlew +# run: chmod +x gradlew +# shell: bash +# +# # 4. Gradle 빌드 (테스트는 제외) +# - name: Build with Gradle +# run: ./gradlew clean build -x test +# shell: bash +# +# # 5. 현재 시간 가져오기 (배포 버전 레이블에 사용) +# - name: Get current time +# uses: josStorer/get-current-time@v2 +# id: current-time +# with: +# format: 'YYYY-MM-DDTHH:mm:ss' +# utcOffset: '+09:00' +# +# - name: Show current time +# run: echo "${{ steps.current-time.outputs.formattedTime }}" +# shell: bash +# +# # 6. 배포 패키지 생성 +# - name: Generate deployment package +# run: | +# mkdir -p deploy +# cp build/libs/*.jar deploy/application.jar +# cp Procfile deploy/Procfile +# cp -r .ebextensions_dev deploy/.ebextensions +# cp -r .platform deploy/.platform +# cd deploy && zip -r deploy.zip . +# +# # 7. Elastic Beanstalk으로 배포 +# - name: Deploy to Elastic Beanstalk +# uses: einaregilsson/beanstalk-deploy@v20 +# with: +# aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} +# aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} +# application_name: 'persi-dev' +# environment_name: 'Persi-dev-env' +# version_label: github-action-${{ steps.current-time.outputs.formattedTime }} +# region: 'ap-northeast-2' +# deployment_package: 'deploy/deploy.zip' +# wait_for_deployment: false \ No newline at end of file