feat: add standalone pod module scaffolder
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
# Pod Module New Workspace Scaffolder Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 기존 Pod Scaffold를 변경하지 않고, Scaffold 화면에서 독립 Tool Pod 프로젝트를 `C:\eGovFrameDev-4.3.1-64bit\workspace`에 생성한다.
|
||||
|
||||
**Architecture:** 기존 `PodScaffolder`와 `/api/v1/scaffold/pod`는 유지한다. `NewPodProjectScaffolder`가 독립 Gradle 프로젝트 파일을 생성하고, 별도 `/api/v1/scaffold/pod-new` endpoint와 `Pod Module New` UI가 이를 호출한다. 생성 프로젝트는 `../dat-lib-datmt` composite build로 공통 라이브러리를 해석한다.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 3.5.11, Gradle composite build, JUnit 5, MockMvc, static HTML/JavaScript.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-09-new-pod-module-workspace-scaffolder-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- `PodScaffolder` 및 기존 `/api/v1/scaffold/pod`의 코드와 동작을 변경하지 않는다.
|
||||
- 기존 Pod 모듈 `dat-was-cus`, `dat-was-pro`, `dat-was-sal`, `dat-was-sys`를 수정하지 않는다.
|
||||
- 신규 생성 대상은 사용자 입력 workspace의 `<workspace>/<moduleName>`이며, 기본 workspace는 `C:\eGovFrameDev-4.3.1-64bit\workspace`이다.
|
||||
- moduleName은 `dat-was-` 접두사가 붙은 소문자·숫자·하이픈 식별자만 허용하고 `dat-was-lib`는 거부한다.
|
||||
- port는 1부터 65535까지의 정수만 허용한다.
|
||||
- 생성 프로젝트는 `../dat-lib-datmt`를 포함하고 `io.shinhanlife:dat-lib-datmt:0.0.1-SNAPSHOT`를 `:dat-was-lib`로 치환한다.
|
||||
- 사용자 요청이 없는 한 commit 또는 push하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 독립 Pod 프로젝트 생성기
|
||||
|
||||
**Files:**
|
||||
- Create: `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/NewPodProjectScaffolder.java`
|
||||
- Create: `dat-was-lib/src/test/java/io/shinhanlife/dat/lib/util/NewPodProjectScaffolderTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `NewPodProjectScaffolder.scaffold(Path workspaceRoot, String moduleName, int port, String author, String createdDate)` returning a human-readable generation summary.
|
||||
- Produces: `NewPodProjectScaffolder.validateModuleName(String moduleName)` and `validatePort(int port)` throwing `IllegalArgumentException` for invalid input.
|
||||
- Consumes: a writable workspace root; creates no files outside it.
|
||||
|
||||
- [ ] **Step 1: Write the failing generator test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void createsStandaloneProjectWithCompositeBuild() throws Exception {
|
||||
String result = NewPodProjectScaffolder.scaffold(workspace, "dat-was-payment", 8099,
|
||||
"tester", "2026.09.09");
|
||||
|
||||
Path project = workspace.resolve("dat-was-payment");
|
||||
assertTrue(Files.exists(project.resolve("settings.gradle")));
|
||||
assertTrue(Files.exists(project.resolve("build.gradle")));
|
||||
assertTrue(Files.exists(project.resolve("src/main/resources/application.yml")));
|
||||
assertTrue(Files.exists(project.resolve("docker-compose.yml")));
|
||||
assertTrue(Files.exists(project.resolve("k8s/base/deployment.yaml")));
|
||||
assertTrue(Files.readString(project.resolve("settings.gradle"))
|
||||
.contains("includeBuild('../dat-lib-datmt')"));
|
||||
assertTrue(Files.readString(project.resolve("build.gradle"))
|
||||
.contains("io.shinhanlife:dat-lib-datmt:0.0.1-SNAPSHOT"));
|
||||
assertTrue(Files.readString(project.resolve("src/main/resources/application.yml"))
|
||||
.contains("port: ${PORT:8099}"));
|
||||
assertTrue(result.contains("dat-was-payment"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the new test and verify the expected failure**
|
||||
|
||||
Run: `.\gradlew.bat :dat-was-lib:test --tests "io.shinhanlife.dat.lib.util.NewPodProjectScaffolderTest.createsStandaloneProjectWithCompositeBuild" --no-daemon`
|
||||
|
||||
Expected: compilation failure because `NewPodProjectScaffolder` does not exist.
|
||||
|
||||
- [ ] **Step 3: Write failing validation and non-overwrite tests**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void rejectsInvalidNamesPortsAndExistingProjects() throws Exception {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> NewPodProjectScaffolder.scaffold(workspace, "payment", 8099, "tester", "2026.09.09"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> NewPodProjectScaffolder.scaffold(workspace, "dat-was-lib", 8099, "tester", "2026.09.09"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> NewPodProjectScaffolder.scaffold(workspace, "dat-was-payment", 0, "tester", "2026.09.09"));
|
||||
Files.createDirectories(workspace.resolve("dat-was-payment"));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> NewPodProjectScaffolder.scaffold(workspace, "dat-was-payment", 8099, "tester", "2026.09.09"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run validation test and verify the expected failure**
|
||||
|
||||
Run: `.\gradlew.bat :dat-was-lib:test --tests "io.shinhanlife.dat.lib.util.NewPodProjectScaffolderTest.rejectsInvalidNamesPortsAndExistingProjects" --no-daemon`
|
||||
|
||||
Expected: compilation failure because the new generator is not implemented.
|
||||
|
||||
- [ ] **Step 5: Implement the generator**
|
||||
|
||||
Create only the requested standalone project root. Generate these files:
|
||||
|
||||
```text
|
||||
<workspace>/<moduleName>/settings.gradle
|
||||
<workspace>/<moduleName>/build.gradle
|
||||
<workspace>/<moduleName>/README.md
|
||||
<workspace>/<moduleName>/Dockerfile
|
||||
<workspace>/<moduleName>/docker-compose.yml
|
||||
<workspace>/<moduleName>/k8s/base/deployment.yaml
|
||||
<workspace>/<moduleName>/k8s/base/service.yaml
|
||||
<workspace>/<moduleName>/src/main/java/io/shinhanlife/dat/mcc/<shortName>/DatWas<PascalName>Application.java
|
||||
<workspace>/<moduleName>/src/main/resources/application.yml
|
||||
<workspace>/<moduleName>/src/main/resources/application-local.yml
|
||||
<workspace>/<moduleName>/src/main/resources/application-dev.yml
|
||||
<workspace>/<moduleName>/src/main/resources/application-test.yml
|
||||
<workspace>/<moduleName>/src/main/resources/application-prod.yml
|
||||
<workspace>/<moduleName>/src/main/resources/tool-service-manifest.yml
|
||||
```
|
||||
|
||||
Use this generated `settings.gradle` content:
|
||||
|
||||
```gradle
|
||||
rootProject.name = '<moduleName>'
|
||||
|
||||
includeBuild('../dat-lib-datmt') {
|
||||
dependencySubstitution {
|
||||
substitute module('io.shinhanlife:dat-lib-datmt') using project(':dat-was-lib')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use this generated dependency declaration:
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
implementation 'io.shinhanlife:dat-lib-datmt:0.0.1-SNAPSHOT'
|
||||
}
|
||||
```
|
||||
|
||||
Make all writes UTF-8 and use `Files.createDirectories` only after validation. Build a temporary project directory next to the final target and move it into place atomically after all writes succeed; remove that temporary directory if any write fails.
|
||||
|
||||
- [ ] **Step 6: Run generator tests and verify they pass**
|
||||
|
||||
Run: `.\gradlew.bat :dat-was-lib:test --tests "io.shinhanlife.dat.lib.util.NewPodProjectScaffolderTest" --no-daemon`
|
||||
|
||||
Expected: all new generator tests pass.
|
||||
|
||||
### Task 2: 신규 Scaffold API
|
||||
|
||||
**Files:**
|
||||
- Modify: `dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java`
|
||||
- Modify: `dat-gateway/src/test/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingControllerToolDraftTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: JSON `{"moduleName":"dat-was-payment","port":8099,"author":"tester","workspacePath":"C:\\temp\\workspace"}`.
|
||||
- Produces: `POST /api/v1/scaffold/pod-new` response with HTTP 200 and generated-project summary, or HTTP 400 with a validation message.
|
||||
- Consumes: `NewPodProjectScaffolder.scaffold(Path, String, int, String, String)` from Task 1.
|
||||
|
||||
- [ ] **Step 1: Write the failing MockMvc success test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void createsStandalonePodProjectWithoutCallingExistingPodScaffolder() throws Exception {
|
||||
mockMvc.perform(post("/api/v1/scaffold/pod-new")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"moduleName":"dat-was-payment","port":8099,
|
||||
"author":"tester","workspacePath":"%s"}
|
||||
""".formatted(root.toString().replace("\\", "\\\\"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("dat-was-payment")));
|
||||
|
||||
assertTrue(Files.exists(root.resolve("dat-was-payment/settings.gradle")));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the API test and verify the expected failure**
|
||||
|
||||
Run: `.\gradlew.bat :dat-gateway:test --tests "io.shinhanlife.dat.mcg.presentation.ScaffoldingControllerToolDraftTest.createsStandalonePodProjectWithoutCallingExistingPodScaffolder" --no-daemon`
|
||||
|
||||
Expected: HTTP 404 because `/api/v1/scaffold/pod-new` does not exist.
|
||||
|
||||
- [ ] **Step 3: Add the isolated endpoint and request validation**
|
||||
|
||||
Add `@PostMapping("/pod-new")` without altering `scaffoldPod`. Define a `NewPodProjectRequest` record in `ScaffoldingController` with `moduleName`, `Integer port`, `author`, and `workspacePath`. Default a missing or blank workspace path only for this new endpoint to `C:\eGovFrameDev-4.3.1-64bit\workspace`. Return `ResponseEntity.badRequest()` for invalid names, missing ports, invalid ports, or a pre-existing target path. Do not set `AXHUB_SOURCE_DIR` in this endpoint.
|
||||
|
||||
- [ ] **Step 4: Write and run the duplicate-target API test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void rejectsExistingStandalonePodProject() throws Exception {
|
||||
Files.createDirectories(root.resolve("dat-was-payment"));
|
||||
|
||||
mockMvc.perform(post("/api/v1/scaffold/pod-new")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"moduleName":"dat-was-payment","port":8099,
|
||||
"workspacePath":"%s"}
|
||||
""".formatted(root.toString().replace("\\", "\\\\"))))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
```
|
||||
|
||||
Run: `.\gradlew.bat :dat-gateway:test --tests "io.shinhanlife.dat.mcg.presentation.ScaffoldingControllerToolDraftTest" --no-daemon`
|
||||
|
||||
Expected: all controller scaffolding tests pass, including existing `/pod` behavior.
|
||||
|
||||
### Task 3: Pod Module New 화면
|
||||
|
||||
**Files:**
|
||||
- Modify: `dat-gateway/src/main/resources/static/admin/scaffold.html`
|
||||
- Test: `dat-gateway/src/test/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingControllerToolDraftTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `/api/v1/scaffold/pod-new` from Task 2.
|
||||
- Produces: a separate `newPodForm`; no existing `podForm` markup, `handleFormSubmit('podForm', ...)`, or existing manifest draft request changes.
|
||||
|
||||
- [ ] **Step 1: Write a failing static-page assertion**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void scaffoldPageContainsSeparateNewPodForm() throws Exception {
|
||||
String page = Files.readString(Path.of("src/main/resources/static/admin/scaffold.html"));
|
||||
assertTrue(page.contains("Pod Module New"));
|
||||
assertTrue(page.contains("id=\"newPodForm\""));
|
||||
assertTrue(page.contains("/api/v1/scaffold/pod-new"));
|
||||
assertTrue(page.contains("C:\\eGovFrameDev-4.3.1-64bit\\workspace"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the assertion and verify the expected failure**
|
||||
|
||||
Run: `.\gradlew.bat :dat-gateway:test --tests "io.shinhanlife.dat.mcg.presentation.ScaffoldingControllerToolDraftTest.scaffoldPageContainsSeparateNewPodForm" --no-daemon`
|
||||
|
||||
Expected: assertion failure because the new tab and form do not exist.
|
||||
|
||||
- [ ] **Step 3: Add a separate tab and form**
|
||||
|
||||
Add a new `Pod Module New` tab after the existing Pod Module tab. Its `newPodForm` must have only workspace path, module name, service port, author, and date inputs. Pre-fill only this new form's workspace path with `C:\eGovFrameDev-4.3.1-64bit\workspace`. Bind it with `handleFormSubmit('newPodForm', '/api/v1/scaffold/pod-new')`. Keep `podForm`, its workspace default, and its `/pod` handler unchanged.
|
||||
|
||||
- [ ] **Step 4: Run the static-page assertion and controller tests**
|
||||
|
||||
Run: `.\gradlew.bat :dat-gateway:test --tests "io.shinhanlife.dat.mcg.presentation.ScaffoldingControllerToolDraftTest" --no-daemon`
|
||||
|
||||
Expected: all scaffold controller tests pass.
|
||||
|
||||
### Task 4: End-to-end generated project verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Documents: `Pod Module New` creates an independent repository under the selected workspace, while existing Pod Module continues to create modules under its selected root.
|
||||
|
||||
- [ ] **Step 1: Add a Korean README section**
|
||||
|
||||
Document the two Scaffold paths with this behavior table:
|
||||
|
||||
```markdown
|
||||
| 기능 | 생성 위치 | 기존 Pod 영향 |
|
||||
| --- | --- | --- |
|
||||
| Pod Module | 선택한 기존 프로젝트 root | 기존 동작 유지 |
|
||||
| Pod Module New | `C:\eGovFrameDev-4.3.1-64bit\workspace\<moduleName>` | 기존 Pod/Scaffolder 미수정 |
|
||||
```
|
||||
|
||||
State that `Pod Module New` requires `../dat-lib-datmt` to exist beside the generated project and that the generated port comes from the form input, overridable by `PORT` at runtime.
|
||||
|
||||
- [ ] **Step 2: Create one temporary project through the new endpoint**
|
||||
|
||||
Use a unique throwaway module name such as `dat-was-scaffoldverify` and workspace `C:\eGovFrameDev-4.3.1-64bit\workspace`. Confirm the generated directory contains the settings file, composite dependency substitution, application source, and Docker/Kubernetes files.
|
||||
|
||||
- [ ] **Step 3: Compile the generated project**
|
||||
|
||||
Run from the generated project:
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat compileJava --no-daemon
|
||||
```
|
||||
|
||||
Expected: Gradle resolves `../dat-lib-datmt/:dat-was-lib` and exits successfully.
|
||||
|
||||
- [ ] **Step 4: Remove only the verified throwaway project**
|
||||
|
||||
Verify the absolute path is exactly `C:\eGovFrameDev-4.3.1-64bit\workspace\dat-was-scaffoldverify`, then remove that one generated verification project. Do not alter any user-created project under `workspace`.
|
||||
|
||||
- [ ] **Step 5: Run final targeted verification**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat :dat-was-lib:test --tests "io.shinhanlife.dat.lib.util.NewPodProjectScaffolderTest" --no-daemon
|
||||
.\gradlew.bat :dat-gateway:test --tests "io.shinhanlife.dat.mcg.presentation.ScaffoldingControllerToolDraftTest" --no-daemon
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: both focused test classes pass and `git diff --check` reports no whitespace errors.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Pod Module New Workspace Scaffolder 설계
|
||||
|
||||
## 목표
|
||||
|
||||
기존 Pod Module Scaffold(`PodScaffolder`, `/api/v1/scaffold/pod`, 기존 화면)을 변경하지 않는다.
|
||||
Scaffold 화면에 별도 `Pod Module New` 기능을 추가하여, 사용자가 입력한 Pod 모듈명을 기준으로 독립 Tool Pod 프로젝트를 `C:\eGovFrameDev-4.3.1-64bit\workspace` 아래에 생성한다.
|
||||
|
||||
## 생성 구조
|
||||
|
||||
`dat-was-payment` 입력 시 다음 독립 프로젝트를 생성한다.
|
||||
|
||||
```text
|
||||
C:\eGovFrameDev-4.3.1-64bit\workspace\dat-was-payment\
|
||||
settings.gradle
|
||||
build.gradle
|
||||
README.md
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
k8s\
|
||||
src\main\java\...
|
||||
src\main\resources\...
|
||||
```
|
||||
|
||||
입력 모듈명은 저장소 루트명, Gradle 루트 프로젝트명, Spring application name에 동일하게 사용한다.
|
||||
생성 프로젝트는 `../dat-lib-datmt`를 Gradle composite build로 포함하고, `io.shinhanlife:dat-lib-datmt:0.0.1-SNAPSHOT` 좌표를 `:dat-was-lib`로 치환한다.
|
||||
|
||||
## 새 생성 경로
|
||||
|
||||
- 신규 `NewPodProjectScaffolder`가 독립 프로젝트 파일을 생성한다.
|
||||
- 신규 Controller endpoint가 module name과 사용자가 입력한 서비스 포트를 전달한다.
|
||||
- Scaffold 화면에는 기존 Pod Module 탭과 별개인 `Pod Module New` 영역을 둔다.
|
||||
- workspace 경로는 사용자 입력값을 사용하며 기본값은 `C:\eGovFrameDev-4.3.1-64bit\workspace`이다.
|
||||
- 존재하는 프로젝트 경로에는 생성하지 않고 명확한 오류를 반환한다.
|
||||
|
||||
## 기존 기능 보호
|
||||
|
||||
다음 기존 파일과 동작은 변경하지 않는다.
|
||||
|
||||
- `PodScaffolder`
|
||||
- `/api/v1/scaffold/pod`
|
||||
- 기존 Pod Module 화면과 기존 Pod 모듈
|
||||
|
||||
## 생성 파일의 주요 설정
|
||||
|
||||
- Spring Boot 3.5.11, Java 21
|
||||
- 공통 라이브러리 composite build 연결
|
||||
- `server.port`는 사용자가 입력한 서비스 포트를 기본값으로 사용하고, 환경변수 `PORT`로 덮어쓸 수 있다.
|
||||
- Docker Compose 서비스명은 모듈명의 `dat-was-` 접두사를 `was-`로 치환한다.
|
||||
- Tool manifest는 새 Pod의 라우팅 함수만 포함한다.
|
||||
- README는 공통 라이브러리 위치, 실행 방법, 입력 포트 설정을 안내한다.
|
||||
|
||||
## 검증
|
||||
|
||||
1. 신규 Scaffolder 단위 테스트로 생성 파일, composite build 설정, 기존 경로 거부를 검증한다.
|
||||
2. Controller 테스트로 신규 endpoint의 정상 생성과 중복 경로 오류를 검증한다.
|
||||
3. 임시 workspace에 생성한 Pod의 `compileJava`를 실행해 `../dat-lib-datmt` 의존성 해석을 확인한다.
|
||||
4. 기존 `PodScaffolder` 관련 테스트를 실행해 기존 생성 경로가 바뀌지 않았음을 확인한다.
|
||||
Reference in New Issue
Block a user