프로젝트 코드
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
주요 기능 설명
- 로또 기계 애니메이션: 45개의 작은 공들이 로또 기계 내부에서 계속 움직입니다.
- 순차적 번호 표시: 번호가 하나씩 순차적으로 나타나며 회전 애니메이션이 적용됩니다.
- 색상 구분: 번호 대역별로 다른 색상이 적용됩니다.
- 마지막 정렬 애니메이션: 모든 번호가 표시된 후 바운스 효과가 적용됩니다.
문제 해결
일반적인 오류와 해결 방법
- “module not found” 오류
- module-info.java 파일이 제대로 설정되었는지 확인
- 모든 의존성이 pom.xml에 정의되었는지 확인
- “FXML LoadException” 오류
- FXML 파일의 컨트롤러 클래스 경로 확인
- 모든 @FXML 어노테이션이 올바르게 매핑되었는지 확인
- JAR 실행 시 오류
- Launcher 클래스가 있는지 확인
- maven-shade-plugin 설정 확인
결론
이 가이드를 따라하면 멋진 애니메이션이 포함된 로또 번호 생성기를 만들 수 있습니다. 개발 환경에서의 실행부터 배포 가능한 네이티브 애플리케이션까지 모든 단계를 다루었습니다.
추가적인 기능을 원한다면 다음을 고려해볼 수 있습니다:
- 번호 저장 기능
- 통계 분석
- 다양한 테마
- 사운드 효과
즐거운 코딩되세요! 🎯
답글 남기기