javaFX maven 프로젝트를 빌드하면 Target 폴더에 jar파일이 생성되게 된다. 동일한 폴더에 jre와 javafx-sdk를 복사해 넣으면 작동 환경은 구성된 것이다. 그런데 이렇게 해도 잘 안된다. 그냥 javafx-sdk에 있는 lib, bin을 jre폴더에 추가 복사해넣는게 제일 낫다.
4. pom.xml
우선 jar파일이 정상적으로 실행이 되어야한다. 그런데 실행하다보면 MainClass가 빠졌다는 등의 오류가 발생하는 경우가 있다. 이런경우 maven-shade-plugin을 사용해 Fat JAR을 생성한다. 아래 플러그인 부분에서 Fat JAR만들기 부분 참조
<plugins><!-- 컴파일러 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version><configuration><source>17</source><target>17</target></configuration></plugin><!-- ✅ Fat JAR (실행 가능한 JAR) 만들기 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.5.0</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformerimplementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>com.example.javafx05.HelloApplication</mainClass></transformer></transformers></configuration></execution></executions></plugin></plugins>
실행은 우측의 Maven아이콘 클릭 후, 수명주기 -> package를 더블 클릭하면 패키징이 되면서 jar파일이 생성된다.
C# WPF로 작업하던 프로그램이 오류를 뱉는데, 디버깅이 안되어 Java로 전부 바꾸는 실험을 했다. WPF는 Visual Studio Code에서 만든거라, 디버깅이 좀 힘들었다. 반면에 Java는 무료이면서도 강력한 IntelliJ가 있다. 물론 이클립스도 있다.
이번엔 IntelliJ로 프로젝트를 만들었는데, 사실 그것도 처음이라 좀 많이 힘들었다.
이번 프로젝트는 출입 통제하는 프로그램이다. 자료실에 드나드는 사람에게 ID 태그를 읽혀서 들어올땐 "안녕하세요", 나갈땐 "안녕히가세요"를 출력해주고, 다른 탭에서 전체 출입인원 목록을 확인하면 된다.
프로젝트 구성은 아래와 같다.
Spring Boot를 해본 사람이라면 좀 익숙할 수도 있겠다. 그것과 비슷한 구조로 만들려고 신경을 써봤다. 각 파일들의 코드를 기록으로 남기고자 한다. 참고하려는 분들은 아래의 프로젝트 구성도를 계속 확인하면서 보면 좋겠다.
1. pom.xml
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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>
.
.
.
<!-- MyBatis --><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.10</version></dependency><!-- OraCLE.. 두가지가 다 필요했다. --><dependency><groupId>com.oracle.database.jdbc</groupId><artifactId>ojdbc11</artifactId><version>23.5.0.24.07</version></dependency><dependency><groupId>com.oracle.ojdbc</groupId><artifactId>orai18n</artifactId><version>19.3.0.0</version></dependency><!-- Serial 통신 라이브러리 --><dependency><groupId>com.fazecast</groupId><artifactId>jSerialComm</artifactId><version>2.11.0</version></dependency></dependencies>
...
2. 화면구성(hello-view.fxml)
화면은 단순하다. 그렇지만 수많은 시행착오끝에 만든 화면이다. JavaFX로 만들어보는건 처음이라..
시작점이 되는 파일이다. 코드 아래쪽을 보면 SerialService라는 서비스를 불러내는데, controller를 인자로 넘겨준다. controller는 화면 구성요소와 그에 대한 controll을 하는 부분이다. 이 controller를 넘겨줘야 Serial 데이터의 입력에 맞춰 화면에 정보를 뿌릴 수 있다.
위의 HelloApplication에서 시작하여 Serial데이터를 받기 시작하는 서비스이다. 데이터를 너무 빨리 처리하면 Serial데이터가 두번에 나눠져 들어온다. 그래서 초기에 약간의 대기시간(50ms)을 두었으며, 그래도 모를 예외를 대비해서 12자리를 모두 채우면 데이터를 처리하도록 만들었다.
데이터(message)가 취득되면 emp_check이라는 직원 체크 함수로 간다. 이 함수에서 MyBatis를 통한 DB데이터를 가져오고, 최종 만들어진 데이터 및 인사말은
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.example.javafx05.emp.EmpMapper"><selectid="select_member"resultType="com.example.javafx05.emp.EmpDto">
select emp_# as emp_no,
emp_x,
kornm_n,
hannm_n,
engnm_n,
res_#1 as res,
dept_c,
dept_n
from bemp a
where a.emp_#=#{emp_no}
</select><selectid="history_found"resultType="map">
select EMP_# as emp_no,
IN_D,
OUT_D,
PURPOSE,
LOCATION
FROM BEMP_DATAROOM_HSTRY a
where a.emp_#=#{emp_no}
AND to_char(a.in_d,'YYYYMMDD')=to_char(SYSDATE,'YYYYMMDD')
AND a.OUT_D IS null
ORDER BY a.IN_D DESC
</select><selectid="history_in_log">
insert into BEMP_DATAROOM_HSTRY (emp_#, in_d, out_d, purpose, location)
values (#{emp_no}, sysdate,NULL,#{purpose},#{location})
</select><selectid="history_out_log">
update BEMP_DATAROOM_HSTRY a
set a.out_d=sysdate
where a.emp_#=#{emp_no}
and a.out_d IS NULL
AND to_char(a.in_d,'YYYYMMDD')=to_char(SYSDATE,'YYYYMMDD')
</select></mapper>
12. HistoryDto
package com.example.javafx05.history;
public record HistoryDto( String emp_no,
String kornm_n,
String dept_c,
String in_d,
String out_d,
String purpose,
String location){
}
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.example.javafx05.history.HistoryMapper"><selectid="historyRecords"resultType="com.example.javafx05.history.HistoryDto">
select a.emp_# ,
b.KORNM_N ,
b.dept_c,
a.in_d,
a.out_d,
a.purpose,
a.location
from temp_dataroom_hstry a, temp b
where a.emp_#=b."EMP_#"
<iftest="emp_name!=null and emp_name!=''">
and b.kornm_n like '%'||#{emp_name}||'%'
</if><iftest="purpose!=null and purpose!=''">
and a.purpose like '%'||#{purpose}||'%'
</if><iftest="location!=null and location!=''">
and a.location like '%'||#{location}||'%'
</if><iftest="from!=null and from!=''">
and a.in_d > to_date(#{from},'YYYY-MM-DD')
</if><iftest="to!=null and to!=''">
and a.in_d < to_date(#{to},'YYYY-MM-DD')
</if>
order by a.in_d desc
</select></mapper>
namespacewinform_ex02;
using System;
using System.Text;
using System.Xml;
using MaterialSkin; //추가using MaterialSkin.Controls; //추가// public partial class Form1 : FormpublicpartialclassForm1 : MaterialForm
{
publicForm1()
{
InitializeComponent();
// 아래 추가var materialSkinManager = MaterialSkinManager.Instance;
materialSkinManager.AddFormToManage(this);
materialSkinManager.Theme = MaterialSkinManager.Themes.DARK;
materialSkinManager.ColorScheme=new ColorScheme(Primary.BlueGrey900, Primary.BlueGrey900, Primary.BlueGrey500, Accent.LightBlue200, TextShade.WHITE);
// 여기까지 // 이하는 버튼 클릭 이벤트...this.button1.Click += new System.EventHandler(this.button1_Click);
}
privatevoidbutton1_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDlg = new OpenFileDialog();
openFileDlg.Filter = "XML Files (*.xml)|*.xml";
openFileDlg.ShowDialog();
// MessageBox.Show("Hello World!");if (openFileDlg.FileName.Length > 0)
{
foreach (string Filename in openFileDlg.FileNames)
{
this.textBox1.Text = Filename;
XmlDocument xmlDoc = new XmlDocument(); // XmlDocument 객체 생성
xmlDoc.Load(Filename); // XML 파일 로드
XmlNodeList allNodes = xmlDoc.SelectNodes("//bookstore//*");
StringBuilder sb = new StringBuilder();
foreach(XmlNode node in allNodes){
if(node.NodeType == XmlNodeType.Element){
sb.Append(node.InnerText);
}
elseif (node.NodeType == XmlNodeType.Text){
sb.Append(node.Value );
}
// sb.Append("\n");
sb.Append(Environment.NewLine);
}
this.result1.Text = sb.ToString();
}
}
}
}
ColorScheme클래스의 생성 인자
materialSkinManager.ColorScheme=new ColorScheme(Primary.BlueGrey900, Primary.BlueGrey900, Primary.BlueGrey500, Accent.LightBlue200, TextShade.WHITE); // 인자1 `Primary.BlueGrey900`: 테마의 기본 색상. 메인 강조 색상. 진한 청회색. // 인자2 `Primary.BlueGrey900`: 보조 색상으로 호버 색상. 초점 색상. // 인자3 `Primary.BlueGrey500`: 3차 색상. 배경색 or 은은한 그림자 색상으로 가끔 사용됨. // 인자4 `Accent.LightBlue200`: 버튼, 링크 등 강조 색상. 연한 파란색 // 인자5 `TextShade.WHITE`: 텍스트 음영
4. Form1.Designer.cs (버튼 클릭 이벤트/폼 예제)
namespacewinform_ex02;
partialclassForm1
{
///<summary>/// Required designer variable.///</summary>private System.ComponentModel.IContainer components = null;
///<summary>/// Clean up any resources being used.///</summary>///<param name="disposing">true if managed resources should be disposed; otherwise, false.</param>protectedoverridevoidDispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code///<summary>/// Required method for Designer support - do not modify/// the contents of this method with the code editor.///</summary>private Button button1;
private TextBox textBox1;
private TextBox result1;
private Panel panel1;
privatevoidInitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
// 버튼 컨트롤 생성 및 설정this.button1 = new System.Windows.Forms.Button();
this.button1.Location = new System.Drawing.Point(10, 10);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(70, 50);
this.button1.Text = "Click Me!";
this.button1.UseVisualStyleBackColor = true;
// this.button1.Click += new System.EventHandler(this.button1_Click); // 클릭 이벤트 핸들러 등록// 버튼을 폼에 추가this.Controls.Add(this.button1);
// 텍스트 박스 생성 및 추가this.textBox1 = new System.Windows.Forms.TextBox();
this.textBox1.Location = new System.Drawing.Point(10, 70);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(500, 500);
this.textBox1.Text = "";
// this.textBox1.UseVisualStyleBackColor = true;this.Controls.Add(this.textBox1);
// 패널 생성 및 추가 this.panel1 = new System.Windows.Forms.Panel();
this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right | System.Windows.Forms.AnchorStyles.Bottom)));
this.panel1.Location = new System.Drawing.Point(10, 100);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(1090,330);
// this.panel1.TabIndex = 0;// this.panel1.Width = this.ClientSize.Width;// this.panel1.Height = this.ClientSize.Width;// this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;this.panel1.AutoScroll = true;
this.panel1.BackColor = Color.DimGray;
this.Controls.Add(this.panel1);
// 텍스트 박스 생성 및 추가this.result1 = new System.Windows.Forms.TextBox();
this.result1.Name = "resul1";
this.result1.Multiline = true;
this.result1.Location = new System.Drawing.Point(0, 0);
this.result1.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.result1.Dock = System.Windows.Forms.DockStyle.Fill; // Dock 속성 설정this.result1.Text = "";
// 텍스트 박스의 크기와 위치를 조정 // this.result1.Width = this.panel1.ClientRectangle.Width;// this.result1.Height = this.panel1.ClientRectangle.Height; this.panel1.Controls.Add(this.result1);
}
#endregion
}
간단하게 'Hello World'만 return하는 서버를 만들어 두었습니다. Github에서 해당 서버로 자동배포하는 것까지 구성은 못해서 FTP로 일일이 옮겨넣었습니다.
var express = require('express');
var app = express();
var user = require('./routes/user');
const cors = require('cors');
app.use(cors());
app.get('/', function (req, res) { // 기본 root('/') 는 main.js에서 routing
res.send('Hello World');
});
//app.use('/user', user); // 나머지 접근은 router(/routes/user)에서 routing
app.listen(3000, function () { // 3000 포트로 서버 실행
console.log('Example App is listening on port 3000');
});
소스코드에 보시면, 다른 서버로부터의 요청이 허용되도록 app.use(cors())를 선언해줘야합니다.
[[redirects]]
from = "/api/*"
to = "http://1xx.6xx.2xx.2xx:3000/:splat"
status = 200
force = true
oracle 서버에서 cors 허용을 해놓았는데도 원래의 주소로 request를 보내면 오류가 납니다. 이유는 oracle에서는 http로 서비스를 하고 있고, netlify에서는 https로 서비스를 하고 있어서 그렇습니다. oracle에도 https서비스를 위한 인증서를 구해서 넣으면 좋지만, 위와 같이 proxy 우회 방법으로 간단히 두 서버간 통신이 가능합니다.