feat(backend): replace fastapi backend with micronaut/java + mybatis
This commit fully replaces the legacy Python FastAPI backend with a new Java-based Micronaut + MyBatis CRUD backend: - add Maven build configuration and project dependencies - implement database config, auto data initialization, and custom error handling - add CRUD controllers, repository, mapper, and entity classes - include Swagger UI static resources and configuration - update README.md to reflect new backend setup and execution steps - remove old Python backend files (main.py, requirements.txt, init_db.py) - add Java/Maven-specific gitignore rules
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,16 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
import sqlite3 # SQLite 데이터베이스를 다루기 위한 모듈을 가져온다.
|
||||
from pathlib import Path # 파일 경로를 안전하게 계산하기 위한 모듈을 가져온다.
|
||||
|
||||
DB_PATH = Path(__file__).resolve().parent / "app.db" # 현재 파일 기준으로 데이터베이스 파일 경로를 정한다.
|
||||
|
||||
|
||||
def main() -> None: # 데이터베이스를 초기화하는 메인 함수를 정의한다.
|
||||
connection = sqlite3.connect(DB_PATH) # SQLite 데이터베이스에 연결한다.
|
||||
cursor = connection.cursor() # SQL 실행을 위한 커서를 만든다.
|
||||
cursor.execute("CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL)") # 메시지 테이블이 없으면 생성한다.
|
||||
cursor.execute("DELETE FROM messages") # 예제를 단순하게 유지하기 위해 기존 메시지를 모두 지운다.
|
||||
cursor.execute("INSERT INTO messages (content) VALUES (?)", ("hello world",)) # hello world 예제 데이터를 한 건 추가한다.
|
||||
connection.commit() # 변경 내용을 데이터베이스에 저장한다.
|
||||
connection.close() # 데이터베이스 연결을 닫는다.
|
||||
print(f"Database initialized at: {DB_PATH}") # 초기화된 데이터베이스 경로를 출력한다.
|
||||
|
||||
|
||||
if __name__ == "__main__": # 현재 파일을 직접 실행했을 때만 초기화 함수를 호출한다.
|
||||
main() # 데이터베이스 초기화를 수행한다.
|
||||
@@ -1,87 +0,0 @@
|
||||
import sqlite3 # SQLite 데이터베이스를 읽고 쓰기 위한 모듈을 가져온다.
|
||||
from pathlib import Path # 데이터베이스 파일 경로를 계산하기 위한 모듈을 가져온다.
|
||||
|
||||
from fastapi import FastAPI, HTTPException # FastAPI 앱과 예외 응답 도구를 가져온다.
|
||||
from fastapi.middleware.cors import CORSMiddleware # 프론트엔드 연동을 위한 CORS 미들웨어를 가져온다.
|
||||
from pydantic import BaseModel # 요청 본문을 검증하기 위한 기본 모델 클래스를 가져온다.
|
||||
|
||||
DB_PATH = Path(__file__).resolve().parent / "app.db" # 현재 파일 기준으로 데이터베이스 파일 경로를 정한다.
|
||||
app = FastAPI(title="SQLite CRUD API") # FastAPI 애플리케이션 인스턴스를 만든다.
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["http://127.0.0.1:5173", "http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) # Vue 개발 서버에서 API를 호출할 수 있도록 CORS를 허용한다.
|
||||
|
||||
|
||||
class MessagePayload(BaseModel): # 메시지 생성과 수정을 위한 요청 모델을 정의한다.
|
||||
content: str # 요청 본문에서 메시지 내용을 문자열로 받는다.
|
||||
|
||||
|
||||
def ensure_database_exists() -> None: # 데이터베이스 파일 존재 여부를 확인하는 함수를 정의한다.
|
||||
if not DB_PATH.exists(): # 데이터베이스 파일이 아직 생성되지 않았는지 확인한다.
|
||||
raise HTTPException(status_code=500, detail="Database file does not exist. Run backend/init_db.py first.") # 초기화 스크립트 실행이 필요하다는 오류를 반환한다.
|
||||
|
||||
|
||||
def get_connection() -> sqlite3.Connection: # SQLite 연결 객체를 공통으로 만드는 함수를 정의한다.
|
||||
ensure_database_exists() # 데이터베이스 파일이 존재하는지 먼저 확인한다.
|
||||
connection = sqlite3.connect(DB_PATH) # SQLite 데이터베이스에 연결한다.
|
||||
connection.row_factory = sqlite3.Row # 컬럼 이름으로 접근할 수 있도록 Row 팩토리를 지정한다.
|
||||
return connection # 설정이 끝난 연결 객체를 반환한다.
|
||||
|
||||
|
||||
def normalize_content(content: str) -> str: # 메시지 내용을 공통 규칙으로 정리하는 함수를 정의한다.
|
||||
normalized_content = content.strip() # 앞뒤 공백을 제거해 실제 입력값만 남긴다.
|
||||
if not normalized_content: # 공백만 있거나 빈 문자열인 경우를 확인한다.
|
||||
raise HTTPException(status_code=400, detail="Content must not be empty") # 빈 메시지는 허용하지 않는다는 오류를 반환한다.
|
||||
return normalized_content # 검증을 통과한 메시지 내용을 반환한다.
|
||||
|
||||
|
||||
def serialize_message(row: sqlite3.Row) -> dict[str, int | str]: # SQLite 행 데이터를 JSON 응답용 딕셔너리로 바꾸는 함수를 정의한다.
|
||||
return {"id": row["id"], "content": row["content"]} # id와 content만 꺼내서 반환한다.
|
||||
|
||||
|
||||
def read_first_message() -> str: # 데이터베이스에서 첫 번째 메시지를 읽는 함수를 정의한다.
|
||||
with get_connection() as connection: # 데이터베이스 연결을 열고 자동으로 닫히게 한다.
|
||||
row = connection.execute("SELECT content FROM messages ORDER BY id ASC LIMIT 1").fetchone() # 가장 먼저 저장된 메시지 한 건을 조회한다.
|
||||
if row is None: # 조회된 메시지가 없는 경우를 확인한다.
|
||||
raise HTTPException(status_code=404, detail="Message not found") # 메시지가 없다는 404 오류를 반환한다.
|
||||
return row["content"] # 조회한 메시지 내용을 반환한다.
|
||||
|
||||
|
||||
@app.get("/api/message") # 기존 예제와 호환되도록 첫 번째 메시지를 반환하는 GET 엔드포인트를 유지한다.
|
||||
def get_message() -> dict[str, str]: # JSON 응답 형태의 딕셔너리를 반환하는 함수를 정의한다.
|
||||
return {"message": read_first_message()} # 데이터베이스에서 읽은 첫 번째 메시지를 JSON으로 반환한다.
|
||||
|
||||
|
||||
@app.get("/api/messages") # 전체 메시지 목록을 반환하는 GET 엔드포인트를 정의한다.
|
||||
def list_messages() -> list[dict[str, int | str]]: # 메시지 목록을 JSON 배열 형태로 반환하는 함수를 정의한다.
|
||||
with get_connection() as connection: # 데이터베이스 연결을 열고 자동으로 닫히게 한다.
|
||||
rows = connection.execute("SELECT id, content FROM messages ORDER BY id ASC").fetchall() # 저장된 메시지를 id 오름차순으로 모두 조회한다.
|
||||
return [serialize_message(row) for row in rows] # 조회된 모든 행을 직렬화해 반환한다.
|
||||
|
||||
|
||||
@app.post("/api/messages", status_code=201) # 새 메시지를 저장하는 POST 엔드포인트를 정의한다.
|
||||
def create_message(payload: MessagePayload) -> dict[str, int | str]: # 생성된 메시지 정보를 반환하는 함수를 정의한다.
|
||||
normalized_content = normalize_content(payload.content) # 요청 본문의 메시지 내용을 공통 규칙으로 정리한다.
|
||||
with get_connection() as connection: # 데이터베이스 연결을 열고 자동으로 닫히게 한다.
|
||||
cursor = connection.execute("INSERT INTO messages (content) VALUES (?)", (normalized_content,)) # 정리된 메시지 내용을 테이블에 저장한다.
|
||||
connection.commit() # INSERT 결과를 데이터베이스에 반영한다.
|
||||
return {"id": int(cursor.lastrowid), "content": normalized_content} # 생성된 id와 메시지 내용을 응답으로 반환한다.
|
||||
|
||||
|
||||
@app.put("/api/messages/{message_id}") # 기존 메시지를 수정하는 PUT 엔드포인트를 정의한다.
|
||||
def update_message(message_id: int, payload: MessagePayload) -> dict[str, int | str]: # 수정된 메시지 정보를 반환하는 함수를 정의한다.
|
||||
normalized_content = normalize_content(payload.content) # 요청 본문의 메시지 내용을 공통 규칙으로 정리한다.
|
||||
with get_connection() as connection: # 데이터베이스 연결을 열고 자동으로 닫히게 한다.
|
||||
cursor = connection.execute("UPDATE messages SET content = ? WHERE id = ?", (normalized_content, message_id)) # 지정한 id의 메시지 내용을 새 값으로 수정한다.
|
||||
connection.commit() # UPDATE 결과를 데이터베이스에 반영한다.
|
||||
if cursor.rowcount == 0: # 실제로 수정된 행이 없는 경우를 확인한다.
|
||||
raise HTTPException(status_code=404, detail="Message not found") # 없는 메시지라는 404 오류를 반환한다.
|
||||
return {"id": message_id, "content": normalized_content} # 수정된 id와 메시지 내용을 응답으로 반환한다.
|
||||
|
||||
|
||||
@app.delete("/api/messages/{message_id}") # 기존 메시지를 삭제하는 DELETE 엔드포인트를 정의한다.
|
||||
def delete_message(message_id: int) -> dict[str, bool]: # 삭제 성공 여부를 반환하는 함수를 정의한다.
|
||||
with get_connection() as connection: # 데이터베이스 연결을 열고 자동으로 닫히게 한다.
|
||||
cursor = connection.execute("DELETE FROM messages WHERE id = ?", (message_id,)) # 지정한 id의 메시지를 테이블에서 삭제한다.
|
||||
connection.commit() # DELETE 결과를 데이터베이스에 반영한다.
|
||||
if cursor.rowcount == 0: # 실제로 삭제된 행이 없는 경우를 확인한다.
|
||||
raise HTTPException(status_code=404, detail="Message not found") # 없는 메시지라는 404 오류를 반환한다.
|
||||
return {"success": True} # 삭제가 성공했음을 JSON으로 반환한다.
|
||||
@@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1).replace('?', '&');
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
run();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>skilldesk</groupId>
|
||||
<artifactId>skilldesk-backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<micronaut.version>4.6.3</micronaut.version>
|
||||
<micronaut.openapi.version>6.12.3</micronaut.openapi.version>
|
||||
<swagger.version>2.2.28</swagger.version>
|
||||
<mybatis.version>3.5.16</mybatis.version>
|
||||
<sqlite.version>3.45.3.0</sqlite.version>
|
||||
<exec.mainClass>skilldesk.Application</exec.mainClass>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.micronaut.platform</groupId>
|
||||
<artifactId>micronaut-platform</artifactId>
|
||||
<version>${micronaut.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-inject</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-http-server-netty</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-http-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-dbcp2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>${mybatis.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micronaut.openapi</groupId>
|
||||
<artifactId>micronaut-openapi-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micronaut.openapi</groupId>
|
||||
<artifactId>micronaut-openapi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>${swagger.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>${sqlite.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.micronaut.maven</groupId>
|
||||
<artifactId>micronaut-maven-plugin</artifactId>
|
||||
<version>4.7.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<configuration>
|
||||
<mainClass>${exec.mainClass}</mainClass>
|
||||
<includePluginDependencies>true</includePluginDependencies>
|
||||
<includeProjectDependencies>true</includeProjectDependencies>
|
||||
<classpathScope>runtime</classpathScope>
|
||||
<systemProperties>
|
||||
<systemProperty>
|
||||
<key>micronaut.server.port</key>
|
||||
<value>8000</value>
|
||||
</systemProperty>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-inject-java</artifactId>
|
||||
<version>${micronaut.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-http-validation</artifactId>
|
||||
<version>${micronaut.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>io.micronaut.openapi</groupId>
|
||||
<artifactId>micronaut-openapi-annotations</artifactId>
|
||||
<version>${micronaut.openapi.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>io.micronaut.openapi</groupId>
|
||||
<artifactId>micronaut-openapi</artifactId>
|
||||
<version>${micronaut.openapi.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,2 +0,0 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
@@ -0,0 +1,20 @@
|
||||
package skilldesk;
|
||||
|
||||
import io.micronaut.runtime.Micronaut;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.info.Contact;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
info = @Info(
|
||||
title = "skillDesk API",
|
||||
version = "1.0.0",
|
||||
description = "SQLite + Micronaut + MyBatis CRUD API",
|
||||
contact = @Contact(name = "skillDesk")
|
||||
)
|
||||
)
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
Micronaut.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package skilldesk.config;
|
||||
|
||||
import io.micronaut.context.event.ApplicationEventListener;
|
||||
import io.micronaut.context.event.StartupEvent;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
|
||||
@Singleton
|
||||
public class DataInitializer implements ApplicationEventListener<StartupEvent> {
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
public DataInitializer(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(StartupEvent event) {
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
Statement stmt = conn.createStatement()) {
|
||||
|
||||
stmt.executeUpdate(
|
||||
"CREATE TABLE IF NOT EXISTS messages (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"content TEXT NOT NULL)"
|
||||
);
|
||||
|
||||
java.sql.ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM messages");
|
||||
rs.next();
|
||||
if (rs.getInt(1) == 0) {
|
||||
stmt.executeUpdate("INSERT INTO messages (content) VALUES ('hello world')");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to initialize database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package skilldesk.config;
|
||||
|
||||
import io.micronaut.context.annotation.Bean;
|
||||
import io.micronaut.context.annotation.Factory;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.apache.commons.dbcp2.BasicDataSource;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Factory
|
||||
public class DataSourceConfig {
|
||||
|
||||
@Singleton
|
||||
public DataSource dataSource() {
|
||||
BasicDataSource ds = new BasicDataSource();
|
||||
ds.setUrl("jdbc:sqlite:app.db");
|
||||
ds.setDriverClassName("org.sqlite.JDBC");
|
||||
return ds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package skilldesk.config;
|
||||
|
||||
import io.micronaut.context.annotation.Bean;
|
||||
import io.micronaut.context.annotation.Factory;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import skilldesk.mapper.MessageMapper;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Factory
|
||||
public class MyBatisConfig {
|
||||
|
||||
@Singleton
|
||||
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) {
|
||||
Environment environment = new Environment("skilldesk", new JdbcTransactionFactory(), dataSource);
|
||||
Configuration configuration = new Configuration(environment);
|
||||
configuration.addMapper(MessageMapper.class);
|
||||
return new SqlSessionFactoryBuilder().build(configuration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package skilldesk.config;
|
||||
|
||||
import io.micronaut.http.MutableHttpResponse;
|
||||
import io.micronaut.http.server.exceptions.response.Error;
|
||||
import io.micronaut.http.server.exceptions.response.ErrorContext;
|
||||
import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
public class SimpleErrorResponseProcessor implements ErrorResponseProcessor<Object> {
|
||||
|
||||
@Override
|
||||
public MutableHttpResponse<Object> processResponse(ErrorContext errorContext, MutableHttpResponse<?> response) {
|
||||
Optional<Error> error = errorContext.getErrors().stream().findFirst();
|
||||
@SuppressWarnings("unchecked")
|
||||
MutableHttpResponse<Object> resp = (MutableHttpResponse<Object>) response;
|
||||
if (error.isPresent()) {
|
||||
resp.body(Map.of("detail", error.get().getMessage()));
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package skilldesk.controller;
|
||||
|
||||
import io.micronaut.http.HttpStatus;
|
||||
import io.micronaut.http.annotation.Body;
|
||||
import io.micronaut.http.annotation.Controller;
|
||||
import io.micronaut.http.annotation.Delete;
|
||||
import io.micronaut.http.annotation.Get;
|
||||
import io.micronaut.http.annotation.Post;
|
||||
import io.micronaut.http.annotation.Put;
|
||||
import io.micronaut.http.annotation.Status;
|
||||
import io.micronaut.http.exceptions.HttpStatusException;
|
||||
import skilldesk.entity.Message;
|
||||
import skilldesk.mapper.MessageRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller("/api")
|
||||
public class MessageController {
|
||||
|
||||
private final MessageRepository messageRepository;
|
||||
|
||||
public MessageController(MessageRepository messageRepository) {
|
||||
this.messageRepository = messageRepository;
|
||||
}
|
||||
|
||||
@Get("/message")
|
||||
public Map<String, String> getMessage() {
|
||||
Message message = messageRepository.findFirst();
|
||||
if (message == null) {
|
||||
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Message not found");
|
||||
}
|
||||
return Map.of("message", message.getContent());
|
||||
}
|
||||
|
||||
@Get("/messages")
|
||||
public List<Message> listMessages() {
|
||||
return messageRepository.findAll();
|
||||
}
|
||||
|
||||
@Post("/messages")
|
||||
@Status(HttpStatus.CREATED)
|
||||
public Message createMessage(@Body Map<String, String> body) {
|
||||
String content = body.get("content");
|
||||
if (content == null || content.strip().isEmpty()) {
|
||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Content must not be empty");
|
||||
}
|
||||
Message message = new Message(content.strip());
|
||||
messageRepository.insert(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
@Put("/messages/{id}")
|
||||
public Message updateMessage(Integer id, @Body Map<String, String> body) {
|
||||
String content = body.get("content");
|
||||
if (content == null || content.strip().isEmpty()) {
|
||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Content must not be empty");
|
||||
}
|
||||
int updated = messageRepository.update(id, content.strip());
|
||||
if (updated == 0) {
|
||||
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Message not found");
|
||||
}
|
||||
return new Message(id, content.strip());
|
||||
}
|
||||
|
||||
@Delete("/messages/{id}")
|
||||
public Map<String, Boolean> deleteMessage(Integer id) {
|
||||
int deleted = messageRepository.deleteById(id);
|
||||
if (deleted == 0) {
|
||||
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Message not found");
|
||||
}
|
||||
return Map.of("success", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package skilldesk.controller;
|
||||
|
||||
import io.micronaut.http.HttpRequest;
|
||||
import io.micronaut.http.MediaType;
|
||||
import io.micronaut.http.annotation.Controller;
|
||||
import io.micronaut.http.annotation.Get;
|
||||
import io.micronaut.http.annotation.Produces;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller("/")
|
||||
public class RootController {
|
||||
|
||||
@Get
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Map<String, Object> index(HttpRequest<?> request) {
|
||||
String host = request.getHeaders().get("Host");
|
||||
String base = "http://" + host;
|
||||
return Map.of(
|
||||
"service", "skillDesk API",
|
||||
"endpoints", List.of(
|
||||
Map.of("method", "GET", "url", base + "/api/message", "description", "첫 번째 메시지 조회"),
|
||||
Map.of("method", "GET", "url", base + "/api/messages", "description", "전체 메시지 목록 조회"),
|
||||
Map.of("method", "POST", "url", base + "/api/messages", "description", "새 메시지 생성"),
|
||||
Map.of("method", "PUT", "url", base + "/api/messages/{id}", "description", "메시지 수정"),
|
||||
Map.of("method", "DELETE", "url", base + "/api/messages/{id}", "description", "메시지 삭제")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package skilldesk.entity;
|
||||
|
||||
public class Message {
|
||||
private Integer id;
|
||||
private String content;
|
||||
|
||||
public Message() {}
|
||||
|
||||
public Message(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Message(Integer id, String content) {
|
||||
this.id = id;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package skilldesk.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import skilldesk.entity.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MessageMapper {
|
||||
|
||||
@Select("SELECT id, content FROM messages ORDER BY id ASC LIMIT 1")
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "content", column = "content")
|
||||
})
|
||||
Message findFirst();
|
||||
|
||||
@Select("SELECT id, content FROM messages ORDER BY id ASC")
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "content", column = "content")
|
||||
})
|
||||
List<Message> findAll();
|
||||
|
||||
@Insert("INSERT INTO messages (content) VALUES (#{content})")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
|
||||
void insert(Message message);
|
||||
|
||||
@Update("UPDATE messages SET content = #{content} WHERE id = #{id}")
|
||||
int update(@Param("id") Integer id, @Param("content") String content);
|
||||
|
||||
@Delete("DELETE FROM messages WHERE id = #{id}")
|
||||
int deleteById(Integer id);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package skilldesk.mapper;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import skilldesk.entity.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
public class MessageRepository {
|
||||
|
||||
private final SqlSessionFactory sqlSessionFactory;
|
||||
|
||||
public MessageRepository(SqlSessionFactory sqlSessionFactory) {
|
||||
this.sqlSessionFactory = sqlSessionFactory;
|
||||
}
|
||||
|
||||
public Message findFirst() {
|
||||
try (SqlSession session = sqlSessionFactory.openSession()) {
|
||||
return session.getMapper(MessageMapper.class).findFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Message> findAll() {
|
||||
try (SqlSession session = sqlSessionFactory.openSession()) {
|
||||
return session.getMapper(MessageMapper.class).findAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void insert(Message message) {
|
||||
try (SqlSession session = sqlSessionFactory.openSession(true)) {
|
||||
session.getMapper(MessageMapper.class).insert(message);
|
||||
}
|
||||
}
|
||||
|
||||
public int update(Integer id, String content) {
|
||||
try (SqlSession session = sqlSessionFactory.openSession(true)) {
|
||||
return session.getMapper(MessageMapper.class).update(id, content);
|
||||
}
|
||||
}
|
||||
|
||||
public int deleteById(Integer id) {
|
||||
try (SqlSession session = sqlSessionFactory.openSession(true)) {
|
||||
return session.getMapper(MessageMapper.class).deleteById(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
micronaut:
|
||||
application:
|
||||
name: skilldesk-backend
|
||||
server:
|
||||
port: 8000
|
||||
cors:
|
||||
enabled: true
|
||||
configurations:
|
||||
all:
|
||||
allowedOrigins:
|
||||
- http://127.0.0.1:5173
|
||||
- http://localhost:5173
|
||||
allowedMethods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
allowedHeaders:
|
||||
- Content-Type
|
||||
router:
|
||||
static-resources:
|
||||
swagger:
|
||||
paths: classpath:META-INF/swagger
|
||||
mapping: /swagger/**
|
||||
swagger-ui:
|
||||
paths: classpath:swagger-ui
|
||||
mapping: /swagger-ui/**
|
||||
|
||||
openapi:
|
||||
micronaut:
|
||||
enabled: true
|
||||
server:
|
||||
url: http://127.0.0.1:8000
|
||||
@@ -0,0 +1,10 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,16 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1).replace('?', '&');
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
run();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
window.onload = function() {
|
||||
//<editor-fold desc="Changeable Configuration Block">
|
||||
|
||||
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "/swagger/skilldesk-api-1.0.0.yml",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
|
||||
//</editor-fold>
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
|
||||
window.onload = function() {
|
||||
//<editor-fold desc="Changeable Configuration Block">
|
||||
|
||||
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "https://petstore.swagger.io/v2/swagger.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
|
||||
//</editor-fold>
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user