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 설정 확인

결론

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

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

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

즐거운 코딩되세요! 🎯

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다