diff --git a/.gitignore b/.gitignore
index 43d7546..77aee07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,18 @@ __pycache__/
.trae/
.vite/
dist/
+
+# Java / Maven
+target/
+*.class
+*.jar
+*.war
+*.log
+.settings/
+.project
+.classpath
+*.iml
+.idea/
+*.gradle
+build/
+
diff --git a/README.md b/README.md
index a9bd2bd..74934a9 100644
--- a/README.md
+++ b/README.md
@@ -98,20 +98,20 @@ skillDesk/
- [docs/test-scenarios.md](./docs/test-scenarios.md)
-## SQLite + FastAPI + Vue CRUD 예제 실행
+## SQLite + Micronaut + MyBatis + Vue CRUD 예제 실행
-현재 저장소에는 SQLite의 `messages` 테이블 데이터를 FastAPI가 읽고 쓰며, Vue 화면에서 조회, 추가, 수정, 삭제할 수 있는 최소 CRUD 예제가 포함되어 있습니다.
+현재 저장소에는 SQLite의 `messages` 테이블 데이터를 Micronaut + MyBatis 가 읽고 쓰며, Vue 화면에서 조회, 추가, 수정, 삭제할 수 있는 최소 CRUD 예제가 포함되어 있습니다.
### 1. 백엔드 실행
```bash
-python3 -m venv .venv
-source .venv/bin/activate
-pip install -r backend/requirements.txt
-python backend/init_db.py
-uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
+# backend 디렉토리에서 실행
+cd backend
+mvn exec:java -Dexec.mainClass=skilldesk.Application -Dmicronaut.server.port=8000
```
+DB와 테이블, 초기 데이터(`hello world`)는 최초 실행 시 자동 생성됩니다.
+
### 2. 프론트엔드 실행
새 터미널에서 아래 명령을 실행합니다.
@@ -135,7 +135,7 @@ python3 -m http.server 5173 -d frontend
#### 백엔드 대체 포트 예시
```bash
-uvicorn backend.main:app --host 127.0.0.1 --port 8001 --reload
+mvn exec:java -Dexec.mainClass=skilldesk.Application -Dmicronaut.server.port=8001
```
#### 프론트엔드 대체 포트 예시
diff --git a/backend/favicon-16x16.png b/backend/favicon-16x16.png
new file mode 100644
index 0000000..8b194e6
Binary files /dev/null and b/backend/favicon-16x16.png differ
diff --git a/backend/favicon-32x32.png b/backend/favicon-32x32.png
new file mode 100644
index 0000000..249737f
Binary files /dev/null and b/backend/favicon-32x32.png differ
diff --git a/backend/index.css b/backend/index.css
new file mode 100644
index 0000000..f2376fd
--- /dev/null
+++ b/backend/index.css
@@ -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;
+}
diff --git a/backend/index.html b/backend/index.html
new file mode 100644
index 0000000..84ae62d
--- /dev/null
+++ b/backend/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ Swagger UI
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/backend/init_db.py b/backend/init_db.py
deleted file mode 100644
index ea8b7a9..0000000
--- a/backend/init_db.py
+++ /dev/null
@@ -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() # 데이터베이스 초기화를 수행한다.
diff --git a/backend/main.py b/backend/main.py
deleted file mode 100644
index 70b29a4..0000000
--- a/backend/main.py
+++ /dev/null
@@ -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으로 반환한다.
diff --git a/backend/oauth2-redirect.html b/backend/oauth2-redirect.html
new file mode 100644
index 0000000..5640917
--- /dev/null
+++ b/backend/oauth2-redirect.html
@@ -0,0 +1,79 @@
+
+
+
+ Swagger UI: OAuth2 Redirect
+
+
+
+
+
diff --git a/backend/pom.xml b/backend/pom.xml
new file mode 100644
index 0000000..9b6f755
--- /dev/null
+++ b/backend/pom.xml
@@ -0,0 +1,146 @@
+
+
+ 4.0.0
+
+ skilldesk
+ skilldesk-backend
+ 1.0.0
+ jar
+
+
+ 21
+ 21
+ UTF-8
+ 4.6.3
+ 6.12.3
+ 2.2.28
+ 3.5.16
+ 3.45.3.0
+ skilldesk.Application
+
+
+
+
+
+ io.micronaut.platform
+ micronaut-platform
+ ${micronaut.version}
+ pom
+ import
+
+
+
+
+
+
+ io.micronaut
+ micronaut-inject
+
+
+ io.micronaut
+ micronaut-http-server-netty
+
+
+ io.micronaut
+ micronaut-http-server
+
+
+ io.micronaut
+ micronaut-jackson-databind
+
+
+ io.micronaut
+ micronaut-context
+
+
+ org.apache.commons
+ commons-dbcp2
+
+
+ ch.qos.logback
+ logback-classic
+ runtime
+
+
+ org.mybatis
+ mybatis
+ ${mybatis.version}
+
+
+ io.micronaut.openapi
+ micronaut-openapi-annotations
+
+
+ io.micronaut.openapi
+ micronaut-openapi
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${swagger.version}
+
+
+ org.xerial
+ sqlite-jdbc
+ ${sqlite.version}
+
+
+
+
+
+
+ io.micronaut.maven
+ micronaut-maven-plugin
+ 4.7.2
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.6.3
+
+ ${exec.mainClass}
+ true
+ true
+ runtime
+
+
+ micronaut.server.port
+ 8000
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+
+ io.micronaut
+ micronaut-inject-java
+ ${micronaut.version}
+
+
+ io.micronaut
+ micronaut-http-validation
+ ${micronaut.version}
+
+
+ io.micronaut.openapi
+ micronaut-openapi-annotations
+ ${micronaut.openapi.version}
+
+
+ io.micronaut.openapi
+ micronaut-openapi
+ ${micronaut.openapi.version}
+
+
+
+
+
+
+
diff --git a/backend/requirements.txt b/backend/requirements.txt
deleted file mode 100644
index 97dc7cd..0000000
--- a/backend/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-fastapi
-uvicorn
diff --git a/backend/src/main/java/skilldesk/Application.java b/backend/src/main/java/skilldesk/Application.java
new file mode 100644
index 0000000..3614f65
--- /dev/null
+++ b/backend/src/main/java/skilldesk/Application.java
@@ -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);
+ }
+}
diff --git a/backend/src/main/java/skilldesk/config/DataInitializer.java b/backend/src/main/java/skilldesk/config/DataInitializer.java
new file mode 100644
index 0000000..112386c
--- /dev/null
+++ b/backend/src/main/java/skilldesk/config/DataInitializer.java
@@ -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 {
+
+ 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);
+ }
+ }
+}
diff --git a/backend/src/main/java/skilldesk/config/DataSourceConfig.java b/backend/src/main/java/skilldesk/config/DataSourceConfig.java
new file mode 100644
index 0000000..694c635
--- /dev/null
+++ b/backend/src/main/java/skilldesk/config/DataSourceConfig.java
@@ -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;
+ }
+}
diff --git a/backend/src/main/java/skilldesk/config/MyBatisConfig.java b/backend/src/main/java/skilldesk/config/MyBatisConfig.java
new file mode 100644
index 0000000..b3af02f
--- /dev/null
+++ b/backend/src/main/java/skilldesk/config/MyBatisConfig.java
@@ -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);
+ }
+}
diff --git a/backend/src/main/java/skilldesk/config/SimpleErrorResponseProcessor.java b/backend/src/main/java/skilldesk/config/SimpleErrorResponseProcessor.java
new file mode 100644
index 0000000..0277615
--- /dev/null
+++ b/backend/src/main/java/skilldesk/config/SimpleErrorResponseProcessor.java
@@ -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