feat(board-generator): add board code generator and sample CRUD artifacts

Add Node.js CLI tool with Handlebars templates for generating standard CRUD artifacts: Java entity, service, DAO, controller, MyBatis mapper XML, and Vue frontend pages.
Also generate the full SampleTableBoard CRUD reference implementation, update README with backend execution instructions, and add project plan documentation.
This commit is contained in:
2026-05-31 13:22:03 +09:00
parent d5ac812703
commit 12c40c6004
25 changed files with 3305 additions and 1 deletions
@@ -0,0 +1,98 @@
package {{controllerPackage}};
import java.util.HashMap;
import java.util.Map;
{{#each controllerImports}}
import {{this}};
{{/each}}
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import {{rootPackage}}.{{serviceName}};
import {{entityFqcn}};
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.log4j.Log4j2;
@RestController
@RequestMapping("{{requestBasePath}}")
@Log4j2
public class {{controllerName}} {
private final {{serviceName}} {{entityVarName}}Service;
public {{controllerName}}({{serviceName}} {{entityVarName}}Service) {
this.{{entityVarName}}Service = {{entityVarName}}Service;
}
@Operation(summary = "page 초기정보.")
@PostMapping(value = "list/main.do", params = "method=page")
public Map<String, Object> page(@RequestBody Map<String, Object> params) throws Exception {
Map<String, Object> result = new HashMap<String, Object>();
result.put("menuName", "{{menuName}}");
result.put("result", {{entityVarName}}Service.get{{entityName}}List());
return result;
}
@Operation(summary = "{{menuName}} 목록 조회")
@PostMapping(value = "list/main.do", params = "method=search")
public Map<String, Object> list_search(@RequestBody Map<String, Object> params) throws Exception {
Map<String, Object> result = new HashMap<String, Object>();
result.put("result", {{entityVarName}}Service.get{{entityName}}List());
return result;
}
@Operation(summary = "{{menuName}} 상세 조회")
@PostMapping(value = "item/main.do", params = "method=get{{entityName}}")
public Map<String, Object> item_get{{entityName}}(@RequestBody Map<String, Object> params) throws Exception {
Map<String, Object> result = new HashMap<String, Object>();
result.put("result", {{entityVarName}}Service.get{{entityName}}(getPrimaryKey(params)));
return result;
}
@Operation(summary = "{{menuName}} 등록")
@PostMapping(value = "item/main.do", params = "method=regist")
public Map<String, Object> item_regist(@RequestBody {{entityName}} {{entityVarName}}) throws Exception {
Map<String, Object> result = new HashMap<String, Object>();
result.put("result", {{entityVarName}}Service.create{{entityName}}({{entityVarName}}));
return result;
}
@Operation(summary = "{{menuName}} 수정")
@PostMapping(value = "item/main.do", params = "method=update")
public Map<String, Object> item_update(@RequestBody {{entityName}} {{entityVarName}}) throws Exception {
if ({{entityVarName}}.{{primaryKeyGetterName}}() == null) {
throw new IllegalArgumentException("{{primaryKeyProperty}} 값은 필수입니다.");
}
Map<String, Object> result = new HashMap<String, Object>();
result.put("result", {{entityVarName}}Service.update{{entityName}}({{entityVarName}}));
return result;
}
@Operation(summary = "{{menuName}} 삭제")
@PostMapping(value = "item/main.do", params = "method=delete")
public String item_delete(@RequestBody Map<String, Object> params) throws Exception {
{{entityVarName}}Service.delete{{entityName}}(getPrimaryKey(params));
return "success";
}
private {{primaryKeyJavaType}} getPrimaryKey(Map<String, Object> params) {
if (params == null || params.get("{{primaryKeyProperty}}") == null) {
throw new IllegalArgumentException("{{primaryKeyProperty}} 값은 필수입니다.");
}
String value = params.get("{{primaryKeyProperty}}").toString();
return {{primaryKeyParseExpression}};
}
}
+46
View File
@@ -0,0 +1,46 @@
package {{daoPackage}};
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Repository;
import {{entityFqcn}};
@Repository
public class {{daoName}} {
private static final String MAPPER = "{{mapperNamespace}}.";
private final SqlSession sqlSession;
public {{daoName}}(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
public List<{{entityName}}> select{{entityName}}List() {
return sqlSession.selectList(MAPPER + "select{{entityName}}List");
}
public {{entityName}} select{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
return sqlSession.selectOne(MAPPER + "select{{entityName}}", {{primaryKeyProperty}});
}
public void insert{{entityName}}({{entityName}} {{entityVarName}}) {
sqlSession.insert(MAPPER + "insert{{entityName}}", {{entityVarName}});
}
public void update{{entityName}}({{entityName}} {{entityVarName}}) {
sqlSession.update(MAPPER + "update{{entityName}}", {{entityVarName}});
}
public void delete{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
sqlSession.delete(MAPPER + "delete{{entityName}}", {{primaryKeyProperty}});
}
}
@@ -0,0 +1,15 @@
package {{entityPackage}};
{{#each entityImports}}
import {{this}};
{{/each}}
import lombok.Data;
@Data
public class {{entityName}} {
{{#each columns}}
private {{javaType}} {{propertyName}};
{{/each}}
}
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="{{mapperNamespace}}">
<resultMap id="{{entityVarName}}ResultMap" type="{{entityFqcn}}">
{{#each columns}}
{{#if primaryKey}}<id property="{{propertyName}}" column="{{columnName}}"/>{{else}}<result property="{{propertyName}}" column="{{columnName}}"/>{{/if}}
{{/each}}
</resultMap>
<select id="select{{entityName}}List" resultMap="{{entityVarName}}ResultMap">
SELECT {{#each columns}}{{columnName}}{{#unless @last}}, {{/unless}}{{/each}}
FROM {{metadata.schema}}.{{tableName}}
ORDER BY {{primaryKeyColumn.columnName}} DESC
</select>
<select id="select{{entityName}}" parameterType="{{primaryKeyMyBatisType}}" resultMap="{{entityVarName}}ResultMap">
SELECT {{#each columns}}{{columnName}}{{#unless @last}}, {{/unless}}{{/each}}
FROM {{metadata.schema}}.{{tableName}}
WHERE {{primaryKeyColumn.columnName}} = #{{'{'}}{{primaryKeyProperty}}{{'}'}}
</select>
<insert id="insert{{entityName}}" parameterType="{{entityFqcn}}"{{#if primaryKeyColumn.autoIncrement}} useGeneratedKeys="true" keyProperty="{{primaryKeyProperty}}"{{/if}}>
INSERT INTO {{metadata.schema}}.{{tableName}} (
{{#each insertColumns}}
{{columnName}}{{#unless @last}},{{/unless}}
{{/each}}
)
VALUES (
{{#each insertColumns}}
#{{'{'}}{{propertyName}}{{'}'}}{{#unless @last}},{{/unless}}
{{/each}}
)
</insert>
<update id="update{{entityName}}" parameterType="{{entityFqcn}}">
UPDATE {{metadata.schema}}.{{tableName}}
SET
{{#each updateColumns}}
{{columnName}} = #{{'{'}}{{propertyName}}{{'}'}}{{#unless @last}},{{/unless}}
{{/each}}
WHERE {{primaryKeyColumn.columnName}} = #{{'{'}}{{primaryKeyProperty}}{{'}'}}
</update>
<delete id="delete{{entityName}}" parameterType="{{primaryKeyMyBatisType}}">
DELETE FROM {{metadata.schema}}.{{tableName}}
WHERE {{primaryKeyColumn.columnName}} = #{{'{'}}{{primaryKeyProperty}}{{'}'}}
</delete>
</mapper>
@@ -0,0 +1,219 @@
<template>
<div class="ui--content-wrapper">
<sdl-breadcrumb></sdl-breadcrumb>
<template v-if="isDetailView">
<div class="ui--list-heading clearfix">
<span class="ui--text-total"><strong>{{menuName}}</strong> {{{detailModeExpression}}}</span>
<div class="float-end">
<button type="button" class="btn btn-secondary btn-sm" @click="moveToList()">목록</button>
</div>
</div>
<div class="ui--form-container">
{{#each formColumns}}
<div class="mb-3">
<label>{{label}}</label>
{{#if isTextarea}}
<textarea v-model.trim="form.{{propertyName}}" class="form-control" rows="6"></textarea>
{{else}}
<input v-model.trim="form.{{propertyName}}" type="text" class="form-control" />
{{/if}}
</div>
{{/each}}
<div class="ui--button-container">
<button type="button" class="btn btn-secondary btn-lg" @click="moveToList()">목록</button>
<button type="button" class="btn btn-primary btn-lg" @click="saveItem()">저장</button>
<button v-if="mode === 'EDIT'" type="button" class="btn btn-danger btn-lg" @click="deleteItem()">삭제</button>
</div>
</div>
</template>
<template v-else>
<div class="ui--list-heading clearfix">
<span class="ui--text-total"><strong>Total</strong> {{{totalCountExpression}}}</span>
<div class="float-end">
<button type="button" class="btn btn-primary btn-sm" @click="moveToCreate()">새 글</button>
</div>
</div>
<table class="table table-bordered ui--table">
<thead>
<tr>
{{#each listColumns}}
<th scope="col">{{label}}</th>
{{/each}}
</tr>
</thead>
<tbody>
<tr v-if="itemList.length === 0">
<td :colspan="{{listColumns.length}}" class="text-center">등록된 데이터가 없습니다.</td>
</tr>
<tr v-for="item in itemList" :key="item.{{primaryKeyProperty}}">
{{#each listColumns}}
<td class="text-center">
{{#if @first}}
<button type="button" class="btn btn-link ui--board-link" @click="openDetail(item.{{propertyName}})">{{{vueItemExpression}}}</button>
{{else}}
{{{vueItemExpression}}}
{{/if}}
</td>
{{/each}}
</tr>
</tbody>
</table>
</template>
</div>
</template>
<script>
import axios from 'axios';
import SDLUtil from '@/utils/SDLUtil';
export default {
name: '{{vueComponentName}}',
data() {
return {
itemList: [],
mode: 'CREATE',
form: this.createEmptyForm(),
};
},
computed: {
isDetailView() {
return Boolean(this.getRouteDetailId()) || this.$route.query.mode === 'create';
},
},
watch: {
'$route.query': {
handler() {
this.syncRouteState();
},
deep: true,
},
},
methods: {
createEmptyForm() {
return {
{{primaryKeyProperty}}: null,
{{#each formColumns}}
{{propertyName}}: '',
{{/each}}
};
},
init() {
this.fetchList();
this.syncRouteState();
},
getRouteDetailId() {
const detailId = this.$route.query.detailId;
return detailId || null;
},
syncRouteState() {
const detailId = this.getRouteDetailId();
if (detailId) {
this.fetchDetail(detailId);
return;
}
if (this.$route.query.mode === 'create') {
this.mode = 'CREATE';
this.form = this.createEmptyForm();
return;
}
this.mode = 'CREATE';
this.form = this.createEmptyForm();
},
async fetchList() {
SDLUtil.showLoadingBar(true);
try {
const { data } = await axios.post(`${SDLUtil.API_URL}{{requestBasePath}}list/main.do?method=search`, JSON.stringify({}), {
headers: { 'Content-Type': 'application/json' },
});
this.itemList = data.result || [];
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
async fetchDetail(detailId) {
SDLUtil.showLoadingBar(true);
try {
const { data } = await axios.post(`${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=get{{entityName}}`, JSON.stringify({ {{primaryKeyProperty}}: detailId }), {
headers: { 'Content-Type': 'application/json' },
});
this.form = {
...this.createEmptyForm(),
...(data.result || {}),
};
this.mode = 'EDIT';
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
moveToCreate() {
this.$router.push({ path: this.$route.path, query: { mode: 'create' } });
},
moveToList() {
this.$router.push({ path: this.$route.path, query: {} });
},
openDetail(detailId) {
this.$router.push({ path: this.$route.path, query: { detailId } });
},
async saveItem() {
SDLUtil.showLoadingBar(true);
try {
const requestUrl = this.mode === 'EDIT'
? `${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=update`
: `${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=regist`;
const { data } = await axios.post(requestUrl, JSON.stringify(this.form), {
headers: { 'Content-Type': 'application/json' },
});
await this.fetchList();
if (data.result && data.result.{{primaryKeyProperty}}) {
this.$router.replace({ path: this.$route.path, query: { detailId: data.result.{{primaryKeyProperty}} } });
}
SDLUtil.alert('저장되었습니다.');
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
deleteItem() {
if (!this.form.{{primaryKeyProperty}}) {
return;
}
SDLUtil.confirm({
msg: '삭제하시겠습니까?',
onOkEvt: () => this.doDelete(),
});
},
async doDelete() {
SDLUtil.showLoadingBar(true);
try {
await axios.post(`${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=delete`, JSON.stringify({ {{primaryKeyProperty}}: this.form.{{primaryKeyProperty}} }), {
headers: { 'Content-Type': 'application/json' },
});
await this.fetchList();
this.moveToList();
SDLUtil.alert('삭제되었습니다.');
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
},
mounted() {
this.$nextTick(() => {
this.init();
});
},
};
</script>
<style scoped>
.ui--board-link {
cursor: pointer;
padding: 0;
}
</style>
@@ -0,0 +1,18 @@
package {{rootPackage}};
import java.util.List;
import {{entityFqcn}};
public interface {{serviceName}} {
List<{{entityName}}> get{{entityName}}List();
{{entityName}} get{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}});
{{entityName}} create{{entityName}}({{entityName}} {{entityVarName}});
{{entityName}} update{{entityName}}({{entityName}} {{entityVarName}});
void delete{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}});
}
@@ -0,0 +1,52 @@
package {{implPackage}};
import java.util.List;
import org.springframework.stereotype.Service;
import {{rootPackage}}.{{serviceName}};
import {{daoPackage}}.{{daoName}};
import {{entityFqcn}};
@Service
public class {{serviceImplName}} implements {{serviceName}} {
private final {{daoName}} {{entityVarName}}Dao;
public {{serviceImplName}}({{daoName}} {{entityVarName}}Dao) {
this.{{entityVarName}}Dao = {{entityVarName}}Dao;
}
@Override
public List<{{entityName}}> get{{entityName}}List() {
return {{entityVarName}}Dao.select{{entityName}}List();
}
@Override
public {{entityName}} get{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
return {{entityVarName}}Dao.select{{entityName}}({{primaryKeyProperty}});
}
@Override
public {{entityName}} create{{entityName}}({{entityName}} {{entityVarName}}) {
{{entityVarName}}Dao.insert{{entityName}}({{entityVarName}});
return {{entityVarName}}Dao.select{{entityName}}({{entityVarName}}.{{primaryKeyGetterName}}());
}
@Override
public {{entityName}} update{{entityName}}({{entityName}} {{entityVarName}}) {
{{entityVarName}}Dao.update{{entityName}}({{entityVarName}});
return {{entityVarName}}Dao.select{{entityName}}({{entityVarName}}.{{primaryKeyGetterName}}());
}
@Override
public void delete{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
{{entityVarName}}Dao.delete{{entityName}}({{primaryKeyProperty}});
}
}