[카테고리:] 미분류

  • JavaFX로 멋진 로또 번호 생성기 만들기 – 완벽 가이드

    프로젝트 코드

    1. pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
             https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.hajunho</groupId>
        <artifactId>intelliJavaFX</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>19</maven.compiler.source>
            <maven.compiler.target>19</maven.compiler.target>
            <javafx.version>19.0.2</javafx.version>
            <javafx.runtime.path-name>javafx-runtime</javafx.runtime.path-name>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>${javafx.version}</version>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-fxml</artifactId>
                <version>${javafx.version}</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.13.0</version>
                    <configuration>
                        <release>19</release>
                    </configuration>
                </plugin>
                
                <plugin>
                    <groupId>org.openjfx</groupId>
                    <artifactId>javafx-maven-plugin</artifactId>
                    <version>0.0.8</version>
                    <configuration>
                        <mainClass>com.hajunho.intellijavafx.HelloApplication</mainClass>
                    </configuration>
                </plugin>
                
                <!-- JAR 패키징을 위한 maven-shade-plugin -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-shade-plugin</artifactId>
                    <version>3.4.1</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>shade</goal>
                            </goals>
                            <configuration>
                                <transformers>
                                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                        <mainClass>com.hajunho.intellijavafx.Launcher</mainClass>
                                    </transformer>
                                </transformers>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    
        <!-- 네이티브 패키징을 위한 프로필 -->
        <profiles>
            <profile>
                <id>native</id>
                <build>
                    <plugins>
                        <plugin>
                            <groupId>org.openjfx</groupId>
                            <artifactId>javafx-maven-plugin</artifactId>
                            <version>0.0.8</version>
                            <configuration>
                                <mainClass>com.hajunho.intellijavafx.HelloApplication</mainClass>
                                <launcher>lotto</launcher>
                                <jlinkImageName>lotto-app</jlinkImageName>
                                <jlinkZipName>lotto-app-zip</jlinkZipName>
                                <jpackage>
                                    <installerName>LottoGenerator</installerName>
                                    <appName>로또 번호 생성기</appName>
                                    <vendor>HaJunHo</vendor>
                                    <description>애니메이션이 있는 로또 번호 생성기</description>
                                    <copyright>Copyright ©2025 HaJunHo</copyright>
                                </jpackage>
                            </configuration>
                        </plugin>
                    </plugins>
                </build>
            </profile>
        </profiles>
    </project>
    

    2. module-info.java

    module com.hajunho.intellijavafx {
        requires javafx.controls;
        requires javafx.fxml;
    
        opens com.hajunho.intellijavafx to javafx.fxml;
        exports com.hajunho.intellijavafx;
    }
    

    3. HelloApplication.java

    package com.hajunho.intellijavafx;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    import java.io.IOException;
    
    public class HelloApplication extends Application {
        @Override
        public void start(Stage stage) throws IOException {
            FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
            Scene scene = new Scene(fxmlLoader.load(), 800, 600);
            stage.setTitle("로또 번호 생성기");
            stage.setScene(scene);
            stage.setResizable(false);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    4. HelloController.java

    package com.hajunho.intellijavafx;
    
    import javafx.animation.*;
    import javafx.fxml.FXML;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.StackPane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.scene.text.Font;
    import javafx.util.Duration;
    
    import java.util.*;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class HelloController {
        @FXML
        private HBox ballContainer;
        
        @FXML
        private Button generateButton;
        
        @FXML 
        private Label statusLabel;
        
        @FXML
        private HBox lotteryMachine;
        
        private final Map<Integer, Color> ballColors = new HashMap<>();
        private boolean isAnimating = false;
        
        @FXML
        public void initialize() {
            // 색상 초기화
            ballColors.put(1, Color.valueOf("#FFC107")); // 노란색 (1-10)
            ballColors.put(11, Color.valueOf("#4CAF50")); // 초록색 (11-20)
            ballColors.put(21, Color.valueOf("#F44336")); // 빨간색 (21-30)
            ballColors.put(31, Color.valueOf("#2196F3")); // 파란색 (31-40)
            ballColors.put(41, Color.valueOf("#9C27B0")); // 보라색 (41-45)
            
            // 로또 기계에 작은 공들 생성
            for (int i = 0; i < 45; i++) {
                Circle smallBall = new Circle(5);
                smallBall.setFill(getBallColor(i + 1));
                smallBall.setOpacity(0.5);
                lotteryMachine.getChildren().add(smallBall);
            }
            
            // 초기화 애니메이션
            animateLotteryMachine();
        }
        
        private void animateLotteryMachine() {
            List<Circle> balls = new ArrayList<>();
            for (int i = 0; i < lotteryMachine.getChildren().size(); i++) {
                if (lotteryMachine.getChildren().get(i) instanceof Circle) {
                    balls.add((Circle) lotteryMachine.getChildren().get(i));
                }
            }
            
            // 볼들이 로또 기계 안에서 움직이는 애니메이션
            for (Circle ball : balls) {
                double startX = Math.random() * 300 - 150;
                double startY = Math.random() * 50 - 25;
                
                TranslateTransition tt = new TranslateTransition(Duration.seconds(3 + Math.random() * 2), ball);
                tt.setFromX(startX);
                tt.setFromY(startY);
                tt.setToX(startX + (Math.random() * 100 - 50));
                tt.setToY(startY + (Math.random() * 100 - 50));
                tt.setCycleCount(Animation.INDEFINITE);
                tt.setAutoReverse(true);
                tt.play();
            }
        }
        
        private Color getBallColor(int number) {
            if (number <= 10) return ballColors.get(1);
            else if (number <= 20) return ballColors.get(11);
            else if (number <= 30) return ballColors.get(21);
            else if (number <= 40) return ballColors.get(31);
            else return ballColors.get(41);
        }
        
        @FXML
        protected void generateLottoNumbers() {
            if (isAnimating) return;
            isAnimating = true;
            
            // 기존 공 제거
            ballContainer.getChildren().clear();
            
            // 버튼 비활성화
            generateButton.setDisable(true);
            statusLabel.setText("번호 추첨 중...");
            
            // 번호 생성
            Random random = new Random();
            Set<Integer> numbers = new TreeSet<>();
            while (numbers.size() < 6) {
                numbers.add(random.nextInt(45) + 1);
            }
            
            List<Integer> numberList = new ArrayList<>(numbers);
            AtomicInteger index = new AtomicInteger(0);
            
            // 타이머 시작 - 0.8초 간격으로 번호 하나씩 추가
            Timeline timeline = new Timeline();
            
            for (int i = 0; i < 6; i++) {
                KeyFrame keyFrame = new KeyFrame(Duration.seconds(i * 0.8), event -> {
                    int currentNumber = numberList.get(index.get());
                    
                    // 공 만들기
                    StackPane ballPane = createBall(currentNumber);
                    ballContainer.getChildren().add(ballPane);
                    
                    // 회전 애니메이션
                    RotateTransition rotateTransition = new RotateTransition(Duration.millis(500), ballPane);
                    rotateTransition.setByAngle(360);
                    rotateTransition.setCycleCount(1);
                    rotateTransition.play();
                    
                    // 크기 애니메이션
                    ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(500), ballPane);
                    scaleTransition.setFromX(0.1);
                    scaleTransition.setFromY(0.1);
                    scaleTransition.setToX(1.0);
                    scaleTransition.setToY(1.0);
                    scaleTransition.play();
                    
                    // 다음 인덱스로 이동
                    index.incrementAndGet();
                });
                
                timeline.getKeyFrames().add(keyFrame);
            }
            
            // 마지막 번호 표시 후 타이머 종료 및 버튼 활성화
            KeyFrame endFrame = new KeyFrame(Duration.seconds(6 * 0.8), event -> {
                generateButton.setDisable(false);
                statusLabel.setText("추첨 완료!");
                
                // 번호 정렬 애니메이션
                sortBallsAnimation();
            });
            
            timeline.getKeyFrames().add(endFrame);
            timeline.play();
        }
        
        private void sortBallsAnimation() {
            // 모든 공들에게 작은 바운스 애니메이션 적용
            for (int i = 0; i < ballContainer.getChildren().size(); i++) {
                StackPane ballPane = (StackPane) ballContainer.getChildren().get(i);
                
                TranslateTransition bounce = new TranslateTransition(Duration.millis(200), ballPane);
                bounce.setFromY(0);
                bounce.setToY(-20);
                bounce.setCycleCount(2);
                bounce.setAutoReverse(true);
                bounce.setDelay(Duration.millis(i * 100));
                bounce.play();
                
                // 마지막 애니메이션이 끝나면 isAnimating 플래그 해제
                if (i == ballContainer.getChildren().size() - 1) {
                    bounce.setOnFinished(event -> {
                        isAnimating = false;
                    });
                }
            }
        }
        
        private StackPane createBall(int number) {
            // 공 생성
            Circle ball = new Circle(30);
            ball.setFill(getBallColor(number));
            ball.setStroke(Color.WHITE);
            ball.setStrokeWidth(2);
            
            // 그림자 효과
            ball.setEffect(new javafx.scene.effect.DropShadow(10, Color.color(0, 0, 0, 0.5)));
            
            // 숫자 레이블
            Label numberLabel = new Label(String.valueOf(number));
            numberLabel.setFont(Font.font("Arial", 20));
            numberLabel.setTextFill(Color.WHITE);
            numberLabel.setStyle("-fx-font-weight: bold;");
            
            // 스택 팬에 추가
            StackPane ballPane = new StackPane();
            ballPane.getChildren().addAll(ball, numberLabel);
            
            return ballPane;
        }
    }
    

    5. hello-view.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.VBox?>
    <?import javafx.scene.layout.HBox?>
    <?import javafx.scene.layout.BorderPane?>
    <?import javafx.scene.layout.StackPane?>
    <?import javafx.scene.shape.Rectangle?>
    <?import javafx.scene.text.Font?>
    
    <BorderPane prefHeight="600" prefWidth="800" 
                style="-fx-background-color: linear-gradient(to bottom, #1A237E, #3949AB);" 
                xmlns="http://javafx.com/javafx" 
                xmlns:fx="http://javafx.com/fxml"
                fx:controller="com.hajunho.intellijavafx.HelloController">
        
        <top>
            <VBox alignment="CENTER" spacing="20">
                <padding>
                    <Insets top="30" right="30" bottom="10" left="30"/>
                </padding>
                <Label text="행운의 로또 번호 생성기" style="-fx-text-fill: white; -fx-font-size: 36px; -fx-font-weight: bold;">
                    <effect>
                        <javafx.scene.effect.DropShadow radius="10" color="#00000080"/>
                    </effect>
                </Label>
            </VBox>
        </top>
        
        <center>
            <VBox alignment="CENTER" spacing="40">
                <StackPane>
                    <!-- 로또 기계 배경 -->
                    <Rectangle width="700" height="150" arcWidth="50" arcHeight="50" fill="#1E272E" opacity="0.8">
                        <effect>
                            <javafx.scene.effect.DropShadow radius="20" color="#00000080"/>
                        </effect>
                    </Rectangle>
                    
                    <!-- 로또 기계 내부 -->
                    <HBox fx:id="lotteryMachine" alignment="CENTER" maxWidth="650" maxHeight="130" 
                          style="-fx-background-color: rgba(255,255,255,0.1); -fx-background-radius: 20;">
                        <padding>
                            <Insets top="20" right="20" bottom="20" left="20"/>
                        </padding>
                    </HBox>
                </StackPane>
                
                <!-- 볼 컨테이너 -->
                <HBox fx:id="ballContainer" alignment="CENTER" spacing="15" minHeight="100"/>
                
                <Label fx:id="statusLabel" text="행운의 번호를 뽑아보세요!" style="-fx-text-fill: #FFC107; -fx-font-size: 24px;">
                    <font>
                        <Font name="System Bold" size="22.0" />
                    </font>
                </Label>
            </VBox>
        </center>
        
        <bottom>
            <VBox alignment="CENTER" spacing="20">
                <padding>
                    <Insets top="20" right="30" bottom="30" left="30"/>
                </padding>
                <Button fx:id="generateButton" text="🎯 로또 번호 추첨하기" onAction="#generateLottoNumbers"
                        style="-fx-font-size: 22px; -fx-background-color: #FFC107; -fx-text-fill: #1A237E; -fx-background-radius: 30; -fx-padding: 15 30; -fx-font-weight: bold;">
                    <effect>
                        <javafx.scene.effect.DropShadow radius="10" color="#00000080"/>
                    </effect>
                </Button>
            </VBox>
        </bottom>
    </BorderPane>
    

    6. Launcher.java (JAR 실행을 위한)

    package com.hajunho.intellijavafx;
    
    public class Launcher {
        public static void main(String[] args) {
            HelloApplication.main(args);
        }
    }
    

    실행하기

    1. 개발 환경에서 실행

    mvn clean javafx:run
    

    2. JAR 파일 생성

    mvn clean package
    

    3. JAR 파일 실행

    java -jar target/intelliJavaFX-1.0-SNAPSHOT.jar
    

    네이티브 애플리케이션 만들기

    1. JLink로 커스텀 런타임 이미지 생성

    mvn clean javafx:jlink
    

    2. 생성된 애플리케이션 실행

    # macOS/Linux
    target/lotto-app/bin/lotto
    
    # Windows
    target\lotto-app\bin\lotto.bat
    

    3. 네이티브 인스톨러 생성

    mvn clean javafx:jlink javafx:jpackage -Pnative
    

    이 명령은 다음과 같은 네이티브 인스톨러를 생성합니다:

    • Windows: .msi 또는 .exe
    • macOS: .dmg 또는 .pkg
    • Linux: .deb 또는 .rpm

    주요 기능 설명

    1. 로또 기계 애니메이션: 45개의 작은 공들이 로또 기계 내부에서 계속 움직입니다.
    2. 순차적 번호 표시: 번호가 하나씩 순차적으로 나타나며 회전 애니메이션이 적용됩니다.
    3. 색상 구분: 번호 대역별로 다른 색상이 적용됩니다.
    4. 마지막 정렬 애니메이션: 모든 번호가 표시된 후 바운스 효과가 적용됩니다.

    문제 해결

    일반적인 오류와 해결 방법

    1. “module not found” 오류
      • module-info.java 파일이 제대로 설정되었는지 확인
      • 모든 의존성이 pom.xml에 정의되었는지 확인
    2. “FXML LoadException” 오류
      • FXML 파일의 컨트롤러 클래스 경로 확인
      • 모든 @FXML 어노테이션이 올바르게 매핑되었는지 확인
    3. JAR 실행 시 오류
      • Launcher 클래스가 있는지 확인
      • maven-shade-plugin 설정 확인

    결론

    이 가이드를 따라하면 멋진 애니메이션이 포함된 로또 번호 생성기를 만들 수 있습니다. 개발 환경에서의 실행부터 배포 가능한 네이티브 애플리케이션까지 모든 단계를 다루었습니다.

    추가적인 기능을 원한다면 다음을 고려해볼 수 있습니다:

    • 번호 저장 기능
    • 통계 분석
    • 다양한 테마
    • 사운드 효과

    즐거운 코딩되세요! 🎯